stremio/apps/master-api/prisma/schema.prisma
Jos Vooges | STH 607386b94c Keep release tags in imported filenames and add OpenSubtitles NL pick.
Downloads confirm flow can select a Dutch subtitle that is written beside the movie on import.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 18:35:05 +02:00

428 lines
14 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
}
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")
}
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")
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)
iptvLine IptvLine?
playbackSessions PlaybackSession[]
@@map("addon_tokens")
}
/** Xtream/XUI credentials bound to one Stremio addon install (token). */
model IptvLine {
id String @id @default(uuid())
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")
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
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")
genres String[] @default([])
runtime Int?
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")
mediaFiles MediaFile[]
matches LibraryMatch[]
@@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([])
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[]
@@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")
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])
@@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")
available Boolean @default(true)
lastSeen DateTime @default(now()) @map("last_seen")
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])
@@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 + fixed 4K 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 4K movies library folder */
fourKHostPath String @map("four_k_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")
}
model DownloadJob {
id String @id @default(uuid())
nodeId String @map("node_id")
status DownloadJobStatus @default(DOWNLOADING)
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")
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")
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")
}