Show end times on detail/player, tighten TV chrome, and let profiles toggle or upload a cropped photo via API and mobile/iOS.
734 lines
26 KiB
Text
734 lines
26 KiB
Text
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
enum NodeStatus {
|
||
ONLINE
|
||
OFFLINE
|
||
DEGRADED
|
||
REVOKED
|
||
UPDATING
|
||
SCANNING
|
||
ERROR
|
||
}
|
||
|
||
enum PlaybackSessionStatus {
|
||
ACTIVE
|
||
IDLE
|
||
EXPIRED
|
||
REVOKED
|
||
COMPLETED
|
||
}
|
||
|
||
enum LocationType {
|
||
LOCAL
|
||
REMOTE
|
||
}
|
||
|
||
enum LibraryShelfKind {
|
||
MOVIE
|
||
SERIES
|
||
}
|
||
|
||
enum DownloadJobStatus {
|
||
DOWNLOADING
|
||
FINISHED
|
||
IMPORTING
|
||
IMPORTED
|
||
FAILED
|
||
}
|
||
|
||
enum GooglePlaySyncStatus {
|
||
NOT_ENABLED
|
||
PENDING_ADD
|
||
ACTIVE
|
||
PENDING_REMOVE
|
||
REMOVED
|
||
ERROR
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(uuid())
|
||
email String @unique
|
||
passwordHash String @map("password_hash")
|
||
name String?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
addonTokens AddonToken[]
|
||
sessions AdminSession[]
|
||
|
||
@@map("users")
|
||
}
|
||
|
||
/** App-gebruiker (Android/iOS/Tizen) — huishouden met optioneel IPTV + Stremio-addons. */
|
||
model ViewerUser {
|
||
id String @id @default(uuid())
|
||
email String @unique
|
||
passwordHash String @map("password_hash")
|
||
name String?
|
||
enabled Boolean @default(true)
|
||
appAccess Boolean @default(true) @map("app_access")
|
||
/** Live TV in de apps (Xtream-lijn op deze gebruiker). */
|
||
iptvAccess Boolean @default(false) @map("iptv_access")
|
||
/** @deprecated Legacy: IPTV via addon-token; voorkeur = iptvLine op deze user. */
|
||
iptvAddonTokenId String? @map("iptv_addon_token_id")
|
||
/** Google-account voor Play Closed Testing; fallback = email */
|
||
googlePlayEmail String? @map("google_play_email")
|
||
/** Gewenste Play-distributietoegang (sync met Google Group) */
|
||
googlePlayAccess Boolean @default(false) @map("google_play_access")
|
||
googlePlayMembershipId String? @map("google_play_membership_id")
|
||
googlePlaySyncStatus GooglePlaySyncStatus @default(NOT_ENABLED) @map("google_play_sync_status")
|
||
googlePlayLastSyncedAt DateTime? @map("google_play_last_synced_at")
|
||
googlePlaySyncError String? @map("google_play_sync_error") @db.Text
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
devices ViewerDevice[]
|
||
profiles ViewerProfile[]
|
||
linkCodes DeviceLinkCode[]
|
||
googlePlayAuditLogs GooglePlayAccessAuditLog[]
|
||
/** Legacy koppeling — behouden voor migratie. */
|
||
iptvAddonToken AddonToken? @relation("ViewerLegacyIptvToken", fields: [iptvAddonTokenId], references: [id], onDelete: SetNull)
|
||
iptvLine IptvLine?
|
||
addonTokens AddonToken[] @relation("ViewerAddonTokens")
|
||
|
||
@@map("viewer_users")
|
||
}
|
||
|
||
/** Bruteforce-bescherming op e-mail/wachtwoord (claim + direct login). */
|
||
model ViewerLoginLock {
|
||
email String @id
|
||
failedCount Int @default(0) @map("failed_count")
|
||
lockedUntil DateTime? @map("locked_until")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
@@map("viewer_login_locks")
|
||
}
|
||
|
||
/** Netflix-stijl kijkprofiel onder één ViewerUser (voortgang/prefs per profiel). */
|
||
model ViewerProfile {
|
||
id String @id @default(uuid())
|
||
viewerUserId String @map("viewer_user_id")
|
||
name String
|
||
avatarId String @default("fox") @map("avatar_id")
|
||
/** Eigen geüploade foto (relatief pad onder data/profile-avatars). */
|
||
avatarImage String? @map("avatar_image")
|
||
/** Toon avatar/foto in UI (uit = alleen naam). */
|
||
showAvatar Boolean @default(true) @map("show_avatar")
|
||
isOwner Boolean @default(false) @map("is_owner")
|
||
isKids Boolean @default(false) @map("is_kids")
|
||
pinHash String? @map("pin_hash")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
|
||
devices ViewerDevice[]
|
||
progress WatchProgress[]
|
||
shelfPrefs ViewerShelfPreference[]
|
||
favorites ViewerFavorite[]
|
||
|
||
@@unique([viewerUserId, name])
|
||
@@index([viewerUserId])
|
||
@@map("viewer_profiles")
|
||
}
|
||
|
||
model ViewerDevice {
|
||
id String @id @default(uuid())
|
||
viewerUserId String @map("viewer_user_id")
|
||
name String
|
||
platform String @default("android_tv")
|
||
refreshTokenHash String @unique @map("refresh_token_hash")
|
||
activeProfileId String? @map("active_profile_id")
|
||
lastSeenAt DateTime? @map("last_seen_at")
|
||
revoked Boolean @default(false)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
|
||
activeProfile ViewerProfile? @relation(fields: [activeProfileId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([viewerUserId])
|
||
@@index([activeProfileId])
|
||
@@map("viewer_devices")
|
||
}
|
||
|
||
/** Kortstondige koppelcode (TV toont code → gebruiker claimt op /link). */
|
||
model DeviceLinkCode {
|
||
id String @id @default(uuid())
|
||
codeHash String @unique @map("code_hash")
|
||
/** Display-code (alleen tot claim; daarna null) */
|
||
codeDisplay String? @map("code_display")
|
||
deviceName String @map("device_name")
|
||
platform String @default("android_tv")
|
||
expiresAt DateTime @map("expires_at")
|
||
consumedAt DateTime? @map("consumed_at")
|
||
viewerUserId String? @map("viewer_user_id")
|
||
/** Eenmalig token voor device poll na claim */
|
||
claimTokenHash String? @unique @map("claim_token_hash")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
viewer ViewerUser? @relation(fields: [viewerUserId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([expiresAt])
|
||
@@map("device_link_codes")
|
||
}
|
||
|
||
enum WatchMediaType {
|
||
MOVIE
|
||
EPISODE
|
||
}
|
||
|
||
model WatchProgress {
|
||
id String @id @default(uuid())
|
||
profileId String @map("profile_id")
|
||
mediaType WatchMediaType @map("media_type")
|
||
movieId String? @map("movie_id")
|
||
episodeId String? @map("episode_id")
|
||
seriesId String? @map("series_id")
|
||
positionMs Int @default(0) @map("position_ms")
|
||
durationMs Int @default(0) @map("duration_ms")
|
||
completed Boolean @default(false)
|
||
hiddenFromContinue Boolean @default(false) @map("hidden_from_continue")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
profile ViewerProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([profileId, movieId])
|
||
@@unique([profileId, episodeId])
|
||
@@index([profileId, updatedAt])
|
||
@@index([profileId, hiddenFromContinue, completed])
|
||
@@map("watch_progress")
|
||
}
|
||
|
||
model ViewerShelfPreference {
|
||
id String @id @default(uuid())
|
||
profileId String @map("profile_id")
|
||
shelfId String @map("shelf_id")
|
||
visible Boolean @default(true)
|
||
sortOrder Int @default(0) @map("sort_order")
|
||
|
||
profile ViewerProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([profileId, shelfId])
|
||
@@index([profileId, sortOrder])
|
||
@@map("viewer_shelf_preferences")
|
||
}
|
||
|
||
enum FavoriteMediaType {
|
||
MOVIE
|
||
SERIES
|
||
}
|
||
|
||
/** Favorieten/bookmarks per kijkprofiel — gesynchroniseerd over devices. */
|
||
model ViewerFavorite {
|
||
id String @id @default(uuid())
|
||
profileId String @map("profile_id")
|
||
mediaType FavoriteMediaType @map("media_type")
|
||
mediaId String @map("media_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
profile ViewerProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([profileId, mediaType, mediaId])
|
||
@@index([profileId, createdAt])
|
||
@@map("viewer_favorites")
|
||
}
|
||
|
||
model AdminSession {
|
||
id String @id @default(uuid())
|
||
userId String @map("user_id")
|
||
tokenHash String @unique @map("token_hash")
|
||
expiresAt DateTime @map("expires_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("admin_sessions")
|
||
}
|
||
|
||
model AddonToken {
|
||
id String @id @default(uuid())
|
||
userId String @map("user_id")
|
||
/** Gebruiker (huishouden) waaraan deze Stremio-addon hangt. */
|
||
viewerUserId String? @map("viewer_user_id")
|
||
tokenHash String @unique @map("token_hash")
|
||
label String?
|
||
revoked Boolean @default(false)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
viewerUser ViewerUser? @relation("ViewerAddonTokens", fields: [viewerUserId], references: [id], onDelete: SetNull)
|
||
iptvLine IptvLine?
|
||
playbackSessions PlaybackSession[]
|
||
legacyIptvViewers ViewerUser[] @relation("ViewerLegacyIptvToken")
|
||
|
||
@@index([viewerUserId])
|
||
@@map("addon_tokens")
|
||
}
|
||
|
||
/** Xtream/XUI credentials — bij voorkeur per ViewerUser; legacy optioneel per AddonToken. */
|
||
model IptvLine {
|
||
id String @id @default(uuid())
|
||
/** Primair: IPTV-lijn van één app-gebruiker. */
|
||
viewerUserId String? @unique @map("viewer_user_id")
|
||
/** Legacy: IPTV direct op Stremio-addon (pre-merge). */
|
||
addonTokenId String? @unique @map("addon_token_id")
|
||
label String?
|
||
baseUrl String @map("base_url")
|
||
username String
|
||
/** AES-GCM ciphertext (base64) */
|
||
passwordEnc String @map("password_enc")
|
||
enableLive Boolean @default(true) @map("enable_live")
|
||
enableVod Boolean @default(true) @map("enable_vod")
|
||
enableSeries Boolean @default(true) @map("enable_series")
|
||
lastOkAt DateTime? @map("last_ok_at")
|
||
lastError String? @map("last_error")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
viewerUser ViewerUser? @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
|
||
addonToken AddonToken? @relation(fields: [addonTokenId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("iptv_lines")
|
||
}
|
||
|
||
model Location {
|
||
id String @id @default(uuid())
|
||
name String @unique
|
||
description String?
|
||
priority Int @default(0)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
nodes Node[]
|
||
|
||
@@map("locations")
|
||
}
|
||
|
||
model Node {
|
||
id String @id @default(uuid())
|
||
name String
|
||
hostname String?
|
||
locationId String? @map("location_id")
|
||
locationType LocationType @default(LOCAL) @map("location_type")
|
||
status NodeStatus @default(OFFLINE)
|
||
version String?
|
||
architecture String?
|
||
publicStreamUrl String @map("public_stream_url")
|
||
lastSeen DateTime? @map("last_seen")
|
||
totalStorage BigInt @default(0) @map("total_storage")
|
||
freeStorage BigInt @default(0) @map("free_storage")
|
||
activeStreams Int @default(0) @map("active_streams")
|
||
currentBandwidth BigInt @default(0) @map("current_bandwidth")
|
||
libraryFileCount Int @default(0) @map("library_file_count")
|
||
lastRevision Int @default(0) @map("last_revision")
|
||
/** Node scanner: "off" or duration like "24h" / "6h" */
|
||
fullScanInterval String @default("off") @map("full_scan_interval")
|
||
/** Nightly catch-up time HH:MM in node local TZ */
|
||
fullScanAt String @default("03:30") @map("full_scan_at")
|
||
/** Host/container scan roots for movies (e.g. /mnt/user/NL Films) — legacy; prefer scanRoots */
|
||
moviesPaths String[] @default([]) @map("movies_paths")
|
||
/** Host/container scan roots for series — legacy; prefer scanRoots */
|
||
seriesPaths String[] @default([]) @map("series_paths")
|
||
revoked Boolean @default(false)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
location Location? @relation(fields: [locationId], references: [id])
|
||
credentials NodeCredential?
|
||
mediaFiles MediaFile[]
|
||
playbackSessions PlaybackSession[]
|
||
scanRoots NodeScanRoot[]
|
||
synologyDsConfig SynologyDsConfig?
|
||
downloadJobs DownloadJob[]
|
||
|
||
@@map("nodes")
|
||
}
|
||
|
||
/** Named Stremio catalog / shelf shared across all nodes (e.g. Kids Films). */
|
||
model LibraryShelf {
|
||
id String @id @default(uuid())
|
||
name String
|
||
homepageName String? @map("homepage_name")
|
||
slug String @unique
|
||
kind LibraryShelfKind
|
||
sortOrder Int @default(0) @map("sort_order")
|
||
enabled Boolean @default(true)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
scanRoots NodeScanRoot[]
|
||
mediaFiles MediaFile[]
|
||
|
||
@@map("library_shelves")
|
||
}
|
||
|
||
/** One filesystem root on a node bound to a library shelf. */
|
||
model NodeScanRoot {
|
||
id String @id @default(uuid())
|
||
nodeId String @map("node_id")
|
||
path String
|
||
shelfId String @map("shelf_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||
shelf LibraryShelf @relation(fields: [shelfId], references: [id], onDelete: Restrict)
|
||
|
||
@@unique([nodeId, path])
|
||
@@index([shelfId])
|
||
@@map("node_scan_roots")
|
||
}
|
||
|
||
model NodeCredential {
|
||
id String @id @default(uuid())
|
||
nodeId String @unique @map("node_id")
|
||
apiKeyHash String @map("api_key_hash")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
rotatedAt DateTime? @map("rotated_at")
|
||
|
||
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("node_credentials")
|
||
}
|
||
|
||
model NodeEnrollmentToken {
|
||
id String @id @default(uuid())
|
||
tokenHash String @unique @map("token_hash")
|
||
name String?
|
||
locationId String? @map("location_id")
|
||
expiresAt DateTime @map("expires_at")
|
||
usedAt DateTime? @map("used_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@map("node_enrollment_tokens")
|
||
}
|
||
|
||
model Movie {
|
||
id String @id @default(uuid())
|
||
title String
|
||
originalTitle String? @map("original_title")
|
||
year Int?
|
||
overview String?
|
||
posterUrl String? @map("poster_url")
|
||
backdropUrl String? @map("backdrop_url")
|
||
logoUrl String? @map("logo_url")
|
||
genres String[] @default([])
|
||
/** TMDB adult-flag; kids-profielen filteren hierop. */
|
||
adult Boolean @default(false)
|
||
runtime Int?
|
||
imdbId String? @unique @map("imdb_id")
|
||
tmdbId Int? @unique @map("tmdb_id")
|
||
/** TMDB release_date — for “Net uitgebracht”. */
|
||
releaseDate DateTime? @map("release_date")
|
||
/**
|
||
* Eerste keer dat deze titel in het cluster beschikbaar kwam.
|
||
* Bump’t niet als een tweede node dezelfde film aanlevert.
|
||
*/
|
||
libraryAddedAt DateTime? @map("library_added_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
mediaFiles MediaFile[]
|
||
matches LibraryMatch[]
|
||
subtitles LibrarySubtitle[]
|
||
|
||
@@index([adult])
|
||
@@index([libraryAddedAt])
|
||
@@index([releaseDate])
|
||
@@map("movies")
|
||
}
|
||
|
||
model Series {
|
||
id String @id @default(uuid())
|
||
title String
|
||
year Int?
|
||
overview String?
|
||
posterUrl String? @map("poster_url")
|
||
backdropUrl String? @map("backdrop_url")
|
||
logoUrl String? @map("logo_url")
|
||
genres String[] @default([])
|
||
/** TMDB adult-flag; kids-profielen filteren hierop. */
|
||
adult Boolean @default(false)
|
||
imdbId String? @unique @map("imdb_id")
|
||
tmdbId Int? @unique @map("tmdb_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
seasons Season[]
|
||
episodes Episode[]
|
||
matches LibraryMatch[]
|
||
|
||
@@index([adult])
|
||
@@map("series")
|
||
}
|
||
|
||
model Season {
|
||
id String @id @default(uuid())
|
||
seriesId String @map("series_id")
|
||
seasonNumber Int @map("season_number")
|
||
|
||
series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
|
||
episodes Episode[]
|
||
|
||
@@unique([seriesId, seasonNumber])
|
||
@@map("seasons")
|
||
}
|
||
|
||
model Episode {
|
||
id String @id @default(uuid())
|
||
seriesId String @map("series_id")
|
||
seasonId String @map("season_id")
|
||
seasonNumber Int @map("season_number")
|
||
episodeNumber Int @map("episode_number")
|
||
title String?
|
||
overview String?
|
||
stillUrl String? @map("still_url")
|
||
runtime Int?
|
||
airDate DateTime? @map("air_date")
|
||
imdbId String? @map("imdb_id")
|
||
tmdbId Int? @map("tmdb_id")
|
||
/** Eerste keer dat deze aflevering in het cluster kwam (geen bump door 2e node). */
|
||
libraryAddedAt DateTime? @map("library_added_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
|
||
season Season @relation(fields: [seasonId], references: [id], onDelete: Cascade)
|
||
mediaFiles MediaFile[]
|
||
matches LibraryMatch[]
|
||
|
||
@@unique([seriesId, seasonNumber, episodeNumber])
|
||
@@index([libraryAddedAt])
|
||
@@map("episodes")
|
||
}
|
||
|
||
model MediaFile {
|
||
id String @id @default(uuid())
|
||
nodeId String @map("node_id")
|
||
movieId String? @map("movie_id")
|
||
episodeId String? @map("episode_id")
|
||
shelfId String? @map("shelf_id")
|
||
localFileId String @map("local_file_id")
|
||
sizeBytes BigInt @map("size_bytes")
|
||
container String?
|
||
resolution String?
|
||
videoCodec String? @map("video_codec")
|
||
videoBitrate Int? @map("video_bitrate")
|
||
hdrType String? @map("hdr_type")
|
||
audioCodec String? @map("audio_codec")
|
||
audioChannels String? @map("audio_channels")
|
||
audioLanguage String? @map("audio_language")
|
||
releaseName String @map("release_name")
|
||
/** Absolute path on the media-node host (filled from node ≥1.3.7). */
|
||
path String? @map("path")
|
||
available Boolean @default(true)
|
||
lastSeen DateTime @default(now()) @map("last_seen")
|
||
/// Eerste indexering van dit bestand: bij create ≈ file-mtime (niet scan-now).
|
||
discoveredAt DateTime @default(now()) @map("discovered_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||
movie Movie? @relation(fields: [movieId], references: [id])
|
||
episode Episode? @relation(fields: [episodeId], references: [id])
|
||
shelf LibraryShelf? @relation(fields: [shelfId], references: [id], onDelete: SetNull)
|
||
playbackSessions PlaybackSession[]
|
||
|
||
@@unique([nodeId, localFileId])
|
||
@@index([shelfId])
|
||
@@index([discoveredAt])
|
||
@@map("media_files")
|
||
}
|
||
|
||
model LibraryMatch {
|
||
id String @id @default(uuid())
|
||
movieId String? @map("movie_id")
|
||
seriesId String? @map("series_id")
|
||
episodeId String? @map("episode_id")
|
||
releaseName String @map("release_name")
|
||
ignored Boolean @default(false)
|
||
manual Boolean @default(false)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
movie Movie? @relation(fields: [movieId], references: [id])
|
||
series Series? @relation(fields: [seriesId], references: [id])
|
||
episode Episode? @relation(fields: [episodeId], references: [id])
|
||
|
||
@@map("library_matches")
|
||
}
|
||
|
||
model PlaybackSession {
|
||
id String @id @default(uuid())
|
||
tokenHash String @unique @map("token_hash")
|
||
userId String? @map("user_id")
|
||
addonTokenId String? @map("addon_token_id")
|
||
nodeId String @map("node_id")
|
||
mediaFileId String @map("media_file_id")
|
||
status PlaybackSessionStatus @default(ACTIVE)
|
||
idleTimeoutSeconds Int @map("idle_timeout_seconds")
|
||
absoluteExpiresAt DateTime @map("absolute_expires_at")
|
||
lastActivity DateTime @default(now()) @map("last_activity")
|
||
bytesSent BigInt @default(0) @map("bytes_sent")
|
||
lastByteOffset BigInt @default(0) @map("last_byte_offset")
|
||
fileSizeBytes BigInt @default(0) @map("file_size_bytes")
|
||
revoked Boolean @default(false)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
node Node @relation(fields: [nodeId], references: [id])
|
||
mediaFile MediaFile @relation(fields: [mediaFileId], references: [id])
|
||
addonToken AddonToken? @relation(fields: [addonTokenId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([status, lastActivity])
|
||
@@index([addonTokenId])
|
||
@@map("playback_sessions")
|
||
}
|
||
|
||
/** Synology Download Station credentials + library import paths (per node). */
|
||
model SynologyDsConfig {
|
||
id String @id @default(uuid())
|
||
nodeId String @unique @map("node_id")
|
||
/** DSM base URL, e.g. https://nas.local:5001 */
|
||
baseUrl String @map("base_url")
|
||
username String
|
||
passwordEnc String @map("password_enc")
|
||
/** DS task destination (share-relative), e.g. Downloads/complete */
|
||
downloadDestination String @map("download_destination")
|
||
/** Absolute host path of that destination on the media-node */
|
||
downloadHostPath String @map("download_host_path")
|
||
/** Absolute host path for 1080p movies (e.g. …/Media/Films/Films) */
|
||
filmsHostPath String @default("") @map("films_host_path")
|
||
/** Absolute host path for 4K movies (e.g. …/Media/Films/4K) */
|
||
fourKHostPath String @map("four_k_host_path")
|
||
/** Absolute host path for series (e.g. …/Media/Series/Series) */
|
||
seriesHostPath String @default("") @map("series_host_path")
|
||
enabled Boolean @default(true)
|
||
lastOkAt DateTime? @map("last_ok_at")
|
||
lastError String? @map("last_error")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("synology_ds_configs")
|
||
}
|
||
|
||
enum DownloadLibraryTarget {
|
||
FILMS
|
||
FOUR_K
|
||
SERIES
|
||
}
|
||
|
||
enum DownloadSeriesMode {
|
||
EPISODE
|
||
SEASON
|
||
}
|
||
|
||
model DownloadJob {
|
||
id String @id @default(uuid())
|
||
nodeId String @map("node_id")
|
||
status DownloadJobStatus @default(DOWNLOADING)
|
||
/** Where to import after DS finishes */
|
||
libraryTarget DownloadLibraryTarget @default(FOUR_K) @map("library_target")
|
||
/** Series only: single episode vs whole season pack */
|
||
seriesMode DownloadSeriesMode? @map("series_mode")
|
||
sourceUri String @map("source_uri")
|
||
sourceTitle String @map("source_title")
|
||
dsTaskId String? @map("ds_task_id")
|
||
tmdbId Int? @map("tmdb_id")
|
||
imdbId String? @map("imdb_id")
|
||
movieTitle String @map("movie_title")
|
||
movieYear Int? @map("movie_year")
|
||
posterUrl String? @map("poster_url")
|
||
seasonNumber Int? @map("season_number")
|
||
episodeNumber Int? @map("episode_number")
|
||
episodeTitle String? @map("episode_title")
|
||
/** Season pack tracking JSON */
|
||
episodesJson String? @map("episodes_json") @db.Text
|
||
destFolderName String? @map("dest_folder_name")
|
||
sourcePath String? @map("source_path")
|
||
importedPath String? @map("imported_path")
|
||
releaseTags String? @map("release_tags")
|
||
subtitleFileId Int? @map("subtitle_file_id")
|
||
subtitleLang String? @map("subtitle_lang")
|
||
subtitleSource String? @map("subtitle_source")
|
||
/** Cached subtitle body (downloaded at job create) */
|
||
subtitleContent String? @map("subtitle_content") @db.Text
|
||
subtitleError String? @map("subtitle_error")
|
||
subtitlePath String? @map("subtitle_path")
|
||
error String?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([status, updatedAt])
|
||
@@index([nodeId, status])
|
||
@@map("download_jobs")
|
||
}
|
||
|
||
/** Singleton-style integration credentials (OpenSubtitles, etc.). */
|
||
model IntegrationSetting {
|
||
id String @id
|
||
apiKeyEnc String? @map("api_key_enc")
|
||
username String?
|
||
passwordEnc String? @map("password_enc")
|
||
/** Must match OpenSubtitles.com API Consumer / User-Agent exactly */
|
||
userAgent String? @map("user_agent")
|
||
/** Non-secret JSON config (e.g. Google Play group/project fields) */
|
||
configJson String? @map("config_json") @db.Text
|
||
enabled Boolean @default(true)
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@map("integration_settings")
|
||
}
|
||
|
||
/** Sidecar / OpenSubtitles text stored for Stremio stream.subtitles. */
|
||
model LibrarySubtitle {
|
||
id String @id @default(uuid())
|
||
movieId String? @map("movie_id")
|
||
tmdbId Int? @map("tmdb_id")
|
||
imdbId String? @map("imdb_id")
|
||
language String @default("nl")
|
||
format String @default("srt")
|
||
content String
|
||
fileName String? @map("file_name")
|
||
downloadJobId String? @map("download_job_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
movie Movie? @relation(fields: [movieId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([movieId])
|
||
@@index([tmdbId])
|
||
@@index([imdbId])
|
||
@@map("library_subtitles")
|
||
}
|
||
|
||
/** Audit trail voor Google Play group-lidmaatschap (geen secrets). */
|
||
model GooglePlayAccessAuditLog {
|
||
id String @id @default(uuid())
|
||
adminUserId String? @map("admin_user_id")
|
||
adminEmail String? @map("admin_email")
|
||
viewerUserId String @map("viewer_user_id")
|
||
targetEmail String @map("target_email")
|
||
action String
|
||
result String
|
||
membershipId String? @map("membership_id")
|
||
errorMessage String? @map("error_message") @db.Text
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([viewerUserId, createdAt])
|
||
@@map("google_play_access_audit_logs")
|
||
}
|