Files
Lyra/web/prisma/schema.prisma
T
Jonathan 9c2495825f feat(db): add Job.paused column for per-item press pause
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:54:19 +02:00

245 lines
7.7 KiB
Plaintext

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())
// Force re-acquisition: skip the "already in library" dedupe so an owned album is
// re-downloaded on demand. The import step still keeps the copy only if it's higher
// quality, so a forced upgrade never downgrades. Set by the Library "Replace / upgrade".
force Boolean @default(false)
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)
paused Boolean @default(false)
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?
// MusicBrainz ARTIST MBID when known (captured at import + scan). Own-state ("in library")
// prefers this over the fragile exact artist-name match; null rows fall back to name-match
// (re-scan to populate).
artistMbid 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)
// User marked this release "do not monitor" — the monitor skips it (never enqueued as
// wanted/upgrade) and it drops off the Wanted list, independent of `monitored`, so
// auto-monitor-future can't silently re-grab it. Reversible from the discography page.
ignored 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[]
// Why this album is suggested (kind='album' only): "newest" (the artist's newest un-owned
// release), "popular" (their most-played per Last.fm), or "newest,popular" when they're the
// same release. Null for artist suggestions / rows from before this column existed.
albumReason 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])
}
// Throttle registry for discovery seeds that are NOT WatchedArtist rows — i.e. artists you own
// albums by (LibraryItem.artistMbid) but don't follow. Mirrors WatchedArtist.lastDiscoveredAt so
// each library-derived seed is swept once per interval, not every sweep.
model DiscoverySeed {
mbid String @id
lastDiscoveredAt DateTime?
}
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])
}
// Album folders on disk that the library scan could not resolve to a MusicBrainz release
// (no match, or an unreadable/corrupt file). Surfaced on /library so the user can fix the
// metadata or add them manually instead of them being silently skipped. Keyed by path; a
// row is removed when the album later matches, and stale rows (a prior scan's, or a folder
// since removed) are pruned when a scan completes.
model UnmatchedAlbum {
id String @id @default(cuid())
artist String
album String
path String @unique
reason String
scanId String
scannedAt DateTime @default(now())
}
// 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])
}