No description
  • TypeScript 87.4%
  • Shell 9.1%
  • Dockerfile 1.8%
  • JavaScript 0.9%
  • CSS 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-05 05:17:01 +00:00
prisma Add tag-driven release workflow and in-app update check 2026-09-01 08:06:10 +02:00
public Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
scripts Wiki Update 2026-09-05 06:43:04 +02:00
src Instalations anpassung vereinheitlichung 2026-09-01 15:51:28 +02:00
wiki Wiki Update 2026-09-05 06:43:04 +02:00
.dockerignore Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
.env.example Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
.env.production.example Instalation angepasst 2026-09-01 16:04:30 +02:00
.gitignore Add tag-driven release workflow and in-app update check 2026-09-01 08:06:10 +02:00
.nvmrc Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
AGENTS.md Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
Caddyfile Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
CLAUDE.md Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
components.json Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
docker-compose.dev.yml Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
docker-compose.yml Instalations anpassung vereinheitlichung 2026-09-01 15:51:28 +02:00
docker-entrypoint.sh Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
Dockerfile Update Fix 2026-09-05 07:06:46 +02:00
eslint.config.mjs Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
next.config.ts Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
package-lock.json Backup und Restore hinzugefügt 2026-09-01 13:47:15 +02:00
package.json Backup und Restore hinzugefügt 2026-09-01 13:47:15 +02:00
postcss.config.mjs Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
prisma.config.ts Add admin toggle to disable self-registration 2026-08-26 20:49:16 +02:00
README.md wiki update 2026-09-01 17:21:36 +02:00
tsconfig.json Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00
vitest.config.ts Initial commit: Silberling Film-Bibliothek PWA 2026-08-26 19:07:07 +02:00

Silberling

Mobile-first PWA zur Verwaltung physischer Film-Sammlungen (DVD / Blu-ray / 4K UHD). Barcode scannen → Film landet mit deutschem Titel, Beschreibung, Genre, Erscheinungsdatum, Bewertung und Cover in der Bibliothek.

Server aufsetzen? Die vollständige Anleitung von der Container-Erstellung bis zum ersten Login steht im Wiki.

Status: Alle fünf geplanten Phasen sind umgesetzt: Fundament (Auth, Metadaten-Provider-Kette, Bibliotheks-Grid), Barcode-Scanner mit vollständiger Auflösungskette, lokale Poster-Ablage, Freundschaften/Freigaben/Verleih-Tracker sowie PWA-Feinschliff (Offline-Bibliothek, Statistiken, Export/Bulk-Import, Dark Mode, Admin-Bereich).

Architektur auf einen Blick

  • Next.js 15+ (App Router), TypeScript, Tailwind CSS, shadcn/ui (Radix-Variante)
  • PostgreSQL 16 + Prisma als einziger Datenspeicher, inklusive Barcode-Katalog und Metadaten-Katalog
  • Auth.js (NextAuth v5) mit Credentials-Provider und argon2id-Passwort-Hashing
  • Austauschbare Metadaten-Provider-Kette statt einer fest verdrahteten API (siehe unten)
  • Konfiguration ausschließlich über Environment-Variablen, kein Wissen über Deployment-Details im Code

Metadaten-Provider-Kette

Filme werden nie aus einer einzigen Quelle geladen. Ein MetadataProvider-Interface (src/lib/metadata/types.ts) definiert search() und fetchDetails(); ein Merger (src/lib/metadata/merge.ts) setzt daraus pro Feld nach fester Priorität den finalen Datensatz zusammen und protokolliert in sourceMap, welcher Provider welches Feld geliefert hat.

Provider Pflicht? Key nötig? Liefert Lizenz
Wikidata ja nein dt. Titel, Originaltitel, Genre, Regie, Besetzung, Laufzeit, FSK, Land, sowie die Cross-Reference-IDs (IMDb, TMDB) CC0
Deutsche Wikipedia ja nein ausformulierte deutsche Beschreibung CC BY-SA (Quellenangabe Pflicht)
TMDB optional ja, vom Serverbetreiber Cover, Bewertung, alternative Beschreibung eigene TMDB-Lizenz (Attribution Pflicht)
Cinemeta optional, standardmäßig aus nein Notfall-Fallback für Cover/Rating (nur Englisch, inoffiziell) -

