docs: bebilderte Anleitung, API-Dokumentation und Screenshots

docs/ANLEITUNG.md führt einmal durch die ganze Anwendung — von der ersten
Anmeldung über das tägliche Abhaken bis zu Universen, Statistik und
Verwaltung, mit 18 Aufnahmen aus der laufenden Instanz.

Die Bilder entstehen mit npm run screenshots: aufgenommen mit doppelter
Pixeldichte für scharfe Schrift, abgelegt als WebP in halber Kantenlänge —
13 MB werden so zu 0,6 MB.
This commit is contained in:
Florian Wilde
2026-09-01 20:25:13 +02:00
parent cf38bd6854
commit 8d6f1d044a
24 changed files with 732 additions and 8 deletions
+45 -5
View File
@@ -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.
<p align="center">
<img src="docs/screenshots/02-uebersicht.webp" alt="Die Übersicht von Flimmer" width="820">
</p>
> **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.
+360
View File
@@ -0,0 +1,360 @@
<p align="center">
<img src="../public/brand/flimmer-logo.svg" alt="Flimmer" width="320">
</p>
# 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).
+190
View File
@@ -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
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+4 -3
View File
@@ -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",
+34
View File
@@ -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`);
+99
View File
@@ -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();