- TypeScript 87.4%
- Shell 9.1%
- Dockerfile 1.8%
- JavaScript 0.9%
- CSS 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| prisma | ||
| public | ||
| scripts | ||
| src | ||
| wiki | ||
| .dockerignore | ||
| .env.example | ||
| .env.production.example | ||
| .gitignore | ||
| .nvmrc | ||
| AGENTS.md | ||
| Caddyfile | ||
| CLAUDE.md | ||
| components.json | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
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:
- Eigene
barcodes-Tabelle → sofortiger Treffer, Film wird ohne Rückfrage hinzugefügt (status: "added"), danach per „Passt / Falsch" bestätigbar. - UPCitemdb (kostenloser Trial, 100 Anfragen/Tag/IP, max. 6/Minute) liefert einen rohen Produkttitel.
- Titel-Normalisierung (
src/lib/barcode/normalizeTitle.ts) entfernt Medien-/Editionshinweise, extrahiert Jahr/Medium/Edition. - Suche über die Metadaten-Provider-Kette, Treffer nach Titelähnlichkeit + Jahr sortiert → Bestätigungsdialog.
- 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]/libraryansehen. - Ü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 aktuellenvisibility-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 unternpm run dev- ein SW im Next-Dev-Server würde mit dessen Hot-Module-Replacement kollidieren. Zum Testen alsonpm 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
/statszeigt 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. /importnimmt 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 dieselberesolveBarcode()-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
CONFIRMEDgesetzt. - 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:
mkcertauf dem Mac installieren, ein Zertifikat für die LAN-IP ausstellen, die Root-CA auf dem iPhone installieren und unter "Zertifikatsvertrauen" aktivieren. Dannnext dev --experimental-httpsstarten.
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):
- Direkt auf dem Server bauen (schnell, nativ, keine Emulation).
- Über einen CI-Runner auf dem Server (z. B. Forgejo Actions), Ergebnis in eine Registry pushen.
- Notfalls
docker buildx build --platform linux/amd64auf 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 inCaddyfilebzw.SILBERLING_DOMAINsetzen, 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 überAPP_BIND_ADDRESS/APP_PORTin.envverö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
vund ohne-betaübergeben (1.2.0). - Ohne ein ausdrückliches
ypusht und taggt das Skript nichts. Vor jedem Push laufennpm ci,prisma generate,tsc --noEmit,eslint,vitestundnext buildals CI-Ersatz. release-tagbricht ab, solange der Pull Request nicht gemergt ist - die Kontrolle im Browser lässt sich technisch nicht überspringen.- Voraussetzung:
export FORGEJO_TOKEN="..."(Berechtigungwrite:repository, anzulegen unterhttps://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.