From 9ee8664233a75ff5d4d56dc92088dc64a807410a Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Tue, 1 Sep 2026 21:15:53 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20eigene=20Sammlung=20=E2=80=94=20?= =?UTF-8?q?=E2=80=9Ehab=20ich"=20mit=20Quelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streaming-Verfügbarkeit beantwortet nur die halbe Frage: manche Filme laufen nirgends mehr, die hat man gekauft und liegen zu Hause — in Jellyfin, Plex, Emby, auf dem NAS oder als Scheibe. Bewusst getrennt vom Feld „wo schaue ich das": das eine sagt, wo ein Titel gerade läuft, und das ändert sich ständig; das andere sagt, was einem gehört, und das bleibt. Auf der Detailseite steht die Sammlung deshalb vor der Verfügbarkeit — was man hat, ist die verlässlichere Antwort. Durchgezogen bis in Filter, Export, CSV-Import und REST-API. --- CHANGELOG.md | 27 +++-- DEVLOG.md | 28 ++++- Dockerfile | 14 ++- README.md | 3 + docker/prisma-cli-package.json | 9 ++ docs/ANLEITUNG.md | 55 +++++++--- docs/API.md | 17 +++- docs/screenshots/20-sammlung.webp | Bin 0 -> 48468 bytes package.json | 2 +- .../20260901190508_sammlung/migration.sql | 6 ++ prisma/schema.prisma | 8 ++ prisma/seed.ts | 13 ++- releases/{v0.6.1.md => v0.7.0.md} | 23 ++++- src/app/actions/library.ts | 2 + src/app/api/v1/bibliothek/[entryId]/route.ts | 4 + src/app/api/v1/bibliothek/route.ts | 1 + src/app/api/v1/openapi.json/route.ts | 15 +++ src/app/bibliothek/page.tsx | 1 + src/app/titel/[id]/page.tsx | 45 ++++++++ src/components/library/EntryNotes.tsx | 96 +++++++++++++++++- src/components/library/LibraryCard.tsx | 40 ++++++-- src/components/library/LibraryFilters.tsx | 13 ++- src/lib/library.ts | 13 ++- src/lib/queries.ts | 13 ++- src/lib/transfer.ts | 19 +++- tests/library.test.ts | 52 ++++++++++ 26 files changed, 467 insertions(+), 52 deletions(-) create mode 100644 docker/prisma-cli-package.json create mode 100644 docs/screenshots/20-sammlung.webp create mode 100644 prisma/migrations/20260901190508_sammlung/migration.sql rename releases/{v0.6.1.md => v0.7.0.md} (66%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a37d8..d85e7f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,26 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] -## [0.6.1] — 2026-09-01 +## [0.7.0] — 2026-09-01 -Nacharbeiten aus der Abnahme: das Docker-Image wurde zum ersten Mal wirklich -gebaut und gestartet — dabei kam ein Fehler ans Licht, den nur der echte -Containerstart zeigt. +„Hab ich" — Titel, die in der eigenen Sammlung liegen. Dazu die Nacharbeiten +aus der Abnahme: das Docker-Image wurde zum ersten Mal wirklich gebaut und +gestartet, und dabei kamen Fehler ans Licht, die nur der echte Containerstart +zeigt. + +### Hinzugefügt + +- **Eigene Sammlung**: Jeder Eintrag lässt sich als „hab ich" markieren, mit + Angabe wo — Jellyfin, Plex, Emby, NAS, Blu-ray, DVD oder frei getippt. + Bewusst getrennt vom Feld „wo schaue ich das": Streaming-Verfügbarkeit kommt + und geht, der gekaufte Film auf dem Heimserver bleibt. Gerade die Titel, die + nirgends mehr laufen, will man ja wiederfinden. + - Abzeichen auf der Posterkachel und im Kopf der Detailseite + - Auf der Detailseite steht die Sammlung **vor** der Streaming-Verfügbarkeit + — was man hat, ist die verlässlichere Antwort auf „wo kann ich das schauen" + - Eigener Filter in der Bibliothek samt Zähler + - In Export (CSV und JSON), CSV-Import und REST-API (`?sammlung=1` beim + Lesen, `owned` und `ownedSource` beim Ändern) ### Behoben @@ -306,8 +321,8 @@ Erste lauffähige Etappe: Gerüst, Marke, Datenmodell und Anmeldung. dass weitere Sprachen als zusätzliche Datei dazukommen können. - **Seed-Skript** mit Demo-Konten sowie README und Design-Dokumentation. -[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.6.1...HEAD -[0.6.1]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.6.1 +[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.7.0...HEAD +[0.7.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.7.0 [0.6.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.6.0 [0.5.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.5.0 [0.4.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.4.0 diff --git a/DEVLOG.md b/DEVLOG.md index 84a06e3..936f6eb 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -91,6 +91,18 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt. --- +## Nach der Abnahme (v0.7.0) + +| # | Start | Ende | Dauer | Arbeitsschritt | +|---|---|---|---|---| +| 7.1 | 20:25 | 20:50 | 25 min | Docker-Image zum ersten Mal wirklich gebaut und gestartet; zwei Fehler behoben (fehlende Prisma-Abhängigkeiten im Laufzeitimage, Schriftabruf beim Bauen), Gitea-Workflows, Erscheinungsmeldung nachgezogen | +| 7.2 | 20:50 | 21:05 | 15 min | Compose-Stack von null verifiziert: Migration, erstes Konto wird Administrator, Verwaltung erreichbar | +| 7.3 | 21:05 | 21:16 | 11 min | Rückfrage aus dem Betrieb umgesetzt: „hab ich" für die eigene Sammlung (Jellyfin, Plex, NAS, Blu-ray) — Schema, Logik, Oberfläche, Filter, Import/Export, API, Tests, Anleitung | + +**Nacharbeiten gesamt: 51 min** (2026-09-01, 20:25 – 21:16) + +--- + ## Gesamtübersicht | Etappe | Version | Dauer | @@ -101,8 +113,9 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt. | 4 — Universen | 0.4.0 | 14 min | | 5 — Dashboard, Kalender, Statistik | 0.5.0 | 15 min | | 6 — Benachrichtigungen, Sync, Import/Export, API, PWA | 0.6.0 | 31 min | +| Nacharbeiten — Docker verifiziert, eigene Sammlung | 0.7.0 | 51 min | -**Gesamtentwicklungszeit: 2 h 16 min** (136 min, 2026-09-01, 18:09 – 20:25) +**Gesamtentwicklungszeit: 3 h 07 min** (187 min, 2026-09-01, 18:09 – 21:16) ### Womit die Zeit verging @@ -111,7 +124,7 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt. | ~35 min | Oberfläche und Design (Branding, Komponenten, Seiten) | | ~30 min | Domänenlogik (Tracking, Universen, Statistik, Abgleich) | | ~25 min | Schnittstellen nach außen (TMDB, REST-API, Import/Export, Benachrichtigungen) | -| ~20 min | Tests — 49 Stück, die unterwegs sieben echte Fehler gefunden haben | +| ~25 min | Tests — 52 Stück, die unterwegs acht echte Fehler gefunden haben | | ~15 min | Dokumentation, Screenshots, Changelog, Releases | | ~11 min | Gerüst, Docker, Datenmodell, Werkzeugkette | @@ -124,3 +137,14 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt. 5. Poster zeigten das kaputte Bildsymbol statt der Rückfallkachel. 6. Eine `server-only`-Kette landete im Browser-Bundle und brach den Build. 7. Der SVG-Titel eines Diagramms brach die Hydration. +8. Eine Testerwartung zur Reihenfolge war falsch — nicht der Code. + +### Fehler, die erst der echte Containerstart zeigte + +1. Dem Laufzeitimage fehlten transitive Abhängigkeiten der Prisma-CLI, die + Migration lief nicht an. Die CLI wird jetzt in einer eigenen Build-Stufe + vollständig installiert. +2. npm blockiert seit Version 11 Install-Skripte; ohne Freigabe fehlte die + plattformpassende Prisma-Engine. +3. `next/font/google` lud die Schrift beim Bauen herunter und ließ den Build + im Container hängen. Die Schrift liegt jetzt im Repository. diff --git a/Dockerfile b/Dockerfile index 4e46135..0b971ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,10 +16,10 @@ ENV NEXT_TELEMETRY_DISABLED=1 FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ -# npm blockt Install-Skripte seit Version 11 standardmäßig. Prisma braucht seine -# eigenen, um die passende Engine für die Plattform bereitzulegen — deshalb hier -# gezielt erlaubt statt pauschal. -RUN npm ci --no-audit --no-fund --allow-scripts "prisma,@prisma/engines" +# npm blockt Install-Skripte seit Version 11. Welche laufen dürfen, steht im +# Feld "allowScripts" der package.json — für Prisma, damit die zur Plattform +# passende Engine bereitgelegt wird. +RUN npm ci --no-audit --no-fund # --- Build ------------------------------------------------------------------- FROM base AS builder @@ -41,7 +41,11 @@ RUN npx prisma generate && npx next build # schief: @prisma/config braucht weitere Module, die dabei fehlten. FROM base AS prisma-cli WORKDIR /cli -RUN npm init -y > /dev/null && npm install --omit=dev --no-audit --no-fund --allow-scripts "prisma,@prisma/engines" prisma@7.10.0 +# Die package.json liegt als Datei bei: nur mit dem Feld "allowScripts" lässt +# npm die Install-Skripte von Prisma zu — sonst fehlt im Image die zur +# Plattform passende Engine. +COPY docker/prisma-cli-package.json ./package.json +RUN npm install --omit=dev --no-audit --no-fund prisma@7.10.0 # --- Laufzeit ---------------------------------------------------------------- FROM base AS runner diff --git a/README.md b/README.md index f9a16d4..cadc874 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ bleiben auf deinem Server. Story-Chronologie, Einsteiger-Route) und Gesamtfortschritt. - **Metadaten von TMDB** — Suche, Poster, Staffeln und Episoden, offizielle Collections, Streaming-Verfügbarkeit in deiner Region. +- **Eigene Sammlung** — markiere, was du schon hast, und wo: Jellyfin, Plex, + NAS, Blu-ray. Gerade die Titel, die nirgends mehr streamen, findest du so + wieder. - **Benachrichtigungen** — bei neuen Episoden und Releases, per E-Mail, ntfy, Gotify oder Webhook (Discord/Slack-kompatibel), pro Nutzer einstellbar. - **Mehrere Nutzer** — jede Person hat ihre eigene Bibliothek; Universen lassen diff --git a/docker/prisma-cli-package.json b/docker/prisma-cli-package.json new file mode 100644 index 0000000..050dbbf --- /dev/null +++ b/docker/prisma-cli-package.json @@ -0,0 +1,9 @@ +{ + "name": "flimmer-prisma-cli", + "private": true, + "description": "Nur zum Installieren der Prisma-CLI ins Laufzeitimage. npm blockt Install-Skripte seit Version 11; ohne die Freigabe hier fehlt im Container die plattformpassende Engine. Bei einem Prisma-Update auch die Version in package.json anpassen.", + "allowScripts": { + "prisma@7.10.0": true, + "@prisma/engines@7.10.0": true + } +} diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md index dcd9d9d..89451fa 100644 --- a/docs/ANLEITUNG.md +++ b/docs/ANLEITUNG.md @@ -17,19 +17,20 @@ die echten. 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. [Öffentliches Profil](#öffentliches-profil) -16. [Auf dem Handy](#auf-dem-handy) -17. [Helles Farbschema](#helles-farbschema) +5. [Hab ich — die eigene Sammlung](#hab-ich--die-eigene-sammlung) +6. [Die Bibliothek](#die-bibliothek) +7. [Universen](#universen) +8. [Kalender](#kalender) +9. [Statistik](#statistik) +10. [Einstellungen](#einstellungen) +11. [Benachrichtigungen](#benachrichtigungen) +12. [Import und Export](#import-und-export) +13. [API-Schlüssel](#api-schlüssel) +14. [Papierkorb](#papierkorb) +15. [Verwaltung](#verwaltung) +16. [Öffentliches Profil](#öffentliches-profil) +17. [Auf dem Handy](#auf-dem-handy) +18. [Helles Farbschema](#helles-farbschema) --- @@ -115,6 +116,34 @@ Karte zeigt dann „2. Durchlauf". --- +## Hab ich — die eigene Sammlung + +![Sammlung](screenshots/20-sammlung.webp) + +„Wo läuft das?" beantwortet nur die halbe Frage. Manche Filme laufen nirgends +mehr — die hat man gekauft und liegen bei sich zu Hause, in Jellyfin, Plex, +Emby, auf dem NAS oder als Scheibe im Regal. + +Dafür gibt es das Häkchen **Hab ich** auf der Detailseite, rechts über dem Feld +„wo schaue ich das". Daneben trägst du ein, *wo* der Titel liegt; das Feld +schlägt die gängigen Möglichkeiten vor, nimmt aber auch alles andere an. + +Was das bewirkt: + +- Auf der Detailseite steht die Sammlung **vor** der Streaming-Verfügbarkeit — + was du hast, ist die verlässlichere Antwort. +- Poster in der Bibliothek bekommen ein Abzeichen, und statt des + Streamingdienstes steht dort deine Quelle. +- In der Filterleiste gibt es einen eigenen Knopf **Hab ich** samt Zähler. +- Export und Import führen die Angabe mit (Spalten `HabIch` und `Sammlung`), + ebenso die REST-API. + +Bewusst getrennt vom Feld „wo schaue ich das": Das eine sagt, wo ein Titel +gerade läuft — das ändert sich ständig. Das andere sagt, was dir gehört, und +das bleibt. + +--- + ## Die Bibliothek ![Bibliothek](screenshots/05-bibliothek.webp) diff --git a/docs/API.md b/docs/API.md index 86b41df..4297673 100644 --- a/docs/API.md +++ b/docs/API.md @@ -57,8 +57,9 @@ GET /api/v1/bibliothek?status=WATCHING&typ=SERIES&genre=Drama&q=nord&limit=50&o POST /api/v1/bibliothek ``` -Filter: `q`, `status`, `typ`, `genre`, `tag`, `dienst`, `limit`, `offset`. -Status: `WATCHING`, `PAUSED`, `COMPLETED`, `DROPPED`, `WATCHLIST`. +Filter: `q`, `status`, `typ`, `genre`, `tag`, `dienst`, `sammlung`, `limit`, +`offset`. Status: `WATCHING`, `PAUSED`, `COMPLETED`, `DROPPED`, `WATCHLIST`. +`sammlung=1` liefert nur Titel, die in der eigenen Sammlung liegen. Aufnehmen — entweder mit interner Kennung oder direkt mit der TMDB-Kennung: @@ -75,7 +76,9 @@ DELETE /api/v1/bibliothek/{entryId} ``` `GET` liefert den Eintrag samt `progress` und `nextEpisode`. -`PATCH` ändert `status`, `rating` (1–10), `notes`, `whereToWatch`, `favorite`. +`PATCH` ändert `status`, `rating` (1–10), `notes`, `whereToWatch`, `favorite` +sowie `owned` (liegt der Titel in der eigenen Sammlung?) und `ownedSource` +(wo genau — Jellyfin, Plex, Blu-ray …). `DELETE` legt den Eintrag in den Papierkorb — dort bleibt er 30 Tage. ### Sichtungen @@ -161,6 +164,14 @@ GET /api/v1/export?format=csv JSON enthält zusätzlich alle Sichtungen und Universen. +## Beispiel: was ich habe, aber noch nicht gesehen habe + +```bash +curl -s -H "Authorization: Bearer $KEY" \ + "$HOST/api/v1/bibliothek?sammlung=1&status=WATCHLIST" \ +| jq -r '.items[] | "\(.title) — liegt bei: \(.ownedSource // "ohne Angabe")"' +``` + ## Beispiel: Serien mit offener Episode auflisten ```bash diff --git a/docs/screenshots/20-sammlung.webp b/docs/screenshots/20-sammlung.webp new file mode 100644 index 0000000000000000000000000000000000000000..56b5aff908aed7acade5c4ef30e6f74aa6762f83 GIT binary patch literal 48468 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 5d4d2a9..e5c4a50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flimmer", - "version": "0.6.1", + "version": "0.7.0", "private": true, "description": "Flimmer — selbst hostbarer Serien- und Film-Tracker", "license": "MIT", diff --git a/prisma/migrations/20260901190508_sammlung/migration.sql b/prisma/migrations/20260901190508_sammlung/migration.sql new file mode 100644 index 0000000..b45dfb7 --- /dev/null +++ b/prisma/migrations/20260901190508_sammlung/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "LibraryEntry" ADD COLUMN "owned" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "ownedSource" TEXT; + +-- CreateIndex +CREATE INDEX "LibraryEntry_userId_owned_idx" ON "LibraryEntry"("userId", "owned"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e6098e7..30b3d16 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -289,6 +289,13 @@ model LibraryEntry { /// „Wo schaue ich das" — Vorschlag aus der Verfügbarkeit, überschreibbar whereToWatch String? + /// Liegt der Titel in der eigenen Sammlung? Unabhängig davon, ob ihn gerade + /// ein Streamingdienst führt — gekaufte Filme laufen irgendwann nirgends mehr. + owned Boolean @default(false) + + /// Wo genau: „Jellyfin", „Blu-ray", „NAS", „Plex" … + ownedSource String? + favorite Boolean @default(false) /// Aktueller Durchlauf: 0 = Erstsichtung, 1 = erster Rewatch … @@ -309,6 +316,7 @@ model LibraryEntry { @@unique([userId, titleId]) @@index([userId, status]) @@index([userId, updatedAt]) + @@index([userId, owned]) @@index([deletedAt]) } diff --git a/prisma/seed.ts b/prisma/seed.ts index c01caed..44a4b6c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -244,6 +244,9 @@ type EntryPlan = { startedDaysAgo?: number; /** Abstand zwischen zwei Episoden in Tagen. */ spacingDays?: number; + /** Liegt der Titel in der eigenen Sammlung? */ + owned?: boolean; + ownedSource?: string; }; const ENTRY_PLANS: EntryPlan[] = [ @@ -315,8 +318,8 @@ const ENTRY_PLANS: EntryPlan[] = [ }, { key: "zwischenstopp", status: "WATCHLIST", tags: ["vorgemerkt"] }, - { key: "sternenpfad-1", status: "COMPLETED", rating: 8, seen: true, tags: ["sternenpfad"], startedDaysAgo: 340 }, - { key: "sternenpfad-2", status: "COMPLETED", rating: 9, seen: true, favorite: true, tags: ["sternenpfad"], startedDaysAgo: 320 }, + { key: "sternenpfad-1", status: "COMPLETED", rating: 8, seen: true, tags: ["sternenpfad"], startedDaysAgo: 340, owned: true, ownedSource: "Jellyfin" }, + { key: "sternenpfad-2", status: "COMPLETED", rating: 9, seen: true, favorite: true, tags: ["sternenpfad"], startedDaysAgo: 320, owned: true, ownedSource: "Jellyfin" }, { key: "sternenpfad-3", status: "COMPLETED", rating: 7, seen: true, tags: ["sternenpfad"], startedDaysAgo: 250 }, { key: "sternenpfad-4", status: "WATCHLIST", tags: ["sternenpfad"] }, { key: "sternenpfad-null", status: "WATCHLIST", tags: ["sternenpfad", "kino"] }, @@ -330,7 +333,7 @@ const ENTRY_PLANS: EntryPlan[] = [ startedDaysAgo: 180, }, { key: "stille-bilder-2", status: "WATCHLIST" }, - { key: "hochwasser", status: "COMPLETED", rating: 10, seen: true, favorite: true, startedDaysAgo: 60 }, + { key: "hochwasser", status: "COMPLETED", rating: 10, seen: true, favorite: true, startedDaysAgo: 60, owned: true, ownedSource: "Blu-ray" }, { key: "pfand", status: "DROPPED", rating: 4, notes: "Nach vierzig Minuten abgebrochen.", startedDaysAgo: 40 }, { key: "die-vermessung", status: "COMPLETED", rating: 7, seen: true, startedDaysAgo: 400 }, ]; @@ -355,6 +358,8 @@ async function seedLibrary(userId: string, titleIds: Map) { rating: plan.rating ?? null, notes: plan.notes ?? null, favorite: plan.favorite ?? false, + owned: plan.owned ?? false, + ownedSource: plan.ownedSource ?? null, currentRun, startedAt: plan.startedDaysAgo ? daysAgo(plan.startedDaysAgo) : null, finishedAt: plan.status === "COMPLETED" ? daysAgo(Math.max(1, (plan.startedDaysAgo ?? 30) - 20)) : null, @@ -364,6 +369,8 @@ async function seedLibrary(userId: string, titleIds: Map) { rating: plan.rating ?? null, notes: plan.notes ?? null, favorite: plan.favorite ?? false, + owned: plan.owned ?? false, + ownedSource: plan.ownedSource ?? null, currentRun, }, }); diff --git a/releases/v0.6.1.md b/releases/v0.7.0.md similarity index 66% rename from releases/v0.6.1.md rename to releases/v0.7.0.md index 43fa3ff..43e5264 100644 --- a/releases/v0.6.1.md +++ b/releases/v0.7.0.md @@ -1,8 +1,23 @@ -# Flimmer 0.6.1 +# Flimmer 0.7.0 -Nacharbeiten aus der Abnahme: das Docker-Image wurde zum ersten Mal wirklich -gebaut und gestartet — dabei kam ein Fehler ans Licht, den nur der echte -Containerstart zeigt. +„Hab ich" — Titel, die in der eigenen Sammlung liegen. Dazu die Nacharbeiten +aus der Abnahme: das Docker-Image wurde zum ersten Mal wirklich gebaut und +gestartet, und dabei kamen Fehler ans Licht, die nur der echte Containerstart +zeigt. + +### Hinzugefügt + +- **Eigene Sammlung**: Jeder Eintrag lässt sich als „hab ich" markieren, mit + Angabe wo — Jellyfin, Plex, Emby, NAS, Blu-ray, DVD oder frei getippt. + Bewusst getrennt vom Feld „wo schaue ich das": Streaming-Verfügbarkeit kommt + und geht, der gekaufte Film auf dem Heimserver bleibt. Gerade die Titel, die + nirgends mehr laufen, will man ja wiederfinden. + - Abzeichen auf der Posterkachel und im Kopf der Detailseite + - Auf der Detailseite steht die Sammlung **vor** der Streaming-Verfügbarkeit + — was man hat, ist die verlässlichere Antwort auf „wo kann ich das schauen" + - Eigener Filter in der Bibliothek samt Zähler + - In Export (CSV und JSON), CSV-Import und REST-API (`?sammlung=1` beim + Lesen, `owned` und `ownedSource` beim Ändern) ### Behoben diff --git a/src/app/actions/library.ts b/src/app/actions/library.ts index 2781ca2..fc824cf 100644 --- a/src/app/actions/library.ts +++ b/src/app/actions/library.ts @@ -114,6 +114,8 @@ const detailsSchema = z.object({ notes: z.string().max(5000).nullable().optional(), whereToWatch: z.string().max(120).nullable().optional(), favorite: z.boolean().optional(), + owned: z.boolean().optional(), + ownedSource: z.string().max(80).nullable().optional(), }); export async function updateEntryAction(entryId: string, data: unknown) { diff --git a/src/app/api/v1/bibliothek/[entryId]/route.ts b/src/app/api/v1/bibliothek/[entryId]/route.ts index b00cdd5..7c9c9eb 100644 --- a/src/app/api/v1/bibliothek/[entryId]/route.ts +++ b/src/app/api/v1/bibliothek/[entryId]/route.ts @@ -36,6 +36,8 @@ export const GET = withApi(async (_request, user, context) => { rating: entry.rating, notes: entry.notes, whereToWatch: entry.whereToWatch, + owned: entry.owned, + ownedSource: entry.ownedSource, favorite: entry.favorite, currentRun: entry.currentRun, tags: entry.tags.map((link) => link.tag.name), @@ -58,6 +60,8 @@ const patchSchema = z.object({ notes: z.string().max(5000).nullable().optional(), whereToWatch: z.string().max(120).nullable().optional(), favorite: z.boolean().optional(), + owned: z.boolean().optional(), + ownedSource: z.string().max(80).nullable().optional(), }); /** PATCH /api/v1/bibliothek/{id} — Status und Angaben ändern. */ diff --git a/src/app/api/v1/bibliothek/route.ts b/src/app/api/v1/bibliothek/route.ts index 983d462..4945090 100644 --- a/src/app/api/v1/bibliothek/route.ts +++ b/src/app/api/v1/bibliothek/route.ts @@ -22,6 +22,7 @@ export const GET = withApi(async (request, user) => { genre: url.searchParams.get("genre") ?? undefined, tag: url.searchParams.get("tag") ?? undefined, provider: url.searchParams.get("dienst") ?? undefined, + owned: url.searchParams.get("sammlung") === "1" ? true : undefined, }); return { diff --git a/src/app/api/v1/openapi.json/route.ts b/src/app/api/v1/openapi.json/route.ts index 1a2ab4e..9d61d71 100644 --- a/src/app/api/v1/openapi.json/route.ts +++ b/src/app/api/v1/openapi.json/route.ts @@ -73,6 +73,12 @@ export function GET() { { name: "genre", in: "query", schema: { type: "string" } }, { name: "tag", in: "query", schema: { type: "string" } }, { name: "dienst", in: "query", schema: { type: "string" } }, + { + name: "sammlung", + in: "query", + schema: { type: "string", enum: ["1"] }, + description: "Nur Titel, die in der eigenen Sammlung liegen.", + }, { name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 200 } }, { name: "offset", in: "query", schema: { type: "integer", default: 0 } }, ], @@ -123,6 +129,15 @@ export function GET() { notes: { type: "string", nullable: true }, whereToWatch: { type: "string", nullable: true }, favorite: { type: "boolean" }, + owned: { + type: "boolean", + description: "Liegt der Titel in der eigenen Sammlung?", + }, + ownedSource: { + type: "string", + nullable: true, + description: "Wo genau — Jellyfin, Plex, Blu-ray …", + }, }, }, }, diff --git a/src/app/bibliothek/page.tsx b/src/app/bibliothek/page.tsx index 2ef9acc..1b70e0f 100644 --- a/src/app/bibliothek/page.tsx +++ b/src/app/bibliothek/page.tsx @@ -29,6 +29,7 @@ export default async function LibraryPage({ genre: params.genre || undefined, tag: params.tag || undefined, provider: params.dienst || undefined, + owned: params.sammlung === "1" ? true : undefined, sort: SORTS.includes(params.sortierung ?? "") ? (params.sortierung as LibrarySort) : undefined, }; diff --git a/src/app/titel/[id]/page.tsx b/src/app/titel/[id]/page.tsx index c9c6bb5..f98a123 100644 --- a/src/app/titel/[id]/page.tsx +++ b/src/app/titel/[id]/page.tsx @@ -120,6 +120,19 @@ export default async function TitlePage({ params }: { params: Params }) { {year ? ` · ${year}` : ""} {title.status ? ` · ${title.status}` : ""}

