generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum RequestStatus { pending completed needs_attention } enum JobState { requested matching matched downloading tagging imported needs_attention } enum MonitoredReleaseState { wanted grabbed fulfilled } model Request { id String @id @default(cuid()) artist String album String status RequestStatus @default(pending) createdAt DateTime @default(now()) job Job? libraryItem LibraryItem? monitoredReleaseId String? monitoredRelease MonitoredRelease? @relation(fields: [monitoredReleaseId], references: [id], onDelete: SetNull) } model Job { id String @id @default(cuid()) request Request @relation(fields: [requestId], references: [id], onDelete: Cascade) requestId String @unique state JobState @default(requested) currentStage String @default("intake") attempts Int @default(0) error String? claimedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt downloadProgress Float @default(0) candidates Candidate[] } model Candidate { id String @id @default(cuid()) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) jobId String source String format String qualityClass Int trackCount Int confidence Float sourceRef String chosen Boolean @default(false) createdAt DateTime @default(now()) } model LibraryItem { id String @id @default(cuid()) request Request? @relation(fields: [requestId], references: [id], onDelete: Cascade) requestId String? @unique artist String album String path String source String format String qualityClass Int importedAt DateTime @default(now()) // MusicBrainz release-group MBID when known (resolved at manual import) — gives the album // cover art without depending on a matching MonitoredRelease row. rgMbid String? // The album's on-disk audio filenames ("## Title.ext"), captured at import + scan, so the // Library modal can show the REAL tracks on disk (revealing incomplete/edition mismatches) // instead of MusicBrainz's canonical listing. Empty for items imported before this existed // (re-scan to populate). trackNames String[] @default([]) @@unique([artist, album]) } model Config { key String @id value String secret Boolean @default(false) updatedAt DateTime @updatedAt } model WatchedArtist { id String @id @default(cuid()) mbid String @unique name String autoMonitorFuture Boolean @default(false) showAllTypes Boolean @default(false) monitorFrom DateTime @default(now()) lastPolledAt DateTime? lastDiscoveredAt DateTime? createdAt DateTime @default(now()) releases MonitoredRelease[] } model MonitoredRelease { id String @id @default(cuid()) watchedArtist WatchedArtist? @relation(fields: [watchedArtistId], references: [id], onDelete: Cascade) watchedArtistId String? artistMbid String artistName String rgMbid String @unique album String primaryType String? secondaryTypes String[] firstReleaseDate String? monitored Boolean @default(false) state MonitoredReleaseState @default(wanted) currentQualityClass Int? firstGrabbedAt DateTime? lastSearchedAt DateTime? createdAt DateTime @default(now()) requests Request[] } enum DiscoveryKind { artist album } enum SuggestionStatus { pending followed wanted dismissed } model DiscoverySuggestion { id String @id @default(cuid()) kind DiscoveryKind artistMbid String artistName String rgMbid String? album String? primaryType String? secondaryTypes String[] firstReleaseDate String? score Float seedCount Int sources String[] status SuggestionStatus @default(pending) dedupeKey String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model DiscoverySeedContribution { id String @id @default(cuid()) candidateMbid String candidateName String seedMbid String score Float sources String[] updatedAt DateTime @updatedAt @@unique([candidateMbid, seedMbid]) @@index([candidateMbid]) @@index([seedMbid]) } model ScanWorkItem { id String @id @default(cuid()) scanId String artist String album String path String done Boolean @default(false) @@unique([scanId, path]) @@index([scanId, done, artist, album]) } // Generic read-through cache for external API responses shared by web + worker (keyed on // the LOGICAL operation, not the request URL, so both processes share entries). TTL is // applied at read time from fetchedAt in code (no expiresAt column → tunable, no migration). // Named generically so the Last.fm browse cache can ride the same table. model ApiCache { key String @id json Json fetchedAt DateTime @default(now()) @@index([fetchedAt]) }