team3 7cf7144d6d Rad-Markierung in Gruen
Rot ist die Akzentfarbe und steht sonst fuer Warnungen. Gruen sagt hier
besser, was gemeint ist: der Film ist schon dabei.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XVnRop5c3or3B8xE2hpgZ
2026-09-02 19:43:07 +02:00
2026-09-02 19:43:07 +02:00
2026-08-12 19:30:41 +02:00
2026-08-31 11:54:20 +02:00
2026-08-12 19:30:41 +02:00
2026-08-31 11:54:20 +02:00
2026-08-12 19:30:41 +02:00

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

cp .env.example .env      # TMDB_TOKEN und MINIMAX_KEY eintragen
make install

Sync

Holt den Katalog und erzeugt fehlende Teaser. Der erste Lauf dauert einige Minuten, weil für jeden Film ein Teaser generiert wird.

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

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.

make prod    # docker compose up -d --build
make logs
make down

Die SQLite-Datei liegt im Bind-Mount ./data, überlebt also jeden Rebuild.

Datenbank abgleichen

make push    # lokaler Stand → Server
make pull    # Server-Stand → lokal

Beide ziehen erst eine in sich stimmige Kopie über SQLites backup, statt die Datei einer laufenden App direkt zu kopieren. Die überschriebene Datenbank bleibt als .bak daneben liegen.

make push überschreibt auch die Vorschläge auf dem Server. Wer schon einen Film fürs Rad eingereicht hat, verliert ihn.

Ohne Docker geht es auch:

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:

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:

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=<MTX_SENDEPASSWORT>

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.

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.

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.

Description
No description provided
Readme 111 MiB
Languages
Vue 48%
Python 42.6%
JavaScript 6.9%
Shell 1.4%
Makefile 0.7%
Other 0.4%