diff --git a/README.md b/README.md index 9d9d3f0..565717a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,14 @@ der Flimmerkiste und dem warmen Flimmern, das ein Bildschirm im dunklen Zimmer verbreitet. Metadaten kommen von [TMDB](https://www.themoviedb.org/), die Daten bleiben auf deinem Server. +

+ Die Übersicht von Flimmer +

+ +> **Bebilderte Anleitung:** [docs/ANLEITUNG.md](docs/ANLEITUNG.md) — +> führt einmal durch die ganze Anwendung. +> **Schnittstelle:** [docs/API.md](docs/API.md). + ## Was Flimmer kann - **Tracking** — Serien und Filme mit Status (Schaue gerade, Pausiert, @@ -29,8 +37,13 @@ bleiben auf deinem Server. Gotify oder Webhook (Discord/Slack-kompatibel), pro Nutzer einstellbar. - **Mehrere Nutzer** — jede Person hat ihre eigene Bibliothek; Universen lassen sich teilen. Der erste registrierte Zugang wird Administrator. +- **Übersicht, Kalender und Statistik** — was als Nächstes dran ist, wann welche + Episode läuft, und wie viel Zeit wohin geflossen ist. +- **Import und Export** — aus Trakt.tv oder CSV hinein, als CSV oder JSON hinaus. - **REST-API und PWA** — dokumentierte Schnittstelle mit eigenen API-Schlüsseln, - installierbar auf dem Startbildschirm. + installierbar auf dem Startbildschirm, mit Offline-Rückfall. +- **Papierkorb statt Löschen** — 30 Tage Bedenkzeit, und jede Sichtung lässt sich + mit einem Klick zurücknehmen. ## Schnellstart @@ -165,22 +178,49 @@ npm run dev | `npm run db:migrate` | Migration erzeugen und anwenden | | `npm run db:studio` | Prisma Studio | | `npm run db:seed` | Beispieldaten | +| `npm test` | Testlauf gegen die Entwicklungsdatenbank (braucht keinen TMDB-Schlüssel) | | `npm run icons` | Favicon und PWA-Icons aus dem SVG-Logo neu erzeugen | +| `npm run screenshots` | Screenshots der Anleitung neu aufnehmen | + +Ohne TMDB-Schlüssel lässt sich die Suche mit einem lokalen Ersatz ausprobieren, +der aus den Beispieldaten antwortet: + +```bash +node --import tsx scripts/tmdb-stub.mjs 3099 +TMDB_API_KEY=stub TMDB_BASE_URL=http://127.0.0.1:3099/3 npm run dev +``` Aufbau des Projekts: ``` src/ - app/ Routen (App Router), API-Endpunkte + app/ Routen (App Router), API-Endpunkte, Server-Aktionen components/ Wiederverwendbare Bausteine - lib/ Serverseitige Logik: DB, Auth, TMDB, Benachrichtigungen + lib/ Serverseitige Logik: DB, Auth, TMDB, Tracking, Statistik, + Universen, Benachrichtigungen, Abgleich, Import/Export i18n/ Oberflächentexte -prisma/ Schema, Migrationen, Seed +prisma/ Schema, Migrationen, Seed samt Beispielkatalog +tests/ Testlauf mit nachgebildeten TMDB-Antworten public/brand/ Logo in allen Varianten docker/ Entrypoint des Containers -docs/ Anleitung, Design-Dokumentation +docs/ Anleitung mit Screenshots, API, Design-Dokumentation ``` +## Hintergrundlauf + +Einmal täglich frischt Flimmer laufende Serien bei TMDB auf, meldet neue +Episoden, prüft die Verfügbarkeit von Watchlist-Titeln und leert den +Papierkorb. Gesteuert wird das über `ENABLE_SCHEDULER` und `SYNC_HOUR`. + +Wer lieber einen eigenen Cron benutzt, setzt `ENABLE_SCHEDULER=false`, vergibt +ein `CRON_SECRET` und ruft auf: + +```bash +curl -X POST -H "Authorization: Bearer $CRON_SECRET" https://flimmer.example.com/api/cron/sync +``` + +In der Verwaltung lässt sich der Lauf jederzeit von Hand anstoßen. + ## Technik Next.js (App Router) mit TypeScript, PostgreSQL über Prisma, Tailwind CSS. diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md new file mode 100644 index 0000000..3619421 --- /dev/null +++ b/docs/ANLEITUNG.md @@ -0,0 +1,360 @@ +

+ Flimmer +

+ +# Flimmer — Anleitung + +Diese Anleitung führt einmal durch die ganze Anwendung: vom ersten Konto über +das tägliche Abhaken bis zu Universen, Statistik und Schnittstelle. + +Die Bilder stammen aus einer Instanz mit Beispieldaten (`npm run db:seed`). +Die Poster darin sind erfunden — mit hinterlegtem TMDB-Schlüssel stehen dort +die echten. + +## Inhalt + +1. [Erster Start](#erster-start) +2. [Die Übersicht](#die-übersicht) +3. [Titel finden und aufnehmen](#titel-finden-und-aufnehmen) +4. [Eine Serie verfolgen](#eine-serie-verfolgen) +5. [Die Bibliothek](#die-bibliothek) +6. [Universen](#universen) +7. [Kalender](#kalender) +8. [Statistik](#statistik) +9. [Einstellungen](#einstellungen) +10. [Benachrichtigungen](#benachrichtigungen) +11. [Import und Export](#import-und-export) +12. [API-Schlüssel](#api-schlüssel) +13. [Papierkorb](#papierkorb) +14. [Verwaltung](#verwaltung) +15. [Auf dem Handy](#auf-dem-handy) +16. [Helles Farbschema](#helles-farbschema) + +--- + +## Erster Start + +![Anmeldung](screenshots/01-anmeldung.webp) + +Nach der Installation ruf die Adresse deiner Instanz auf. Solange es noch kein +einziges Konto gibt, ist die Registrierung offen — **der erste Zugang wird +automatisch Administrator**. Lege ihn also gleich für dich an. + +Danach kannst du die Registrierung in der [Verwaltung](#verwaltung) schließen, +damit sich niemand sonst anmeldet. + +> Ist SSO eingerichtet (`OIDC_*` in der `.env`), erscheint hier zusätzlich ein +> Knopf für die Anmeldung über den eigenen Anbieter. + +--- + +## Die Übersicht + +![Übersicht](screenshots/02-uebersicht.webp) + +Die Startseite beantwortet drei Fragen: + +- **Weiterschauen** — welche Serien laufen gerade, und welche Episode ist als + Nächstes dran. Der Knopf **+1 Episode** hakt genau diese Folge ab; du musst + die Serie dafür nicht öffnen. +- **Demnächst** — welche Episoden deiner Serien in den nächsten vier Wochen + ausgestrahlt werden. +- **Neu verfügbar** — welche Titel von deiner Watchlist zuletzt auf einem + Streamingdienst in deiner Region aufgetaucht sind. + +Ist die Bibliothek noch leer, steht hier stattdessen der Weg zur Suche. + +--- + +## Titel finden und aufnehmen + +![Suche](screenshots/03-suche.webp) + +Über **Suchen** durchsuchst du TMDB. Die Treffer lassen sich auf *Alles*, +*Serien* oder *Filme* eingrenzen; die Suchanfrage steht in der Adresszeile und +ist damit teilbar. + +Ein Klick auf einen Treffer holt die vollständigen Daten — bei Serien alle +Staffeln und Episoden — und öffnet die Detailseite. Von dort nimmst du den +Titel auf, wahlweise als **Ich schaue das gerade** oder **Auf die Watchlist**. + +> **Nichts dabei?** Unter *Titel selbst anlegen* trägst du etwas ein, das TMDB +> nicht kennt — Heimvideos, Vorträge, regionale Produktionen. Solche Titel +> werden vom automatischen Abgleich nie angefasst. + +--- + +## Eine Serie verfolgen + +![Serie](screenshots/04-serie.webp) + +Die Detailseite ist der Ort, an dem du eine Serie führst: + +- **Fortschrittsbalken** mit gesehenen Episoden und verbleibender Laufzeit, + berechnet aus der echten Episodenanzahl. +- **Als Nächstes dran** nennt Staffel, Episode, Titel und Ausstrahlungsdatum. + Der Knopf **+1 Episode** hakt sie ab. +- Der **Status** (Schaue gerade, Watchlist, Pausiert, Abgeschlossen, + Abgebrochen) ergibt sich beim Abhaken von selbst: die erste Episode startet + die Serie, die letzte schließt sie ab. Pausiert und Abgebrochen bleiben + stehen — wer bewusst pausiert hat, will das nicht durch ein Häkchen + aufgehoben bekommen. +- In der **Staffelliste** hakst du einzelne Episoden ab, ganze Staffeln auf + einmal, oder über *Bis hierhin gesehen* alles Vorherige. Beim Abhaken glimmt + die Zeile kurz warm auf. +- Rechts stehen **Bewertung** (1 bis 10), **wo du das schaust** (mit Vorschlag + aus der Streaming-Verfügbarkeit), **Tags** und ein **Notizfeld**. + +Jede Sichtung meldet sich unten kurz zurück und lässt sich mit **Rückgängig** +zurücknehmen. + +Ist eine Serie durch, erscheint **Noch einmal schauen** — der Fortschritt +beginnt von vorn, die alten Sichtungen bleiben für die Statistik erhalten. Die +Karte zeigt dann „2. Durchlauf". + +--- + +## Die Bibliothek + +![Bibliothek](screenshots/05-bibliothek.webp) + +Alles, was du verfolgst, als Posterraster. Jede Kachel zeigt Status, +Bewertung und — bei Serien — den Fortschritt. + +Filtern kannst du nach: + +| Filter | Wirkung | +|---|---| +| Volltextsuche | Titel und Originaltitel | +| Status | Schaue gerade, Watchlist, Pausiert, Abgeschlossen, Abgebrochen | +| Typ | nur Serien oder nur Filme | +| Genre | alle Genres, die in deiner Bibliothek vorkommen | +| Dienst | Streamingdienste deiner Titel | +| Tag | deine eigenen Schlagworte | + +Sortieren geht nach zuletzt aktualisiert, zuletzt hinzugefügt, Titel, Bewertung +oder Erscheinungsjahr. Die gesamte Auswahl steht in der Adresszeile — ein +Lesezeichen auf „alle pausierten Krimis" ist damit möglich. + +--- + +## Universen + +![Universen](screenshots/06-universen.webp) + +Ein Universum bündelt Titel, die zusammengehören — MCU, Star Wars, Wizarding +World — und zwar **Filme und Serien gemeinsam**. + +![Universum](screenshots/07-universum.webp) + +Auf der Detailseite steht oben der Gesamtfortschritt: wie viel abgeschlossen +ist, wie viel Zeit schon drinsteckt und wie lange es bis zum Ende noch dauert. +Der erste noch offene Eintrag ist als **Als Nächstes dran** hervorgehoben. + +**Mehrere Reihenfolgen** sind der Kern der Sache: + +- Die **Erscheinungsreihenfolge** ist immer da und wird aus den + Erscheinungsdaten berechnet — sie stimmt damit auch, wenn später etwas + dazukommt. +- Über **+ Reihenfolge** legst du weitere an, etwa *Chronologisch* oder + *Für Einsteiger*. Sortiert wird per Ziehen oder über die Pfeile neben jedem + Eintrag; gespeichert wird sofort. + +Titel nimmst du rechts aus deiner Bibliothek auf. Gehört ein Film zu einer +TMDB-Reihe (etwa „Spider-Man Collection"), lässt sich die ganze Reihe mit einem +Klick übernehmen. + +**Teilen** geht mit anderen Konten derselben Instanz, wahlweise nur zum Ansehen +oder zum gemeinsamen Pflegen. Der Fortschritt bleibt dabei persönlich: jede +Person sieht ihren eigenen Stand. + +--- + +## Kalender + +![Kalender](screenshots/09-kalender.webp) + +Das Monatsraster zeigt, wann Episoden deiner Serien laufen. Tage mit Terminen +sind hinterlegt, der heutige Tag trägt den Signaturverlauf. Unter dem Raster +stehen dieselben Termine noch einmal als Liste mit vollem Datum. + +Mit den Pfeilen blätterst du durch die Monate, **Heute** springt zurück. + +Abgebrochene Serien tauchen hier nicht auf. Abgeschlossene schon — wer bei +einer laufenden Serie auf dem Stand ist, will die nächste Folge erst recht +sehen. + +--- + +## Statistik + +![Statistik](screenshots/10-statistik.webp) + +Oben die **Gesamtschauzeit** in Stunden und umgerechnet in Tage am Stück, daneben +Episoden, Filme und abgeschlossene Titel. + +- **Die letzten zwölf Monate** — abgehakte Episoden und Filme je Monat. + Beim Überfahren einer Säule erscheinen die genauen Zahlen samt Schauzeit. +- **Nach Streamingdienst** — wo du tatsächlich geschaut hast. Grundlage ist das + Feld *wo schaue ich das*, nicht die bloße Verfügbarkeit. +- **Nach Genre** — Titel mit mehreren Genres zählen in jedem mit. +- **Jahr in Zahlen** — Schauzeit, Episoden, Filme, neu Angefangenes, + Abgeschlossenes, der stärkste Monat und der Titel mit der meisten Zeit. + Über die Jahreszahlen wechselst du das Jahr. +- Darunter **wo die meiste Zeit hinging** und **deine besten Bewertungen**. + +Alles ist aus den Sichtungen gerechnet; Rewatches zählen mit. + +--- + +## Einstellungen + +![Einstellungen](screenshots/11-einstellungen.webp) + +Hier stellst du Anzeigename und **Region** ein — Letztere bestimmt, welche +Streamingdienste angezeigt werden (Standard: DE). + +Darunter legst du fest, **wovon Flimmer dir berichten soll**: neue Episoden, +neue Releases, neu verfügbare Watchlist-Titel. + +Wenn der Betreiber öffentliche Profile erlaubt, kannst du deins freischalten +und eine Adresse wählen. Die Profilseite zeigt Zahlen und zuletzt Gesehenes — +**keine** Notizen, keine Watchlist, keine E-Mail-Adresse. + +Das Passwort änderst du im Abschnitt darunter; danach wirst du auf allen +Geräten abgemeldet. + +--- + +## Benachrichtigungen + +![Benachrichtigungen](screenshots/12-benachrichtigungen.webp) + +*Wovon* berichtet wird, steht im Profil — *wohin*, hier. Vier Wege stehen zur +Wahl: + +| Kanal | Nötige Angaben | +|---|---| +| **E-Mail** | nichts (oder eine abweichende Adresse); der SMTP-Zugang kommt vom Betreiber | +| **ntfy** | Server (Standard `https://ntfy.sh`) und Topic, optional ein Token | +| **Gotify** | Serveradresse und Anwendungs-Token | +| **Webhook** | die Zieladresse — funktioniert mit Discord, Slack und Mattermost | + +Jeder Kanal lässt sich einzeln stummschalten und mit **Testen** ausprobieren. +Unten steht, was zuletzt verschickt wurde — auch fehlgeschlagene Versuche mit +Grund. + +Dieselbe Meldung kommt nie zweimal: Flimmer merkt sich, was schon draußen ist. + +--- + +## Import und Export + +![Import und Export](screenshots/14-daten.webp) + +**Import** aus zwei Quellen: + +- **CSV**, wie es Tabellenprogramme und viele Tracker ausgeben. Die Kopfzeile + braucht mindestens eine Titelspalte (`Titel`, `title`, `name` oder `show`); + erkannt werden außerdem Typ, Status, Bewertung, TMDB-ID, Jahr und Notiz. + Komma und Semikolon als Trennzeichen funktionieren beide. +- **Trakt.tv** — ein JSON-Export aus Watchlist, History, Ratings oder + Collection. Flimmer sucht sich die Einträge selbst heraus. + +Der Import **überschreibt nichts**. Titel, die schon in deiner Bibliothek +liegen, bleiben unangetastet; es kommt nur dazu, was fehlt. + +**Export** als JSON (mit allen Sichtungen und Universen) oder als CSV für +Tabellenkalkulationen. + +--- + +## API-Schlüssel + +![API-Schlüssel](screenshots/13-api.webp) + +Für Skripte und eigene Anwendungen. Ein Schlüssel ist **einmalig** sichtbar — +danach liegt in der Datenbank nur ein Hash. Er hat dieselben Rechte wie dein +Konto; zieh ihn zurück, sobald du ihn nicht mehr brauchst. + +```bash +curl -H "Authorization: Bearer flm_…" \ + https://flimmer.example.com/api/v1/bibliothek?status=WATCHING +``` + +Alle Endpunkte stehen in [`API.md`](API.md); die maschinenlesbare Fassung +liegt unter `/api/v1/openapi.json`. + +--- + +## Papierkorb + +![Papierkorb](screenshots/16-papierkorb.webp) + +Entfernte Titel landen für **30 Tage** im Papierkorb und behalten dabei ihren +Fortschritt. Von dort lassen sie sich wiederherstellen oder sofort endgültig +löschen. Nimmst du denselben Titel erneut auf, lebt der alte Eintrag samt +Sichtungen wieder auf. + +Nach Ablauf der Frist räumt der tägliche Hintergrundlauf auf. + +--- + +## Verwaltung + +![Verwaltung](screenshots/15-verwaltung.webp) + +Nur für Administratoren. Hier lässt sich + +- der **Name der Instanz** ändern, +- die **Registrierung** öffnen oder schließen, +- **öffentliche Profile** instanzweit erlauben oder abschalten, +- der **Abgleich** von Hand anstoßen, +- und der Zustand der Instanz ablesen: Anzahl Konten, Umfang des Katalogs und + ob TMDB, SMTP, Zeitplan und OIDC eingerichtet sind. + +In der Kontenliste vergibst du Verwaltungsrechte und sperrst Konten. Ein +gesperrtes Konto wird sofort von allen Geräten abgemeldet. Die letzte +Administration lässt sich nicht entmachten, und sich selbst kann niemand +sperren. + +--- + +## Auf dem Handy + +![Auf dem Handy](screenshots/17-mobil-uebersicht.webp) + +Die Oberfläche ist für schmale Geräte ausgelegt: die Navigation rutscht in eine +eigene, wischbare Zeile, Karten stapeln sich untereinander, und der Knopf +**+1 Episode** bleibt groß genug für den Daumen. + +Flimmer ist als **PWA** installierbar — im Browsermenü *Zum Startbildschirm +hinzufügen*. Danach startet die App im eigenen Fenster ohne Adresszeile. +Bereits geladene Seiten und Poster bleiben auch ohne Verbindung sichtbar; +statt einer Fehlerseite erscheint dann ein Hinweis. + +--- + +## Helles Farbschema + +![Helles Farbschema](screenshots/08-hell.webp) + +Über den Knopf im Kopfbereich wechselst du zwischen **Dunkel**, **Hell** und +**System**. Die Wahl bleibt auf dem Gerät gespeichert und greift, bevor die +Seite gezeichnet wird — es blitzt nichts auf. + +--- + +## Und wenn etwas fehlt? + +- **Die Suche findet nichts** → Es fehlt der TMDB-Schlüssel. Trag + `TMDB_API_KEY` in die `.env` ein und starte die Instanz neu. +- **Keine Streamingdienste** → Prüfe die Region in den Einstellungen. TMDB hat + nicht für jedes Land Daten zu jedem Titel. +- **Keine Benachrichtigungen** → Erst im Profil auswählen, *wovon* berichtet + werden soll, dann einen Kanal anlegen und mit *Testen* prüfen. +- **Neue Episoden fehlen** → Der Abgleich läuft einmal täglich. In der + Verwaltung lässt er sich sofort anstoßen. + +Alles Weitere zur Installation, zum Aktualisieren und zum Backup steht in der +[README](../README.md). diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..86b41df --- /dev/null +++ b/docs/API.md @@ -0,0 +1,190 @@ +# Flimmer — REST-API + +Alles, was die Oberfläche kann, geht auch über die Schnittstelle: Bibliothek +lesen und pflegen, Episoden abhaken, suchen, Kalender und Statistik abrufen, +Universen verwalten, Daten exportieren. + +Die maschinenlesbare Beschreibung liegt unter +`GET /api/v1/openapi.json` (OpenAPI 3.1) — damit lassen sich Clients erzeugen +oder die Endpunkte in Insomnia, Bruno oder Postman importieren. + +## Anmeldung + +Jede Anfrage trägt einen persönlichen API-Schlüssel im `Authorization`-Kopf: + +```bash +curl -H "Authorization: Bearer flm_a1b2c3d4.xxxxxxxxxxxx" \ + https://flimmer.example.com/api/v1/me +``` + +Schlüssel legst du unter **Einstellungen → API-Schlüssel** an. Der Klartext ist +**einmalig** sichtbar; danach liegt in der Datenbank nur ein Hash. Ein Schlüssel +hat dieselben Rechte wie dein Konto — behandle ihn wie ein Passwort und zieh ihn +zurück, sobald du ihn nicht mehr brauchst. + +Aus dem Browser heraus funktioniert stattdessen das Sitzungscookie; dieselben +Endpunkte bedienen also Skripte und das eigene Frontend. + +## Antwortformat + +Alle Antworten sind JSON. Zeitangaben in ISO 8601, Laufzeiten in Minuten. +Fehler tragen immer ein Feld `error`: + +```json +{ "error": "Diesen Eintrag gibt es nicht." } +``` + +| Status | Bedeutung | +|---|---| +| 200 | In Ordnung | +| 401 | Kein oder ungültiger Schlüssel | +| 404 | Nicht vorhanden (oder nicht deins) | +| 422 | Anfrage unvollständig — `details` nennt die Felder | +| 502 / 503 | TMDB nicht erreichbar bzw. kein Schlüssel hinterlegt | + +## Endpunkte + +### Konto + +```http +GET /api/v1/me +``` + +### Bibliothek + +```http +GET /api/v1/bibliothek?status=WATCHING&typ=SERIES&genre=Drama&q=nord&limit=50&offset=0 +POST /api/v1/bibliothek +``` + +Filter: `q`, `status`, `typ`, `genre`, `tag`, `dienst`, `limit`, `offset`. +Status: `WATCHING`, `PAUSED`, `COMPLETED`, `DROPPED`, `WATCHLIST`. + +Aufnehmen — entweder mit interner Kennung oder direkt mit der TMDB-Kennung: + +```bash +curl -X POST -H "Authorization: Bearer $KEY" -H "content-type: application/json" \ + -d '{"tmdbId": 1396, "type": "SERIES", "status": "WATCHING"}' \ + https://flimmer.example.com/api/v1/bibliothek +``` + +```http +GET /api/v1/bibliothek/{entryId} +PATCH /api/v1/bibliothek/{entryId} +DELETE /api/v1/bibliothek/{entryId} +``` + +`GET` liefert den Eintrag samt `progress` und `nextEpisode`. +`PATCH` ändert `status`, `rating` (1–10), `notes`, `whereToWatch`, `favorite`. +`DELETE` legt den Eintrag in den Papierkorb — dort bleibt er 30 Tage. + +### Sichtungen + +```http +GET /api/v1/bibliothek/{entryId}/sichtungen +POST /api/v1/bibliothek/{entryId}/sichtungen +DELETE /api/v1/bibliothek/{entryId}/sichtungen +``` + +`POST` kennt fünf Formen: + +| Rumpf | Wirkung | +|---|---| +| `{"action":"next"}` | Die nächste offene Episode abhaken | +| `{"action":"movie"}` | Einen Film als gesehen markieren | +| `{"action":"episodes","episodeIds":["…"]}` | Eine Auswahl abhaken | +| `{"action":"season","seasonId":"…"}` | Eine ganze Staffel | +| `{"action":"upTo","episodeId":"…"}` | Alles bis einschließlich dieser Episode | + +Die Antwort trägt `watchIds` — damit lässt sich genau diese Aktion zurücknehmen: + +```bash +# Nächste Episode abhaken +IDS=$(curl -s -X POST -H "Authorization: Bearer $KEY" -H "content-type: application/json" \ + -d '{"action":"next"}' \ + https://flimmer.example.com/api/v1/bibliothek/$ENTRY/sichtungen | jq -c .watchIds) + +# … und wieder zurücknehmen +curl -X DELETE -H "Authorization: Bearer $KEY" -H "content-type: application/json" \ + -d "{\"watchIds\": $IDS}" \ + https://flimmer.example.com/api/v1/bibliothek/$ENTRY/sichtungen +``` + +Alternativ nimmt `DELETE` auch `{"episodeIds":["…"]}` — dann verschwindet die +Markierung dieser Episoden im aktuellen Durchlauf. + +### Suche + +```http +GET /api/v1/suche?q=nordlicht&typ=serie +``` + +Durchsucht TMDB. Die Treffer tragen `tmdbId` und `type` — direkt verwendbar für +`POST /api/v1/bibliothek`. Ohne hinterlegten TMDB-Schlüssel antwortet der +Endpunkt mit 503. + +### Kalender + +```http +GET /api/v1/kalender?von=2026-09-01&bis=2026-09-30 +``` + +Kommende Episoden der getrackten Serien. Ohne Zeitraum: die nächsten 30 Tage. + +### Statistik + +```http +GET /api/v1/statistik?jahr=2026 +``` + +Liefert `totals`, `monthly`, `providers`, `genres` und `yearReview` auf einmal. + +### Universen + +```http +GET /api/v1/universen +POST /api/v1/universen +GET /api/v1/universen/{id}?reihenfolge={orderId} +POST /api/v1/universen/{id} +``` + +`GET /api/v1/universen/{id}` gibt die Einträge in der gewählten Reihenfolge +zurück, dazu `progress` und `nextUp`. Ohne `reihenfolge` gilt die +Erscheinungsreihenfolge. + +### Export + +```http +GET /api/v1/export?format=json +GET /api/v1/export?format=csv +``` + +JSON enthält zusätzlich alle Sichtungen und Universen. + +## Beispiel: Serien mit offener Episode auflisten + +```bash +#!/usr/bin/env bash +set -euo pipefail + +KEY="flm_…" +HOST="https://flimmer.example.com" + +curl -s -H "Authorization: Bearer $KEY" \ + "$HOST/api/v1/bibliothek?status=WATCHING&typ=SERIES" \ +| jq -r '.items[] | "\(.title): \(.watched)/\(.total)"' +``` + +## Grenzen + +Es gibt bewusst keine Ratenbegrenzung: Flimmer läuft auf deinem eigenen Server, +und ein Limit gegen dich selbst hilft niemandem. Wer die Instanz öffentlich +erreichbar macht, setzt so etwas besser im Reverse Proxy. + +Der Abgleich mit TMDB lässt sich über einen eigenen Endpunkt anstoßen — der +nimmt allerdings das Cron-Geheimnis, nicht einen API-Schlüssel: + +```bash +curl -X POST -H "Authorization: Bearer $CRON_SECRET" \ + https://flimmer.example.com/api/cron/sync +``` diff --git a/docs/screenshots/01-anmeldung.webp b/docs/screenshots/01-anmeldung.webp new file mode 100644 index 0000000..743ff5d Binary files /dev/null and b/docs/screenshots/01-anmeldung.webp differ diff --git a/docs/screenshots/02-uebersicht.webp b/docs/screenshots/02-uebersicht.webp new file mode 100644 index 0000000..866f6e3 Binary files /dev/null and b/docs/screenshots/02-uebersicht.webp differ diff --git a/docs/screenshots/03-suche.webp b/docs/screenshots/03-suche.webp new file mode 100644 index 0000000..29c0aff Binary files /dev/null and b/docs/screenshots/03-suche.webp differ diff --git a/docs/screenshots/04-serie.webp b/docs/screenshots/04-serie.webp new file mode 100644 index 0000000..7e4a5dc Binary files /dev/null and b/docs/screenshots/04-serie.webp differ diff --git a/docs/screenshots/05-bibliothek.webp b/docs/screenshots/05-bibliothek.webp new file mode 100644 index 0000000..f33c7b7 Binary files /dev/null and b/docs/screenshots/05-bibliothek.webp differ diff --git a/docs/screenshots/06-universen.webp b/docs/screenshots/06-universen.webp new file mode 100644 index 0000000..c818e39 Binary files /dev/null and b/docs/screenshots/06-universen.webp differ diff --git a/docs/screenshots/07-universum.webp b/docs/screenshots/07-universum.webp new file mode 100644 index 0000000..9627667 Binary files /dev/null and b/docs/screenshots/07-universum.webp differ diff --git a/docs/screenshots/08-hell.webp b/docs/screenshots/08-hell.webp new file mode 100644 index 0000000..9a5c88d Binary files /dev/null and b/docs/screenshots/08-hell.webp differ diff --git a/docs/screenshots/09-kalender.webp b/docs/screenshots/09-kalender.webp new file mode 100644 index 0000000..60edd3b Binary files /dev/null and b/docs/screenshots/09-kalender.webp differ diff --git a/docs/screenshots/10-statistik.webp b/docs/screenshots/10-statistik.webp new file mode 100644 index 0000000..5a914a4 Binary files /dev/null and b/docs/screenshots/10-statistik.webp differ diff --git a/docs/screenshots/11-einstellungen.webp b/docs/screenshots/11-einstellungen.webp new file mode 100644 index 0000000..c3f3a39 Binary files /dev/null and b/docs/screenshots/11-einstellungen.webp differ diff --git a/docs/screenshots/12-benachrichtigungen.webp b/docs/screenshots/12-benachrichtigungen.webp new file mode 100644 index 0000000..0279981 Binary files /dev/null and b/docs/screenshots/12-benachrichtigungen.webp differ diff --git a/docs/screenshots/13-api.webp b/docs/screenshots/13-api.webp new file mode 100644 index 0000000..e05d346 Binary files /dev/null and b/docs/screenshots/13-api.webp differ diff --git a/docs/screenshots/14-daten.webp b/docs/screenshots/14-daten.webp new file mode 100644 index 0000000..52d09fe Binary files /dev/null and b/docs/screenshots/14-daten.webp differ diff --git a/docs/screenshots/15-verwaltung.webp b/docs/screenshots/15-verwaltung.webp new file mode 100644 index 0000000..89d9cad Binary files /dev/null and b/docs/screenshots/15-verwaltung.webp differ diff --git a/docs/screenshots/16-papierkorb.webp b/docs/screenshots/16-papierkorb.webp new file mode 100644 index 0000000..17190d9 Binary files /dev/null and b/docs/screenshots/16-papierkorb.webp differ diff --git a/docs/screenshots/17-mobil-uebersicht.webp b/docs/screenshots/17-mobil-uebersicht.webp new file mode 100644 index 0000000..7226359 Binary files /dev/null and b/docs/screenshots/17-mobil-uebersicht.webp differ diff --git a/docs/screenshots/18-mobil-serie.webp b/docs/screenshots/18-mobil-serie.webp new file mode 100644 index 0000000..e437316 Binary files /dev/null and b/docs/screenshots/18-mobil-serie.webp differ diff --git a/package.json b/package.json index 4dcd7eb..181ca1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flimmer", - "version": "0.5.0", + "version": "0.6.0", "private": true, "description": "Flimmer — selbst hostbarer Serien- und Film-Tracker", "license": "MIT", @@ -15,8 +15,9 @@ "db:studio": "prisma studio", "db:seed": "tsx prisma/seed.ts", "icons": "tsx scripts/generate-icons.ts", - "screenshots": "tsx scripts/screenshots.ts", - "test": "node --env-file-if-exists=.env --conditions=react-server --test-concurrency=1 --import tsx --test tests/*.test.ts" + "screenshots": "node scripts/screenshots.mjs && node scripts/optimize-screenshots.mjs", + "test": "node --env-file-if-exists=.env --conditions=react-server --test-concurrency=1 --import tsx --test tests/*.test.ts", + "tmdb:stub": "node --import tsx scripts/tmdb-stub.mjs" }, "dependencies": { "@prisma/adapter-pg": "^7.10.0", diff --git a/scripts/optimize-screenshots.mjs b/scripts/optimize-screenshots.mjs new file mode 100644 index 0000000..b4bfd24 --- /dev/null +++ b/scripts/optimize-screenshots.mjs @@ -0,0 +1,34 @@ +import sharp from "sharp"; +import { readdir, unlink, stat } from "node:fs/promises"; +import { join } from "node:path"; + +/** + * Verkleinert die aufgenommenen Screenshots für das Repository. + * Die Aufnahme läuft mit doppelter Pixeldichte (scharfe Schrift), abgelegt + * wird die halbe Kantenlänge als WebP — das spart rund 90 Prozent. + */ +const DIR = "docs/screenshots"; +const files = (await readdir(DIR)).filter((name) => name.endsWith(".png")); + +let before = 0; +let after = 0; + +for (const name of files) { + const source = join(DIR, name); + before += (await stat(source)).size; + + const image = sharp(source); + const meta = await image.metadata(); + const target = join(DIR, name.replace(/\.png$/, ".webp")); + + await image + .resize({ width: Math.round((meta.width ?? 1400) / 2) }) + .webp({ quality: 82, effort: 5 }) + .toFile(target); + + after += (await stat(target)).size; + await unlink(source); +} + +const mb = (bytes) => (bytes / 1024 / 1024).toFixed(1); +console.log(`${files.length} Bilder: ${mb(before)} MB -> ${mb(after)} MB`); diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs new file mode 100644 index 0000000..760c5d7 --- /dev/null +++ b/scripts/screenshots.mjs @@ -0,0 +1,99 @@ +import { chromium } from "playwright"; +import fs from "node:fs"; + +const OUT = "docs/screenshots"; +const BASE = "http://localhost:3011"; +fs.mkdirSync(OUT, { recursive: true }); + +const browser = await chromium.launch(); +const errs = []; + +async function session(width, height, theme) { + const ctx = await browser.newContext({ + viewport: { width, height }, + deviceScaleFactor: 2, + colorScheme: theme === "light" ? "light" : "dark", + locale: "de-DE", + timezoneId: "Europe/Berlin", + }); + await ctx.addInitScript((mode) => localStorage.setItem("flimmer-theme", mode), theme); + // Der Entwicklungs-Indikator von Next hat in einer Anleitung nichts verloren. + await ctx.addStyleTag?.({ content: "nextjs-portal { display: none !important; }" }); + const page = await ctx.newPage(); + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }).catch(() => {}); + page.on("pageerror", (e) => errs.push(String(e).slice(0, 160))); + return { ctx, page }; +} + +async function login(page) { + await page.goto(`${BASE}/anmelden`); + await page.fill("#email", "demo@flimmer.local"); + await page.fill("#password", "flimmer-demo-2026"); + await Promise.all([page.waitForURL(`${BASE}/`), page.click("button[type=submit]")]); +} + +async function shot(page, name, url, opts = {}) { + await page.goto(`${BASE}${url}`, { waitUntil: "networkidle" }); + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }).catch(() => {}); + await page.waitForTimeout(opts.wait ?? 900); + if (opts.before) await opts.before(page); + await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: Boolean(opts.full) }); + console.log(" ", name); +} + +// --- Anmeldung (ohne Sitzung) --- +{ + const { ctx, page } = await session(1280, 800, "dark"); + await shot(page, "01-anmeldung", "/anmelden"); + await ctx.close(); +} + +// --- Angemeldet, dunkel --- +{ + const { ctx, page } = await session(1400, 900, "dark"); + await login(page); + + await shot(page, "02-uebersicht", "/", { full: true }); + await shot(page, "03-suche", "/suche?q=sternenpfad"); + await shot(page, "05-bibliothek", "/bibliothek"); + await shot(page, "09-kalender", "/kalender"); + await shot(page, "10-statistik", "/statistik", { full: true }); + await shot(page, "11-einstellungen", "/einstellungen"); + await shot(page, "12-benachrichtigungen", "/einstellungen/benachrichtigungen"); + await shot(page, "13-api", "/einstellungen/api"); + await shot(page, "14-daten", "/einstellungen/daten"); + await shot(page, "15-verwaltung", "/verwaltung", { full: true }); + await shot(page, "16-papierkorb", "/papierkorb"); + + // Detailseite und Universum brauchen IDs aus der Bibliothek + await page.goto(`${BASE}/bibliothek?q=nordlicht`, { waitUntil: "networkidle" }); + const titleHref = await page.locator("a[href^='/titel/']").first().getAttribute("href"); + if (titleHref) await shot(page, "04-serie", titleHref, { full: true }); + + await page.goto(`${BASE}/universen`, { waitUntil: "networkidle" }); + await shot(page, "06-universen", "/universen"); + const uniHref = await page.locator("a[href^='/universen/']").first().getAttribute("href"); + if (uniHref) await shot(page, "07-universum", uniHref, { full: true }); + + await ctx.close(); +} + +// --- Hell --- +{ + const { ctx, page } = await session(1400, 900, "light"); + await login(page); + await shot(page, "08-hell", "/bibliothek"); + await ctx.close(); +} + +// --- Mobil --- +{ + const { ctx, page } = await session(430, 900, "dark"); + await login(page); + await shot(page, "17-mobil-uebersicht", "/"); + await shot(page, "18-mobil-serie", "/bibliothek"); + await ctx.close(); +} + +console.log("Fehler:", errs.length ? [...new Set(errs)].slice(0, 5) : "keine"); +await browser.close();