236 lines
9.2 KiB
Markdown
236 lines
9.2 KiB
Markdown
# 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
|
||
```
|
||
|
||
## 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 # Backend :8000, Frontend :5173
|
||
make stop
|
||
```
|
||
|
||
Der Vite-Dev-Server leitet `/api` an den Backend-Port weiter.
|
||
|
||
## 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.
|
||
|
||
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.
|
||
|
||
## 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.
|