# Filmabend Privater Katalog-Browser für den wöchentlichen Filmabend. Vier Reiter: - **Netflix** — alles im Netflix-DE-Abo ab 50 Stimmen, aktuell 2567 Filme. - **Kanon** — 194 Einträge aus Horror-Bestenlisten, davon 28 Filmreihen. Zusammen 319 Filme, unabhängig davon, wo sie laufen. - **Suche** — freie TMDB-Suche für alles, was in keinem der beiden steht. - **Rad** — Glücksrad über die eingereichten Vorschläge. Daten kommen von TMDB (Provider-Info stammt dort aus der JustWatch-Partnerschaft). Die deutschen Teaser schreibt MiniMax M3 beim Sync und landen in der SQLite-Datei. ## Einrichten ```bash cp .env.example .env # TMDB_TOKEN und MINIMAX_KEY eintragen make install ``` `HOST_SCHLUESSEL` in der `.env` ist frei wählbar, am besten lang und zufällig: ```bash python3 -c "import secrets; print(secrets.token_urlsafe(24))" ``` Ohne Eintrag bleiben die Hostknöpfe für alle zu. ## Namen und Host Der Host pflegt in der Verwaltung die Namensliste, alle anderen wählen sich einen davon aus. Wer keinen Namen wählt, ist Zuschauer: lesen ja, eintragen nein — keine Vorschläge, keine Sammlung, keine Wertungen, keine Wünsche. **Anmeldefilm statt Passwort.** In der Verwaltung hinterlegt jeder für seinen Namen einen Film. Wer den Namen danach wählt, bekommt die Filmsuche vorgesetzt und muss den richtigen anklicken. Der Server gibt den Film nie ungefragt heraus; über das Augensymbol sieht ihn nur der Host und der Name selbst. Ein Name ohne Film bleibt für alle offen. **Hostmodus.** Einmal `https://…/?host=` aufrufen, dann merkt sich die Sitzung das und der Schlüssel fliegt aus der Adresszeile. Danach blendet `Strg+Shift+Alt+S` die Hostknöpfe ein und aus — nützlich, um die Gastansicht zu prüfen. Ohne freigeschaltete Sitzung tut die Tastenkombination nichts. Der Host überspringt den Anmeldefilm und wechselt die Namen frei. Wer den Link kennt, kann das also auch — für einen Filmabend unter Freunden reicht das. ## Sync Holt den Katalog und erzeugt fehlende Teaser. Der erste Lauf dauert einige Minuten, weil für jeden Film ein Teaser generiert wird. ```bash make sync ``` Teaser werden nur einmal erzeugt. Spätere Syncs schreiben nur für neue Filme. Im Betrieb läuft der Sync täglich um 4:30 von selbst. ## Entwickeln ```bash make dev # MediaMTX, Backend :8000, Frontend :5173 make stop # alles wieder aus ``` `make dev` startet MediaMTX als Container mit. Ohne ihn liefe der Stream-Reiter ins Leere und OBS fände keinen Server. Einzeln geht es über `make mtx` und `make mtx-stop`. Der Vite-Dev-Server leitet `/api` ans Backend und `/stream` an MediaMTX. ## Produktiv Auf dem Server im Projektverzeichnis. Läuft hinter Traefik im externen Docker-Netz `web`, wie die anderen Projekte. ```bash make prod # docker compose up -d --build make logs make down ``` Die SQLite-Datei liegt im Bind-Mount `./data`, überlebt also jeden Rebuild. ### Datenbank vom Server holen ```bash make pull # Server-Stand → lokal ``` Zieht erst eine in sich stimmige Kopie über SQLites `backup`, statt die Datei einer laufenden App direkt zu kopieren. Die überschriebene lokale Datenbank bleibt als `.bak` daneben liegen. Den umgekehrten Weg gibt es absichtlich nicht: Die Sammlungen, Vorschläge und Wertungen auf dem Server sind die echten. Ein lokaler Stand würde sie überschreiben. Ohne Docker geht es auch: ```bash make build # Vue nach backend/static cd backend && .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` ## Wie es läuft ``` APScheduler (täglich 4:30) │ ▼ TMDB /discover/movie ──► pro Film /movie/{id} (Bilder, Videos, Übersetzungen) │ │ │ ▼ │ MiniMax M3 schreibt den Teaser ▼ │ SQLite (backend/horror.db) ◄────────────────┘ │ ▼ FastAPI /api/movies ──► Vue-Grid, Detail-Overlay ``` ## API | Endpoint | Zweck | | --- | --- | | `GET /api/movies` | ganzer Katalog, das Frontend filtert clientseitig | | `GET /api/movies/{id}` | einzelner Film | | `GET /api/status` | Anzahl Filme, letzter Sync, läuft gerade einer | | `GET /api/search?q=` | freie TMDB-Suche, nicht auf den Katalog beschränkt | | `GET /api/search/{id}` | volle Daten, erst aus dem Katalog, sonst von TMDB | | `GET /api/health` | für den Docker-Healthcheck | | `POST /api/sync` | Sync von Hand anstoßen | ## Filmreihen Gehört ein Film zu einer Reihe, zeigt die Detailansicht die anderen Teile — aber nur die, die selbst auf Netflix liegen. Grundlage ist TMDBs `belongs_to_collection`. Ein Klick springt direkt zum nächsten Teil. Im Raster stehen Reihen als eine Karte, die durch ihre Teile blättert. Aus 2567 Netflix-Kacheln werden so 2369 — Herr der Ringe belegt eine statt drei. Die Karte startet beim bestbewerteten Teil, danach richtet sich auch ihr Platz im Raster. ## Der Stream-Reiter Du sendest von deinem Rechner per OBS an den Server, alle schauen im Browser zu. Voice läuft weiter über Discord. ``` OBS ──RTMP──► MediaMTX ──LL-HLS──► FastAPI ──► Browser (hls.js) ``` **Senden:** ```bash make stream # an den Server, beendet vorher ein laufendes OBS make stream-local # an den lokalen MediaMTX, zum Ausprobieren make stream-stop # OBS beenden und den Tonweg zurückstellen ``` Strg-C im Terminal beendet OBS nicht zuverlässig — deshalb `make stream-stop` oder einfach das OBS-Fenster schließen. Das Skript legt ein eigenes OBS-Profil `filmabend` an und startet OBS damit. Deine normale OBS-Konfiguration bleibt unberührt. Beim Start fragt der Desktop, welches Fenster übertragen werden soll — erst nach dieser Auswahl fließen Bilder. Bitrate und Auflösung stehen in `~/.config/obs-studio/basic/profiles/filmabend/basic.ini` und werden nur beim ersten Mal geschrieben. Das Sendeziel aktualisiert das Skript jedes Mal. ### Der sendende Rechner braucht BBR Einmalige Einstellung, ohne die der Stream ruckelt: ```bash printf 'tcp_bbr\n' | sudo tee /etc/modules-load.d/bbr.conf printf 'net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr\n' \ | sudo tee /etc/sysctl.d/99-bbr.conf sudo sysctl --system ``` Warum: Kabelanschlüsse puffern den Upload großzügig. Unter Last stieg die Latenz hier von 26 auf 181 ms, ohne einen einzigen Paketverlust. Der Standard-Algorithmus `cubic` liest den Latenzanstieg als Stau und drosselt. Eine einzelne Verbindung kam so auf 4,5 statt 25 Mbit/s — und RTMP nutzt genau eine. OBS warf daraufhin 45 % der Bilder weg. BBR misst stattdessen den tatsächlichen Durchsatz und lässt sich davon nicht täuschen. Danach: 0 % verworfen. `fq` gehört dazu, BBR braucht diese Warteschlange für seine Sendetaktung. Prüfen mit `sysctl -n net.ipv4.tcp_congestion_control` — es muss `bbr` antworten. Von Hand geht es auch: | Feld | Wert | | --- | --- | | Server | `rtmp://horror.marha.de:1935/` | | Streamschlüssel | `filmabend?user=sender&pass=` | Ohne Passwort weist MediaMTX die Verbindung ab. Zuschauen braucht keins. **Ton:** OBS nimmt den Desktop-Ton über PulseAudio mit. Im Browser ginge das nicht — dort liefert `getDisplayMedia` unter Linux nur Tab-Ton, und Firefox gar keinen. Der sendende Rechner bleibt dabei still. Der Ton läuft über eine Null-Senke, deren Monitor OBS abgreift; auf die Lautsprecher geht nichts. Am Pegel im Stream ändert das nichts, weil PipeWire die Monitor-Ports vom Regler der Senke unberührt lässt. `make stream MITHOEREN=1` legt den Ton zusätzlich auf dein gewohntes Gerät. **Warum alle dasselbe Bild sehen** Jeder Player hält 4 Sekunden Abstand zur Live-Kante (`liveSyncDuration`). Wer zurückfällt, spielt bis zu 1,5-fach schneller ab, bis er wieder auf Ziellatenz ist — das regelt hls.js über `maxLiveSyncPlaybackRate`. Ab 7 Sekunden Rückstand springt es stattdessen. Gemessen mit zwei unabhängigen Playern am selben Stream: | Fall | Ergebnis | | --- | --- | | Dauerlauf, 45 s | Latenz konstant 4,05 s, keine Regelung nötig | | Zwei Player | **11 ms** Bildversatz | | Nach 8 s Aussetzer | zurück auf 12 ms in 9 Sekunden | Die Anzeige rechts zeigt Verzögerung, Abweichung, Tempo und die Sendezeit aus dem Manifest. Zwei Zuschauer mit gleicher Sendezeit sehen exakt dasselbe Bild — damit lässt sich der Gleichlauf im Betrieb prüfen. **Grenzen** - Dein Upload muss stabil 6–8 Mbit/s halten. Reicht er nicht, wird es für alle schlechter. - Der Server schiebt pro Zuschauer die volle Bitrate raus. Bei 15 Leuten und 6 Mbit/s sind das 90 Mbit/s und rund 80 GB je Filmabend. Hetzner liefert 10 Gbit und 20 TB im Monat. - Der HLS-Proxy in FastAPI reicht jedes Byte durch Python. Für einen Filmabend genügt das; bei mehr Last gehört eine Traefik-Route direkt auf MediaMTX davor. ## Vorschläge und Rad Im Reiter **Rad** landen die Filmvorschläge. Jede Sitzung darf einen Film vorschlagen — erkannt über ein Cookie, ohne Anmeldung. Ein zweiter Vorschlag ersetzt den ersten, statt abgelehnt zu werden. Vorgeschlagen wird aus der Detailansicht heraus, mit optionalem Namen. Das klappt auch mit Filmen aus dem Suche-Reiter, die gar nicht im Katalog stehen: Titel, Jahr und Poster werden beim Vorschlagen mitgespeichert. Das Rad verteilt die Vorschläge auf gleich große Segmente und dreht fünf bis sieben Runden. Der Zeiger steht oben; das Ziel wird ausgelost und die Drehung so berechnet, dass die Segmentmitte dort landet. **Gedreht wird für alle gleichzeitig.** Die Ziehung passiert im Server, nicht im Browser — sonst sähe jede Seite ein anderes Ergebnis. Wer auf „Drehen" klickt, stößt nur an; das Ergebnis samt Zielwinkel geht per Server-Sent Events an alle offenen Seiten. Neue Vorschläge kommen auf demselben Weg an. Wer die Seite mitten im Dreh lädt, fragt `GET /api/spin` und steigt in die Restanimation ein. Ist der Dreh schon durch, springt das Rad ohne Animation auf die Endstellung — sonst zeigte der Zeiger woanders hin als die Gewinnerkarte. Die Abonnenten liegen im Speicher des Prozesses. Das trägt genau so weit wie ein einzelner uvicorn-Worker; bei mehreren bekäme jeder nur seine eigenen. | Endpoint | Zweck | | --- | --- | | `GET /api/suggestions` | alle Vorschläge, plus die eigene ID | | `POST /api/suggestions` | vorschlagen oder ersetzen | | `DELETE /api/suggestions` | eigenen Vorschlag zurückziehen | | `GET /api/events` | Live-Kanal (SSE) für Dreh und Vorschläge | | `POST /api/spin` | Dreh für alle auslösen | | `GET /api/spin` | laufenden oder letzten Dreh abfragen | ## Die Filterleiste Rechts am Fensterrand, fest stehend. Von oben nach unten: - Suche und Sortierung - Vier Bereichsregler: Bewertungen, Dauer, Jahr, Bewertung. Die Skalen kommen aus dem offenen Reiter und wechseln beim Umschalten mit. Bei den Stimmen ist die Skala gestaucht — linear säßen fast alle Filme im linken Prozent. - Genres zum Zeigen und zum Ausblenden, je als Badge-Gruppe mit "alle" Eingeschlossene Genres wirken als ODER. Ein Ausschluss sticht immer: Ein Actionfilm mit Komödie fliegt raus, wenn Komödie ausgeblendet ist. Ein früherer Versuch mit acht Subgenres (Geister, Zombies, Slasher …) ist wieder raus. TMDBs Schlagwörter reichten dafür nicht: 77 Filme hatten gar keine, der Rest nur allgemeine wie "supernatural horror". ## Der Kanon-Reiter Die Liste steht in `app/canon_list.py`. Ein Eintrag ist ein Film oder eine Reihe. Reihen bekommen eine breitere Karte, die durch ihre Teile blättert — immer ein Teil vollständig sichtbar. `app/canon.py` löst die deutschen Verleihtitel gegen TMDB auf. Das klappt bei 309 von 320 Titeln allein über die Suche. Der Rest steht in `OVERRIDES` mit fester TMDB-ID, weil die Suche danebengriff: - Ohne Jahresangabe liefert TMDB den jüngsten Titel zuerst. "Final Destination" traf deshalb "Bloodlines", "A Quiet Place" den zweiten Teil. - "Das Böse" ist auf Deutsch sowohl Phantasm als auch Amityville Horror. - "Us" traf "Forgive Us All" von 2025. Die Vorlage führte "Black Christmas" und "Jessy – Die Treppe in den Tod" getrennt. Das ist derselbe Film, hier nur einmal. Innerhalb einer Reihe wird nach Erscheinungsjahr sortiert, nicht nach der Reihenfolge der Vorlage. Die gruppiert das Conjuring-Universum nach Sub-Reihen — erst alle Conjuring, dann Nun, dann Annabelle. ## Woher der Netflix-Link kommt Die TMDB-API nennt nur den Anbieternamen, nicht die Titel-ID. Die steht ausschließlich im HTML der Watch-Seite, URL-kodiert im JustWatch-Link. `app/netflix.py` liest sie dort aus und merkt sie sich in der Spalte `netflix_id`, damit spätere Syncs die Seite nicht erneut laden. Der Abruf läuft seriell mit 1,5 Sekunden Pause. Bei dichteren Abrufen antwortet TMDB mit Status 200, aber ohne Anbieterblock — die ID fehlt dann stillschweigend. Filme ohne ID versucht der nächste Sync erneut. Hat ein Film mehrere IDs, gewinnt die erste. Godzilla Minus One liegt bei Netflix in Farbe und in Schwarzweiß. ## Warum der Katalog bei 50 Stimmen abschneidet JustWatch ordnet unbekannte Titel gelegentlich der Netflix-ID eines gleichnamigen bekannten Films zu. TMDB übernimmt das ungeprüft. Der Film steht dann im Katalog, obwohl Netflix ihn nicht hat. Nachgewiesen an fünf Fällen, alle nach demselben Muster: | Eintrag | Stimmen | bekam die ID von | | --- | --- | --- | | CREEP (2014, 8 Min) | 2 | Creep (2014, 1678 Stimmen) | | Muerte, muerte, muerte | 2 | Bodies Bodies Bodies | | LIENZO | 1 | Die Leinwand / Canvas | | NOISE! | 1 | Noise | | The Roommate (2022) | 1 | The Roommate (2011) | Nachweis über TMDBs Watch-Seiten: Dort steht die Netflix-ID im Weiterleitungslink. Zwei Filme mit derselben ID können nicht beide stimmen. Oberhalb von zwei Stimmen trat kein einziger Fall auf. Die Grenze liegt inzwischen bei 50 Stimmen, weil der Katalog alle Genres umfasst — das hält die Fehlzuordnungen ohnehin draußen. ## Grenzen - Verfügbarkeit hinkt Netflix 24–48 h hinterher. So oft liefert JustWatch an TMDB. - Es gibt keine Info, ob ein Film deutsche Synchronisation hat. TMDB kennt nur die Originalsprache, nicht die Tonspuren bei Netflix. - Der Netflix-Knopf führt bei 730 von 2567 Filmen direkt auf die Titelseite. Der Rest landet auf TMDBs Watch-Seite, von dort ist es ein Klick weiter. Pro Sync kommen 250 dazu, meistgesehene zuerst — mehr auf einmal drosselt TMDB. - Die Szenenbilder sind TMDB-Backdrops. Der Sync wirft Duplikate raus, indem er die Bilder selbst vergleicht (dHash plus Farbhistogramm, siehe `app/imagehash.py`). Motive mit Titeltext fliegen ganz raus. Werbemotive ohne Text bleiben drin — die lassen sich nicht von Szenenfotos trennen. - Neun Filme haben gar keine Bilder. Dann zeigt die Detailansicht weniger. - Der Teaser-Ton richtet sich nach dem Genre. Horror, Thriller, Mystery und Krimi bleiben reißerisch, alles andere bekommt die Anweisung, den Ton des Films zu treffen. Vorher wurde aus einer Bollywood-Romanze ein Dorf mit verschlingenden Almwiesen. ## Attribution This product uses the TMDB API but is not endorsed or certified by TMDB. Verfügbarkeitsdaten stammen von JustWatch.