Reihenfolge und Aktivierung werden über METADATA_PROVIDERS gesteuert (siehe Environment-Variablen unten). Jeder Provider darf einzeln ausfallen, ohne dass das Hinzufügen eines Films fehlschlägt.

Betrieb ohne TMDB-Key: Die App läuft vollständig weiter, allerdings ohne Cover und ohne TMDB-Bewertungen. Es gibt bewusst keine registrierungsfreie Alternative für Filmposter - Wikimedia Commons hostet nur frei lizenzierte Bilder, und Filmposter sind urheberrechtlich geschützt. Der TMDB-Key wird einmalig vom Serverbetreiber hinterlegt; Endnutzer der App registrieren sich nirgends.

Metadaten werden pro Film einmalig von den externen Providern geladen und danach dauerhaft in der eigenen Datenbank gehalten (Movie.metadataFetchedAt). Ein erneuter Scan oder eine erneute Suche nach einem bereits bekannten Film (identifiziert über wikidataId/tmdbId) fragt keine externen Quellen mehr an.

Barcode-Scanner

Unter /scanner (FAB unten rechts) läuft ein kontinuierlicher Scan-Modus: jeder erkannte Code landet in einer Warteschlange und wird einzeln über resolveBarcode() (src/lib/barcode/resolve.ts) aufgelöst:

  1. Eigene barcodes-Tabelle → sofortiger Treffer, Film wird ohne Rückfrage hinzugefügt (status: "added"), danach per „Passt / Falsch" bestätigbar.
  2. UPCitemdb (kostenloser Trial, 100 Anfragen/Tag/IP, max. 6/Minute) liefert einen rohen Produkttitel.
  3. Titel-Normalisierung (src/lib/barcode/normalizeTitle.ts) entfernt Medien-/Editionshinweise, extrahiert Jahr/Medium/Edition.
  4. Suche über die Metadaten-Provider-Kette, Treffer nach Titelähnlichkeit + Jahr sortiert → Bestätigungsdialog.
  5. Manuelle Titelsuche als Ausweg, wenn UPCitemdb nichts findet oder das Tageslimit erreicht ist (kein Fehlerbildschirm, sondern direkter Wechsel in die manuelle Suche).

Erste Zuordnung einer EAN ist PENDING, ab 3 übereinstimmenden Meldungen CONFIRMED, eine abweichende Meldung markiert sie als DISPUTED (Auflösung dafür folgt mit dem Admin-Bereich in Phase 5).

Scan-Erkennung: native BarcodeDetector Web API, wo verfügbar (Chrome/Android); zxing-wasm als Pflicht-Fallback für Safari/iOS, das die native API nicht unterstützt. Welcher Modus aktiv ist, zeigt ein kleines Label im Kamerabild. Die .wasm-Datei wird selbst ausgeliefert (public/zxing/, per postinstall-Hook aus node_modules kopiert) statt von einem CDN geladen.

Poster-Pipeline

Beim ersten Anlegen eines Films lädt src/lib/posters/download.ts das Poster einmalig herunter (Thumb- und Vollbild-URL vom selben Provider, siehe Merge-Regeln), konvertiert beide mit sharp nach WebP (342 px Breite fürs Grid, 780 px für die Detailansicht) und erzeugt zusätzlich einen winzigen 20-px-Blur-Platzhalter als Base64-Data-URI fürs Lazy Loading. Abgelegt wird unter POSTER_STORAGE_PATH/<movieId>/{thumb,full}.webp, ausgeliefert über die eigene Route /api/posters/[movieId]/[size] mit Cache-Control: public, max-age=31536000, immutable - TMDB/Cinemeta werden für ein bereits gespeichertes Cover nie wieder angefragt.

Speicherbedarf: in der Praxis liegen beide WebP-Dateien zusammen meist bei 30-50 KB pro Film (gemessen, ursprüngliche Schätzung war 60-120 KB) - selbst am oberen Ende deutlich unter 150 MB bei 1000 Filmen.

