Zugeordnet wird ueber die TMDB-Kennung, die Jellyfin zu jedem Item mitliefert — dieselbe Kennung, auf der Flimmers Katalog ohnehin aufbaut. Eine unscharfe Suche ueber Titel und Jahr ist damit nicht noetig: die Zuordnung ist eindeutig oder sie findet nicht statt. Was Jellyfin ohne Kennung fuehrt, landet im Bericht statt falsch zugeordnet zu werden. Angemeldet wird mit Benutzername und Passwort, nicht mit einem API-Schluessel. Mit einem Schluessel liefert Jellyfin den Bestand ohne UserData, also ohne Gesehen-Status — und zwar ohne Fehlermeldung. Der Verbindungstest prueft deshalb ausdruecklich nach und sagt es, statt den Abgleich stumm ins Leere laufen zu lassen. Das Zugangstoken liegt mit AES-256-GCM verschluesselt in der Datenbank; abgeleitet aus AUTH_SECRET. Der Abgleich ist eine Einbahnstrasse: er ergaenzt, was auf dem Server liegt oder dort gelaufen ist, und loescht nie etwas, das in Flimmer steht. Fehlende Titel werden nur aufgenommen, wenn das Haekchen gesetzt ist — im taeglichen Lauf also nie. Entdoppelt wird zweifach: Episoden ueber (userId, episodeId, run), Filme ausdruecklich per Nachschlagen. Bei Filmen ist episodeId null, und Postgres behandelt NULL im eindeutigen Index als jeweils verschieden — der Index greift dort nicht. Watch.source haelt fest, was von wo kam. Ohne diese Angabe liesse sich spaeter nicht mehr sagen, welche Sichtung aus Jellyfin stammt. Umgesetzt sind Stufe 1 und 2. Stufe 3 (Zurueckschreiben) kommt in der Abnahmeliste nicht vor und braeuchte eine Versionsweiche, die sich ohne echten Server nicht verlaesslich pruefen laesst; das Feld writeBack ist angelegt, die Logik dahinter bleibt leer. Elf Tests gegen nachgebildete Antworten. Zusaetzlich der ganze Klickweg gegen einen echten HTTP-Server im Produktionsbau: verbinden, pruefen, abgleichen, zweiter Lauf ohne Dubletten, und der Warnfall ohne UserData. Refs #1
This commit is contained in:
+81
-1
@@ -54,6 +54,7 @@ model User {
|
||||
apiKeys ApiKey[]
|
||||
oidcAccounts OidcAccount[]
|
||||
passkeys Passkey[]
|
||||
mediaServers MediaServerConnection[]
|
||||
entries LibraryEntry[]
|
||||
watches Watch[]
|
||||
tags Tag[]
|
||||
@@ -207,6 +208,7 @@ model Title {
|
||||
watches Watch[]
|
||||
providers TitleProvider[]
|
||||
universeItems UniverseItem[]
|
||||
serverItems MediaServerItem[]
|
||||
|
||||
@@unique([type, tmdbId])
|
||||
@@index([type])
|
||||
@@ -250,7 +252,8 @@ model Episode {
|
||||
airDate DateTime?
|
||||
runtime Int?
|
||||
|
||||
watches Watch[]
|
||||
watches Watch[]
|
||||
serverItems MediaServerItem[]
|
||||
|
||||
@@unique([titleId, seasonNumber, episodeNumber])
|
||||
@@index([titleId])
|
||||
@@ -391,6 +394,11 @@ model Watch {
|
||||
run Int @default(0)
|
||||
watchedAt DateTime @default(now())
|
||||
|
||||
/// Woher die Sichtung stammt. Ohne diese Angabe liesse sich spaeter nicht
|
||||
/// mehr sagen, was von einem Medienserver kam — und ein Zuruecknehmen des
|
||||
/// Abgleichs waere unmoeglich.
|
||||
source WatchSource @default(FLIMMER)
|
||||
|
||||
/// Tatsächlich angerechnete Minuten — friert die Laufzeit zum Zeitpunkt der
|
||||
/// Sichtung ein, damit spätere TMDB-Korrekturen die Statistik nicht verbiegen.
|
||||
minutes Int @default(0)
|
||||
@@ -548,3 +556,75 @@ model SyncRun {
|
||||
|
||||
@@index([kind, startedAt])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Medienserver (Jellyfin, Emby)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum WatchSource {
|
||||
FLIMMER
|
||||
JELLYFIN
|
||||
}
|
||||
|
||||
enum MediaServerKind {
|
||||
JELLYFIN
|
||||
EMBY
|
||||
}
|
||||
|
||||
/// Verbindung zu einem Medienserver. Je Nutzer und Server eine.
|
||||
model MediaServerConnection {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
kind MediaServerKind @default(JELLYFIN)
|
||||
/// Anzeigename des Servers, wie ihn Jellyfin meldet.
|
||||
name String @default("Jellyfin")
|
||||
baseUrl String
|
||||
|
||||
/// Zugangstoken, mit AUTH_SECRET verschluesselt — im Klartext hat er hier
|
||||
/// nichts verloren.
|
||||
token String
|
||||
/// Kennung des Jellyfin-Nutzers, dessen Bestand und Sichtungen zaehlen.
|
||||
serverUserId String
|
||||
/// Anmeldename, nur zur Anzeige.
|
||||
serverUserName String @default("")
|
||||
|
||||
enabled Boolean @default(true)
|
||||
syncOwned Boolean @default(true)
|
||||
syncWatched Boolean @default(true)
|
||||
/// Stufe 3: Haekchen aus Flimmer zurueck nach Jellyfin schreiben.
|
||||
writeBack Boolean @default(false)
|
||||
|
||||
lastSyncAt DateTime?
|
||||
lastSyncReport Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
items MediaServerItem[]
|
||||
|
||||
@@unique([userId, baseUrl])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
/// Bereits zugeordnete Items. Macht Folgelaeufe billig und ist die
|
||||
/// Voraussetzung fuers Zurueckschreiben.
|
||||
model MediaServerItem {
|
||||
id String @id @default(cuid())
|
||||
connectionId String
|
||||
connection MediaServerConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Item-Kennung auf dem Medienserver.
|
||||
externalId String
|
||||
|
||||
titleId String?
|
||||
title Title? @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
episodeId String?
|
||||
episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([connectionId, externalId])
|
||||
@@index([connectionId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user