+ {entry?.owned && ( + + Hab ich{entry.ownedSource ? ` · ${entry.ownedSource}` : ""} + + )} {entry && ( ) : (
+
)} @@ -245,6 +259,8 @@ export default async function TitlePage({ params }: { params: Params }) { whereToWatch={entry.whereToWatch} tags={entry.tags.map((link) => link.tag.name)} suggestion={suggestion} + owned={entry.owned} + ownedSource={entry.ownedSource} /> @@ -253,6 +269,7 @@ export default async function TitlePage({ params }: { params: Params }) {

Wo läuft das?

+ )} @@ -299,6 +316,34 @@ export default async function TitlePage({ params }: { params: Params }) { ); } +/** + * Hinweis auf die eigene Sammlung — steht bewusst **vor** der + * Streaming-Verfügbarkeit: was man hat, ist die verlässlichere Antwort auf + * „wo kann ich das schauen". + */ +function OwnedNotice({ owned, source }: { owned: boolean; source: string | null }) { + if (!owned) return null; + + return ( +

+ + + + + In deiner Sammlung + {source ? · {source} : null} + +

+ ); +} + function Fact({ label, children }: { label: string; children: React.ReactNode }) { return (
diff --git a/src/components/library/EntryNotes.tsx b/src/components/library/EntryNotes.tsx index f0f59fd..c24af40 100644 --- a/src/components/library/EntryNotes.tsx +++ b/src/components/library/EntryNotes.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useRef, useState, useTransition } from "react"; +import { useRef, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; +import { clsx } from "clsx"; import { setTagsAction, updateEntryAction } from "@/app/actions/library"; import { useToasts } from "@/components/Toasts"; @@ -17,6 +18,8 @@ export function EntryNotes({ whereToWatch, tags, suggestion, + owned, + ownedSource, }: { entryId: string; notes: string | null; @@ -24,9 +27,12 @@ export function EntryNotes({ tags: string[]; /** Vorschlag aus der Streaming-Verfügbarkeit. */ suggestion: string | null; + owned: boolean; + ownedSource: string | null; }) { return (
+ @@ -34,6 +40,94 @@ export function EntryNotes({ ); } +/** + * „Hab ich" — der Titel liegt in der eigenen Sammlung. + * + * Bewusst getrennt von „wo schaue ich das": Streaming-Verfügbarkeit kommt und + * geht, der gekaufte Film auf dem Heimserver bleibt. Genau die Titel, die + * nirgends mehr laufen, will man ja wiederfinden. + */ +function OwnedEditor({ + entryId, + owned, + source, +}: { + entryId: string; + owned: boolean; + source: string | null; +}) { + const save = useAutosave(entryId); + const [isOwned, setIsOwned] = useState(owned); + const [text, setText] = useState(source ?? ""); + const initial = useRef(source ?? ""); + + return ( +
+ + + {isOwned && ( +
+ + setText(event.target.value)} + onBlur={() => { + if (text === initial.current) return; + initial.current = text; + save({ ownedSource: text }); + }} + list="fl-owned-quellen" + placeholder="Jellyfin, Plex, NAS, Blu-ray …" + className="fl-input py-1.5 text-sm" + /> + + +
+ )} +
+ ); +} + function useAutosave(entryId: string) { const { report } = useToasts(); const router = useRouter(); diff --git a/src/components/library/LibraryCard.tsx b/src/components/library/LibraryCard.tsx index 3567fd0..abc50c5 100644 --- a/src/components/library/LibraryCard.tsx +++ b/src/components/library/LibraryCard.tsx @@ -19,15 +19,31 @@ export function LibraryCard({ item }: { item: LibraryItem }) {
- - {STATUS_LABEL[status]} + {/* Status und Sammlung teilen sich die obere Zeile — unten sitzt bei + den Postern oft der Titel, dort würden sie ihn verdecken. */} + + + {STATUS_LABEL[status]} + + + {item.owned && ( + + Hab ich + + )} {item.rating != null && ( @@ -51,7 +67,11 @@ export function LibraryCard({ item }: { item: LibraryItem }) {

{item.type === "SERIES" ? "Serie" : "Film"} {item.year ? ` · ${item.year}` : ""} - {item.whereToWatch ? ` · ${item.whereToWatch}` : ""} + {item.owned && item.ownedSource + ? ` · ${item.ownedSource}` + : item.whereToWatch + ? ` · ${item.whereToWatch}` + : ""}

{showProgress && ( diff --git a/src/components/library/LibraryFilters.tsx b/src/components/library/LibraryFilters.tsx index 0b4ec18..6d60ff7 100644 --- a/src/components/library/LibraryFilters.tsx +++ b/src/components/library/LibraryFilters.tsx @@ -56,7 +56,9 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) { const status = params.get("status"); const type = params.get("typ"); const sort = params.get("sortierung") ?? "aktualisiert"; - const activeCount = ["genre", "tag", "dienst", "typ"].filter((key) => params.get(key)).length; + const activeCount = ["genre", "tag", "dienst", "typ", "sammlung"].filter((key) => + params.get(key), + ).length; return (
@@ -78,6 +80,15 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) { {STATUS_LABEL[key as StatusKey]} ))} + + {facets.owned > 0 && ( + apply({ sammlung: params.get("sammlung") === "1" ? null : "1" })} + > + Hab ich + + )}
diff --git a/src/lib/library.ts b/src/lib/library.ts index cb2d31e..8a6b926 100644 --- a/src/lib/library.ts +++ b/src/lib/library.ts @@ -110,7 +110,14 @@ export async function setStatus(userId: string, entryId: string, status: EntrySt export async function updateEntryDetails( userId: string, entryId: string, - data: { rating?: number | null; notes?: string | null; whereToWatch?: string | null; favorite?: boolean }, + data: { + rating?: number | null; + notes?: string | null; + whereToWatch?: string | null; + favorite?: boolean; + owned?: boolean; + ownedSource?: string | null; + }, ) { await requireEntry(userId, entryId); @@ -127,6 +134,10 @@ export async function updateEntryDetails( ? { whereToWatch: data.whereToWatch?.trim() || null } : {}), ...(data.favorite !== undefined ? { favorite: data.favorite } : {}), + ...(data.owned !== undefined ? { owned: data.owned } : {}), + ...(data.ownedSource !== undefined + ? { ownedSource: data.ownedSource?.trim() || null } + : {}), }, }); } diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 1ab9dda..79417ab 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -19,6 +19,8 @@ export type LibraryFilters = { genre?: string; tag?: string; provider?: string; + /** Nur Titel, die in der eigenen Sammlung liegen. */ + owned?: boolean; sort?: LibrarySort; }; @@ -42,6 +44,8 @@ export type LibraryItem = { favorite: boolean; currentRun: number; whereToWatch: string | null; + owned: boolean; + ownedSource: string | null; tags: string[]; watched: number; total: number; @@ -56,6 +60,7 @@ export async function getLibrary( userId, deletedAt: null, ...(filters.status ? { status: filters.status } : {}), + ...(filters.owned ? { owned: true } : {}), ...(filters.tag ? { tags: { some: { tag: { name: filters.tag } } } } : {}), title: { ...(filters.type ? { type: filters.type } : {}), @@ -116,6 +121,8 @@ export async function getLibrary( favorite: entry.favorite, currentRun: entry.currentRun, whereToWatch: entry.whereToWatch, + owned: entry.owned, + ownedSource: entry.ownedSource, tags: entry.tags.map((link) => link.tag.name), watched, total, @@ -162,11 +169,13 @@ export type LibraryFacets = { tags: string[]; providers: string[]; counts: Record; + /** Wie viele Titel in der eigenen Sammlung liegen. */ + owned: number; }; /** Werte für die Filterleiste — nur das, was in der Bibliothek wirklich vorkommt. */ export async function getLibraryFacets(userId: string): Promise { - const [genreRows, tagRows, providerRows, statusRows, total] = await Promise.all([ + const [genreRows, tagRows, providerRows, statusRows, total, owned] = await Promise.all([ prisma.$queryRaw<{ genre: string }[]>` SELECT DISTINCT unnest(t."genres") AS genre FROM "LibraryEntry" e @@ -193,6 +202,7 @@ export async function getLibraryFacets(userId: string): Promise { _count: { _all: true }, }), prisma.libraryEntry.count({ where: { userId, deletedAt: null } }), + prisma.libraryEntry.count({ where: { userId, deletedAt: null, owned: true } }), ]); const counts = { @@ -211,6 +221,7 @@ export async function getLibraryFacets(userId: string): Promise { tags: tagRows.map((row) => row.name), providers: providerRows.map((row) => row.name), counts, + owned, }; } diff --git a/src/lib/transfer.ts b/src/lib/transfer.ts index 0cb5091..8c81403 100644 --- a/src/lib/transfer.ts +++ b/src/lib/transfer.ts @@ -135,6 +135,11 @@ const CSV_HEADERS: Record = { notiz: "notes", notes: "notes", kommentar: "notes", + habich: "owned", + owned: "owned", + besitz: "owned", + sammlung: "ownedSource", + quelle: "ownedSource", }; /** @@ -185,6 +190,8 @@ export async function importCsv(userId: string, text: string): Promise= 1 && record.rating <= 10 ? record.rating : null, notes: record.notes, + owned: record.owned ?? false, + ownedSource: record.ownedSource ?? null, }, }); } @@ -394,6 +405,8 @@ export async function exportCsv(userId: string): Promise { "Status", "Bewertung", "Durchlaeufe", + "HabIch", + "Sammlung", "WoGeschaut", "Tags", "Notiz", @@ -410,6 +423,8 @@ export async function exportCsv(userId: string): Promise { entry.status, entry.rating ?? "", entry.currentRun + 1, + entry.owned ? "ja" : "nein", + entry.ownedSource ?? "", entry.whereToWatch ?? "", entry.tags.map((link) => link.tag.name).join(" "), entry.notes?.replace(/\r?\n/g, " ") ?? "", @@ -479,6 +494,8 @@ export async function exportJson(userId: string): Promise { rating: entry.rating, notes: entry.notes, whereToWatch: entry.whereToWatch, + owned: entry.owned, + ownedSource: entry.ownedSource, favorite: entry.favorite, currentRun: entry.currentRun, tags: entry.tags.map((link) => link.tag.name), diff --git a/tests/library.test.ts b/tests/library.test.ts index d18d675..c6f4510 100644 --- a/tests/library.test.ts +++ b/tests/library.test.ts @@ -336,3 +336,55 @@ test("fremde Einträge sind nicht erreichbar", async () => { await prisma.user.delete({ where: { id: other.id } }); }); + +test("Sammlung ist unabhängig von der Streaming-Verfügbarkeit", async () => { + const entry = await addToLibrary(userId, movieTitleId, "WATCHLIST"); + + const fresh = await prisma.libraryEntry.findUniqueOrThrow({ where: { id: entry.id } }); + assert.equal(fresh.owned, false, "standardmäßig gehört einem nichts"); + assert.equal(fresh.ownedSource, null); + + const owned = await updateEntryDetails(userId, entry.id, { + owned: true, + ownedSource: " Jellyfin ", + }); + assert.equal(owned.owned, true); + assert.equal(owned.ownedSource, "Jellyfin", "Rand wird beschnitten"); + + // Der Fundort bleibt davon unberührt: das eine sagt, wo es läuft, + // das andere, wo es bei mir liegt. + assert.equal( + owned.whereToWatch, + "Disney Plus", + "der Vorschlag aus der Verfügbarkeit bleibt davon unberührt", + ); + + const released = await updateEntryDetails(userId, entry.id, { + owned: false, + ownedSource: "", + }); + assert.equal(released.owned, false); + assert.equal(released.ownedSource, null, "leerer Text wird zu null"); +}); + +test("Sammlung: Filter und Zähler in der Bibliothek", async () => { + const movie = await addToLibrary(userId, movieTitleId, "WATCHLIST"); + const series = await addToLibrary(userId, seriesTitleId, "WATCHING"); + await updateEntryDetails(userId, movie.id, { owned: true, ownedSource: "Blu-ray" }); + + const { getLibrary, getLibraryFacets } = await import("../src/lib/queries"); + + const alle = await getLibrary(userId, {}); + assert.equal(alle.length, 2); + + const nurSammlung = await getLibrary(userId, { owned: true }); + assert.equal(nurSammlung.length, 1); + assert.equal(nurSammlung[0]?.entryId, movie.id); + assert.equal(nurSammlung[0]?.ownedSource, "Blu-ray"); + + const facets = await getLibraryFacets(userId); + assert.equal(facets.owned, 1, "der Zähler der Filterleiste stimmt"); + assert.equal(facets.counts.ALLE, 2); + + await prisma.libraryEntry.deleteMany({ where: { id: { in: [movie.id, series.id] } } }); +});