Fehlertoleranz: Schlägt der Download fehl (Netzwerkfehler, 404 o. ä.), wird der Film trotzdem angelegt, nur ohne Cover. Ein Admin kann fehlende Poster über POST /api/admin/retry-posters gezielt nachziehen - der Job fragt dafür nur die Poster-URL des ursprünglichen Providers erneut ab (posterSource als Marker), nicht die kompletten Metadaten.

Freundschaften & Freigaben

Unter /friends lassen sich Freundschaftsanfragen per E-Mail-Adresse senden, annehmen oder ablehnen (src/lib/friends.ts). Die Sichtbarkeit der eigenen Bibliothek wird unter /library/sharing festgelegt (src/lib/sharing.ts, Modell LibraryShare):

  • Privat (Standard) - niemand außer dir.
  • Für Freunde sichtbar - jeder akzeptierte Freund kann deine Bibliothek unter /users/[deineId]/library ansehen.
  • Über Link sichtbar - ein zufälliger Token macht die Bibliothek unter /shared/[token] auch ohne Login sichtbar; der Link wird ungültig, sobald die Sichtbarkeit geändert wird (Prüfung läuft immer gegen den aktuellen visibility-Wert, nicht nur gegen die Existenz des Tokens).
  • Öffentlich sichtbar - wie Freunde, aber ohne Freundschaftsvoraussetzung.

Zusätzlich lässt sich unabhängig von der allgemeinen Sichtbarkeit gezielt einzelnen Freunden Zugriff geben (auch bei "Privat"), wahlweise mit der Berechtigung "nur ansehen" oder "ansehen & ausleihen dürfen". Beim Ansehen einer fremden Bibliothek markiert eine Vergleichsansicht, welche Filme in der eigenen Bibliothek bereits vorhanden sind ("Hast du schon" / "Fehlt dir").

Verleih-Tracker: Auf der Filmdetailseite und unter /loans lässt sich jedes eigene Exemplar mit einem Namen als verliehen markieren (LibraryItem.loanedTo/loanedAt) und wieder als zurückgegeben eintragen. Dort lässt sich ein Exemplar auch als "gesehen" markieren, was in die Statistiken einfließt.

PWA, Offline-Bibliothek & Dark Mode

