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 } 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? @@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[] @@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") 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") 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]) @@map("playback_sessions") }