Die App ist über den Browser installierbar (src/app/manifest.ts, Icons in public/icons/). Ein handgeschriebener Service Worker (public/sw.js, registriert über src/components/service-worker-registration.tsx) cached die App-Shell, /_next/static/* sowie /api/posters/* cache-first (unveränderlich) und alle übrigen GET-Anfragen (u. a. Seitennavigationen wie /library) network-first mit Cache-Fallback. Einmal online besuchte Seiten bleiben dadurch offline abrufbar; für nie besuchte Seiten greift die Fallback-Seite /offline.

Hinweis: Der Service Worker registriert sich bewusst nur im Production-Build (NODE_ENV=production), nicht unter npm run dev - ein SW im Next-Dev-Server würde mit dessen Hot-Module-Replacement kollidieren. Zum Testen also npm run build && npm start (oder die Docker-Variante) verwenden, dann in den Browser-DevTools unter "Application → Service Workers" den Status prüfen bzw. "Offline" simulieren.

Dark Mode läuft über next-themes (System-Standard, manuell umschaltbar über das Sonne/Mond-Icon in der Navigation) und nutzt die bereits vorhandenen Tailwind-.dark-Variablen.

Dashboard (Startseite)

/ (nach Login der Landing-Screen, auch als PWA-start_url) zeigt Kacheln zu allen Bereichen (Scanner, Hinzufügen, Bibliothek, Statistiken, Freunde, Import, Verliehen, Freigaben - mit Badge für offene Freundschaftsanfragen bzw. aktuell verliehene Exemplare), darunter die wichtigsten Kennzahlen, zuletzt hinzugefügte Filme und die Top-Genres. Ist die Bibliothek leer, gibt es stattdessen einen direkten Einstieg zum Hinzufügen des ersten Films.

Jedes eigene Exemplar lässt sich auf der Filmdetailseite über den Papierkorb-Button (mit Sicherheitsabfrage) wieder aus der Bibliothek entfernen - der Film selbst bleibt im gemeinsamen Metadaten-Katalog erhalten, falls ihn andere Nutzer ebenfalls besitzen.

Statistiken, Export & Bulk-Import

  • /stats zeigt Kennzahlen zur eigenen Sammlung: Anzahl Filme/Exemplare, gesehen/verliehen, Gesamtlaufzeit sowie Verteilungen nach Medium, Genre und Jahrzehnt.
  • CSV-/JSON-Export über die Bibliotheksseite bzw. direkt GET /api/export?format=csv|json.
  • /import nimmt eine Liste von EAN-Codes entgegen (Barcode-Aufkleber der ganzen Sammlung abtippen oder aus einer anderen Quelle einfügen) und läuft für jede durch dieselbe resolveBarcode()-Kette wie der Scanner. Automatisch hinzugefügt wird weiterhin nur bei einer bereits bestätigten Zuordnung (Abschnitt 4) - alles andere landet in einer Liste zur manuellen Nachbearbeitung über Scanner oder Titelsuche, statt eine Zuordnung zu erraten.

Admin-Bereich

Unter /admin (nur für Nutzer mit Rolle ADMIN, z. B. der Seed-Nutzer):

  • Widersprüchliche Barcodes aus dem Scanner-Flow auflösen - die mit den meisten Meldungen bestätigte Zuordnung auswählen, der Barcode wird wieder auf CONFIRMED gesetzt.
  • Fehlende Poster nachladen (Retry-Job aus der Poster-Pipeline) manuell anstoßen.
  • API-Nutzung der letzten 14 Tage pro Provider einsehen (ApiUsageCounter).
  • Nutzerrollen verwalten (USER/ADMIN) - die eigene Rolle lässt sich aus Sicherheitsgründen nicht selbst ändern.
  • Updates: installierte Version, Update-Kanal (Beta/Stabil), Prüfintervall, "Jetzt prüfen" sowie der fertige Befehl für ein Update auf dem Server (siehe "Releases, Versionen und Updates").
  • Backup & Wiederherstellung: ein vollständiges Backup als ZIP herunterladen und auf einem anderen Server wieder einspielen (siehe "Backup und Wiederherstellung").

Setup (lokale Entwicklung, Mac/Apple Silicon)

Node-Version siehe .nvmrc (muss zur Major-Version im Docker-Image passen). Nur PostgreSQL läuft lokal im Container, die App selbst läuft nativ über npm run dev (Volume-Mounts unter macOS sind spürbar langsam, natives HMR ist angenehmer).

nvm use
cp .env.example .env   # Werte anpassen, insb. WIKIMEDIA_USER_AGENT und AUTH_SECRET
docker compose -f docker-compose.dev.yml up -d
npm install
npx prisma migrate dev
npm run dev

App läuft dann auf http://localhost:3000. Zum Anlegen eines Demo-Nutzers (demo@silberling.local / demopasswort):

npm run db:seed

Tests

npm test          # Vitest einmalig
npm run test:watch

Kamera-Test auf dem iPhone (Barcode-Scanner)

getUserMedia funktioniert nur in einem sicheren Kontext (HTTPS). localhost auf dem Mac zählt als sicher - der Zugriff vom iPhone über http://192.168.x.x:3000 dagegen nicht, die Kamera bleibt dann blockiert. Zwei Wege, um trotzdem vom iPhone aus zu testen:

  • Empfohlen - Tunnel: Cloudflare Tunnel oder Tailscale Funnel liefert eine echte HTTPS-URL, die vom iPhone direkt funktioniert. Kein Zertifikats-Handling nötig.
  • Alternative - lokale CA: mkcert auf dem Mac installieren, ein Zertifikat für die LAN-IP ausstellen, die Root-CA auf dem iPhone installieren und unter "Zertifikatsvertrauen" aktivieren. Dann next dev --experimental-https starten.

In Produktion ist HTTPS ebenfalls Pflicht, nicht optional - ohne TLS funktioniert der Scanner auf keinem Gerät (siehe Deployment unten).

Environment-Variablen

Vollständig kommentiert in ​.env.example (lokale Entwicklung) und ​.env.production.example (Server). Kurzüberblick:

Variable Pflicht Beschreibung
DATABASE_URL ja PostgreSQL-Verbindung
AUTH_SECRET ja Secret für Auth.js-Sessions/JWTs (openssl rand -base64 32)
AUTH_URL ja Öffentlich erreichbare Basis-URL der App
METADATA_PROVIDERS nein (Default: wikidata,wikipedia_de,tmdb) Kommagetrennte, aktive Provider in Reihenfolge
TMDB_API_KEY nein Key des Serverbetreibers; ohne Key kein Cover/Rating
ENABLE_CINEMETA nein (Default false) Inoffiziellen Notfall-Fallback aktivieren
WIKIMEDIA_USER_AGENT ja (Wikimedia-Etikette) Aussagekräftiger User-Agent mit Kontakt-URL/E-Mail
POSTER_STORAGE_PATH nein Ablageort lokal gespeicherter Poster (ab Phase 3)
TZ nein (Produktion: Europe/Berlin) Container-Zeitzone

Docker-Deployment (Debian 13, amd64)

Das Projekt ist von Anfang an für native Entwicklung auf Apple Silicon und Containerbetrieb auf einem amd64-Server ausgelegt - node_modules werden nie vom Host kopiert (.dockerignore), Prisma erzeugt Engines sowohl für native als auch explizit für debian-openssl-3.0.x (siehe prisma/schema.prisma).

Das App-Image basiert auf node:22-trixie-slim, also auf derselben Debian-Version wie der Server (Debian 13). Die Version steht bewusst im Tag: node:22-slim zeigt je nach Zeitpunkt auf eine andere Debian-Version, und ein stiller Wechsel der libssl-Version würde die Prisma-Engine treffen. Debian 13 liefert OpenSSL 3.5 - Prisma bildet jedes OpenSSL 3.x auf den Binary-Target debian-openssl-3.0.x ab, der Eintrag in prisma/schema.prisma bleibt also unverändert richtig.

Build-Strategie (in dieser Präferenz):

  1. Direkt auf dem Server bauen (schnell, nativ, keine Emulation).
  2. Über einen CI-Runner auf dem Server (z. B. Forgejo Actions), Ergebnis in eine Registry pushen.
  3. Notfalls docker buildx build --platform linux/amd64 auf dem Mac (funktioniert, ist unter QEMU aber spürbar langsamer).

Automatisiertes Server-Setup (Debian, auch im LXC-Container)

scripts/install-server.sh richtet einen frischen Debian-13-Server (Trixie) in einem Durchlauf ein (Debian 12 läuft weiterhin, ältere Versionen lehnt das Skript ab) - ausgelegt auf den häufigsten Fall, einen LXC-Container (z. B. auf Proxmox). Auf einer VM oder einem Root-Server läuft dasselbe Skript unverändert.

apt-get update && apt-get install -y curl
curl -fsSL https://git.hippler.one/AxonByteDev/Silberling/raw/branch/main/scripts/install-server.sh -o install-server.sh
bash install-server.sh

Für den Beta-Kanal dieselben zwei Zeilen mit beta statt main in der URL und REPO_BRANCH=beta davor:

curl -fsSL https://git.hippler.one/AxonByteDev/Silberling/raw/branch/beta/scripts/install-server.sh -o install-server.sh
REPO_BRANCH=beta bash install-server.sh

Schritt für Schritt erklärt (inklusive Proxmox-Einstellungen, Reverse Proxy und erstem Admin-Konto) steht das im Wiki - der Beta-Weg unter Die Beta-Version installieren.

Was das Skript macht: Voraussetzungen prüfen (Debian-Version, systemd, Platz, RAM, Container-Umgebung), Docker Engine + Compose aus dem offiziellen Repo installieren, mit einem echten Testcontainer prüfen, ob Docker hier läuft, den Systemnutzer anlegen (kein Passwort, kein sudo, Gruppe docker), Firewall und automatische Sicherheitsupdates einrichten, das Repository klonen, eine .env mit zufälligen Secrets erzeugen, das Image bauen, den Stack starten und auf den Health-Check warten.

Docker läuft standardmäßig als root-Daemon, der Systemnutzer ist Mitglied der Gruppe docker. Das ist der einzige Modus, der in einem LXC-Container zuverlässig funktioniert - Rootless-Docker braucht verschachtelte User-Namespaces, die der Hypervisor von außen blockieren kann. Innerhalb eines unprivilegierten LXC-Containers ist die Gruppenmitgliedschaft gegenüber dem Host unkritisch. Auf einer echten VM lässt sich mit --rootless der Rootless-Daemon einrichten.

Für den LXC-Fall behebt das Skript die bekannten Stolpersteine selbst: Startet der Testcontainer nicht (der Klassiker "overlay auf overlay"), probiert es der Reihe nach overlay2, fuse-overlayfs und vfs durch und schreibt den funktionierenden Treiber nach /etc/docker/daemon.json. Erst wenn keiner läuft, bricht es ab - mit der konkreten Anweisung für den Proxmox-Host (pct set <CTID> --features nesting=1,keyctl=1,fuse=1). ufw und unattended-upgrades dürfen in eingeschränkten Containern fehlschlagen, ohne die Installation zu stoppen.

Option Wirkung
--preflight Nur prüfen, nichts verändern
--no-build Einrichten, aber noch nicht bauen (erst .env ausfüllen)
--no-firewall ufw unangetastet lassen
--rootless Rootless-Docker statt Gruppe docker (nur auf VMs sinnvoll)

Vorgaben per Umgebungsvariable: SERVICE_USER=filme APP_DIR=/opt/silberling REPO_BRANCH=beta bash install-server.sh. Weichen Nutzer oder Verzeichnis von der Vorgabe ab, trägt das Skript RELEASE_SERVICE_USER/RELEASE_INSTALL_DIR in die .env ein - damit passt der im Admin-Bereich angezeigte Update-Befehl zu genau dieser Installation. Der Update-Kanal wird bei einer frischen Installation passend zum Branch gesetzt.

Das Skript ist idempotent: Erneutes Ausführen überspringt erledigte Schritte und überschreibt eine vorhandene .env nie. Bewusst nicht angefasst: sshd_config (Root-/Passwort-Login) - das per Skript zu ändern ist ein klassischer Weg, sich selbst auszusperren.

Der Stack lauscht danach nur lokal auf 127.0.0.1:3000 - dafür in deinem bestehenden nginx einen proxy_pass http://127.0.0.1:3000;-Block einrichten (HTTPS übernimmt nginx). Läuft der Reverse Proxy auf einem anderen Host, APP_BIND_ADDRESS=0.0.0.0 in .env setzen und den Port gezielt für dessen IP freigeben - das Skript gibt am Ende die passenden Befehle aus.

Manuelles Setup / mitgeliefertes Caddy-Profil

Ohne das Installationsskript, z. B. auf einem bereits eingerichteten Server:

cp .env.production.example .env   # Werte anpassen, insbesondere Passwörter/Secrets
docker compose build
docker compose up -d
docker compose logs -f app

Migrationen laufen automatisch im Container-Entrypoint per prisma migrate deploy (nicht migrate dev), bevor der Server startet. docker-compose.yml enthält db und app immer, dazu optional caddy für TLS-Terminierung als eigenes Compose-Profil:

  • Mit Caddy (All-in-one, ohne eigenen Reverse Proxy): docker compose --profile caddy up -d. Domain in Caddyfile bzw. SILBERLING_DOMAIN setzen, Ports 80/443 freigeben - Caddy holt automatisch ein Let's-Encrypt-Zertifikat.
  • Mit bestehendem Reverse Proxy (Standard, auch beim Installationsskript): docker compose up -d (ohne --profile caddy) - der App-Port wird stattdessen über APP_BIND_ADDRESS/APP_PORT in .env veröffentlicht. HTTPS übernimmt der bestehende Proxy.

Volume-Rechte: Der Container läuft als non-root-User mit fester UID 1001 (user: "1001:1001" in docker-compose.yml). Ohne diese Übereinstimmung schlägt der Poster-Download in /data/posters auf dem Server still fehl, auch wenn er lokal (als eigener Nutzer) funktioniert hat.

Deployment-Einzeiler für ein Update:

git fetch origin --tags && git checkout main && git pull --ff-only origin main \
  && docker compose build --build-arg APP_VERSION=$(git describe --tags --always) \
  && docker compose up -d && docker compose logs -f app

Die Versionsnummer geht bewusst als --build-arg an den Build und nicht über ein vorangestelltes export: Ein vergessenes oder leeres APP_VERSION fällt in docker-compose.yml still auf den Default zurück, und die Instanz meldet sich danach dauerhaft als 0.0.0-dev. Denselben Befehl - fertig zum Kopieren und passend zum eingestellten Kanal - zeigt auch der Admin-Bereich unter "Updates" an.

Backup: über den Admin-Bereich (siehe "Backup und Wiederherstellung"). Wer zusätzlich automatisiert sichern will: pg_dump per Cronjob für die Datenbank, dazu das /data/posters-Volume.

Speicherbedarf: Poster werden einmalig heruntergeladen, als WebP in zwei Größen abgelegt und danach nie wieder von TMDB/Cinemeta nachgeladen. Rechnet mit ca. 30-50 KB pro Film (gemessen), also weit unter 150 MB bei 1000 Filmen.

Releases, Versionen und Updates

Die Versionsnummer steht ausschließlich im Git-Tag. Weder package.json (dauerhaft 0.0.0) noch sonst eine eingecheckte Datei enthält sie - eine eingecheckte Version würde bei jedem Merge beta -> main zum Konflikt. Die Datei VERSION ist gitignored und wird erzeugt: von scripts/publish.sh vor dem lokalen Build und vom Dockerfile aus ARG APP_VERSION. Zur Laufzeit liest src/lib/version.ts sie, Fallback 0.0.0-dev.

Zwei Kanäle: beta ist der Entwicklungs- und Default-Branch und erzeugt Prereleases (v1.2.0-beta.3), main enthält nur, was einen Pull Request überstanden hat, und erzeugt finale Releases (v1.2.0). Auf main wird nie direkt gepusht.

# Beta veröffentlichen (die Beta-Nummer zählt automatisch hoch)
scripts/publish.sh beta 1.2.0

# Finales Release, Schritt 1: Pull Request beta -> main erstellen
scripts/publish.sh release-pr 1.2.0

# ---> Jetzt im Browser: im PR den Reiter "Dateien geändert" durchlesen und
#      mit "Merge-Commit erstellen" mergen (nicht Squash, nicht Rebase). <---

# Finales Release, Schritt 2: Tag + Release anlegen, beta auf main nachziehen
scripts/publish.sh release-tag 1.2.0

Regeln, die man sich merken muss:

  • Version immer ohne v und ohne -beta übergeben (1.2.0).
  • Ohne ein ausdrückliches y pusht und taggt das Skript nichts. Vor jedem Push laufen npm ci, prisma generate, tsc --noEmit, eslint, vitest und next build als CI-Ersatz.
  • release-tag bricht ab, solange der Pull Request nicht gemergt ist - die Kontrolle im Browser lässt sich technisch nicht überspringen.
  • Voraussetzung: export FORGEJO_TOKEN="..." (Berechtigung write:repository, anzulegen unter https://git.hippler.one/user/settings/applications).

Update-Prüfung in der App: Die Instanz fragt die Release-API der Forgejo-Instanz ab - im Kanal "Beta" den neuesten Prerelease, im Kanal "Stabil" den neuesten finalen Release. Das Ergebnis liegt im Cache (AppSettings), damit Seitenaufrufe die API nicht selbst befragen; geprüft wird kurz nach dem Start und danach im eingestellten Intervall (Standard: alle 24 Stunden, im Admin-Bereich änderbar, 0 schaltet ab).

Angewendet wird ein Update nie automatisch. Die App hat bewusst keinen Zugriff auf den Docker-Socket und darf keine Container steuern - sie zeigt stattdessen den fertigen Befehl an, inklusive Wechsel auf den Systembenutzer. Über die Versionsliste im Admin-Bereich lässt sich auch gezielt auf eine ältere Version zurückwechseln, etwa nach einer fehlerhaften Beta.

Backup und Wiederherstellung

Der Admin-Bereich enthält unter "Backup & Wiederherstellung" ein vollständiges Backup der Instanz - gedacht für genau einen Zweck: Auf einem frisch aufgesetzten Server soll hinterher alles wieder so sein wie vorher.

Was drin ist: Das ZIP enthält database.json mit dem kompletten Inhalt aller Tabellen (Nutzerkonten inklusive Passwort-Hashes, sämtliche Bibliotheken aller Nutzer, der Filmkatalog, Barcodes, Freundschaften, Freigaben, Einreichungen, Korrekturwünsche, Einstellungen) sowie unter posters/ eine 1:1-Kopie des Poster-Verzeichnisses - alle Cover in beiden Größen und die Uploads noch offener Einreichungen. Dazu kommt manifest.json mit Version, Zeitpunkt und der Liste der angewendeten Datenbank-Migrationen.

Das Archiv ist eine gewöhnliche ZIP-Datei und lässt sich auf jedem Rechner öffnen. Es enthält Passwort-Hashes und E-Mail-Adressen aller Nutzer - also genauso behandeln wie ein Datenbank-Dump.

Wiederherstellen: Neuen Server wie oben beschrieben aufsetzen, mit einem Admin-Konto anmelden, im Admin-Bereich die ZIP-Datei auswählen, zur Bestätigung ERSETZEN eintippen und den Import starten. Der Import ersetzt sämtliche Daten der Zielinstanz. Danach wirst du abgemeldet, weil die Nutzerkonten jetzt die aus dem Backup sind - anmelden also mit den Zugangsdaten von der alten Instanz. Die Passwörter gelten weiter, denn die Hashes wandern mit.

Abgesichert ist der Ablauf so:

  • Erst wird das komplette Archiv ausgepackt und geprüft, dann erst gelöscht. Eine fremde oder beschädigte Datei lässt die vorhandenen Daten unangetastet.
  • Der Datenbank-Teil läuft in einer Transaktion: Scheitert eine Tabelle, bleibt der alte Stand vollständig erhalten.
  • Die Cover landen zuerst in einem Arbeitsverzeichnis und werden erst nach dem erfolgreichen Datenbank-Import an ihren Platz verschoben.
  • Enthält das Backup Migrationen, die die Zielinstanz noch nicht kennt (Backup aus einer neueren Version), bricht der Import ab und nennt die fehlende Migration. Dann zuerst die Instanz aktualisieren.

Reverse Proxy: Der Upload läuft als ein einziger Request. Bei nginx sonst unbedingt in den betreffenden location-Block aufnehmen, sonst scheitert der Import je nach Sammlungsgröße an 413 Request Entity Too Large oder einem Timeout (Caddy und das mitgelieferte Caddy-Profil brauchen das nicht):

client_max_body_size 0;      # oder z. B. 2G
proxy_read_timeout 600s;
proxy_send_timeout 600s;

Lizenzhinweise

  • Wikidata-Daten stehen unter CC0 und dürfen uneingeschränkt gespeichert und verändert werden.
  • Wikipedia-Beschreibungen stehen unter CC BY-SA - im UI wird die Quelle inkl. Link auf den Artikel angezeigt (siehe Filmdetailseite).
  • TMDB verlangt eine Attribution ("Dieses Produkt verwendet die TMDB API, ist aber nicht von TMDB zertifiziert oder unterstützt."), die im UI angezeigt wird, sobald TMDB-Daten einfließen.
  • Keine kommerzielle Nutzung dieser App - das würde die TMDB-Lizenzbedingungen verletzen.

Ausdrücklich nicht Teil dieser App

Kein Streaming, keine Mediendateien, keine Bezahlfunktion, kein Scraping von IMDb/Amazon/OFDb, keine kostenpflichtigen APIs.