* docs(playback): plan protocol v3 server implementation * docs(playback): incorporate protocol v3 review * feat(playback): implement protocol v3 server * fix(playback): persist empty route diagnostics * feat(playback): harden protocol v3 HDR routing * feat(playback): complete protocol v3 client contract * fix(playback): harden protocol v3 recovery * fix(playback): restore dovi_rpu strip filter for DV remuxes The v3 work renamed the Dolby Vision strip recipe to a dovi_split=mode=bl bitstream filter that does not exist in stock FFmpeg or jellyfin-ffmpeg; the probe failed closed on every deployment, disabling the new validated DV7-to-HDR10 route and regressing the previously working dovi_rpu=strip=1 remux path from main. Restore dovi_rpu across the probe, remux and HLS copy arguments, and the recipe-card constant. Also from review: validate the remux DV mode for every profile (garbage modes on non-P7 sources silently no-opped), reject preserve mode for P7 outright (a base-layer-only remux cannot preserve dual-layer DV), tag dvhe sample entries only for the explicit v3 preserve recipe so legacy web/jellycompat remuxes keep their pre-v3 hev1 labeling, and honor the token-frozen DV mode in the proxy remux path instead of legacy-auto. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): correct v3 planner policy and contract validation Review fixes to the v3 planner and wire contracts: - Bar Profile 7 sources from the non-strip progressive remux route: a base-layer-only remux can never deliver native dual-layer DV, so the planner no longer emits plans claiming validated Dolby Vision while the executed remux drops the enhancement layer. - Accept the device-quirks feature flag from either capability location, matching every other dual-location feature check. - Treat legacy hdr_unknown rows as HDR10 for HDR10-capable clients with a degradation warning instead of leaving them unplayable under v3. - Honor bandwidth_cap_kbps as a hard ceiling in every quality mode and wire the previously dead Metered signal into conservative auto rungs. - Degrade to the validated source-quality route instead of a terminal when only an implicit quality reduction demanded an unsupported transcode; explicit user-selected rungs keep terminal behavior. - Bound inner capability lists and strings; compare attempt keys exactly instead of case-folded; make ParseTrackIDV3 strict about canonical numerics; accept dvdsub/pgssub/dvbsub aliases and stop promising burn-in for unknown subtitle codecs; probe every h264 encoder rather than requiring libx264; normalize the file-level bitrate fallback. - Evaluate subtitle renderability against the engine each candidate route executes on, not always media3_direct. - Pin the with-quirks attempt-key preimage arity in the cross-language fixture so the Kotlin client stays in lockstep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden v3 control-plane reliability Review fixes to the v3 session, store, and handler layer: - Bound concurrent replans with a slot semaphore: each replan pins a pooled connection for its advisory lock while issuing further store queries from the same pool, so an unbounded recovery storm could turn every connection into a lock holder and deadlock the server. - Make CompleteReplan a real compare-and-swap (base-revision predicate, ErrReplanSupersededV3) and map BeginReplan insert races to a replay instead of a raw unique violation. - Fingerprint start requests (request_digest column): an attempt ID reused with different input is now a 409-style conflict rather than a silent replay, and both replay paths check session liveness so dead sessions surface as retryable terminals. - Pre-delete expired attempt rows on SaveAttempt so a retry during the cleanup window cannot wedge on an unreachable conflict. - Align the in-memory store's semantics with Postgres and add DB-backed planstore tests (SILO_TEST_DATABASE_URL), including a regression test inserting every route-event name against the real CHECK constraint. - Session manager: v3 route-set updates own RemuxDVMode outright so a replan onto an SDR source clears a stale strip mode; replacement reservations survive unrelated legacy stream updates; replacement admission excludes the replaced session explicitly instead of decrementing totals it may no longer be part of; the admission CAS loop is bounded and decider errors are logged. - Map transient store failures to 500s instead of terminal 404/403s; authorize route events via identity-only projections after the rate limiter; keep sanitized diagnostics deterministic. - Merge the server-computed durable plan key into replan exclusions so unreproducible client history cannot re-select the failed route. - Remap tracks only when the effective edition changes (a same-file replan no longer switches audio to a lookalike track) and remap ID-only subtitle selections on edition fallback. - Cache the v3/shadow feature flags for five seconds instead of one settings SELECT per playback request; stop remote transports best-effort when the start call times out; carry dvm/tid claims and the transport-scoped job identity through the legacy audio-change re-mint; index playback_route_events(received_at) for the retention delete; run store maintenance for DB-less deployments too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transcode): reap idle node jobs and gate WebVTT conversion - Add an idle reaper to the transcode node: a job untouched by manifest or segment requests for ten minutes is closed and unregistered. After a v3 replan retires a transport ID, a stale in-flight stream token could resurrect the old job via reconstruct and encode to end-of-file for nobody; jobs waiting on readiness count registration as access and are never reaped mid-wait, and reaping keeps the recipe so a still-valid token reconstructs on the next hit. - Reject bitmap subtitle tracks (PGS) on the .vtt conversion path with 415 before headers are written instead of spawning an ffmpeg command that always fails mid-response, and make the extract-format override fall back to source-driven mapping for bitmap codecs. - Drain error bodies on non-202 node responses so the HTTP transport can reuse connections. - Pin the transcode-dir cleanup separator-boundary semantics with a regression test (a session ID sharing another's prefix must not retain foreign directories). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): close v3 planner policy gaps from review - Clamp the final transcode bitrate to bandwidth_cap_kbps: the ladder has no rung below 480p/1500kbps, so lower caps were silently exceeded even though the cap is documented as a hard delivery ceiling. - Treat video-only media as audio-compatible instead of forcing an AAC conversion (or an audio_conversion_unsupported terminal) onto a file with no audio stream. Tracks whose codec failed to probe keep the gate. - Only promise a bitmap subtitle sidecar for embedded PGS with an engine that renders embedded bitmap: external/downloaded bitmap and embedded DVD/DVB published artifact URLs that always failed at fetch. They now fall through to burn-in or its terminal. - Accept client_video_transformations_v1 from either client_features or the nested context when validating client-executor transformations, matching the planner's dual-source reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): probe and execute DV remuxes with one ffmpeg binary The v3 transformation registry probed the configured playback.ffmpeg_path while progressive remux execution resolved the process-global discovery path, so a deployment where only one binary carries dovi_rpu could plan a server_dv7_to_hdr10 route and then fail it at stream time. Resolution now goes through a shared ResolveFFmpegPath (configured path first, discovery fallback — the same rule the transcode pipeline already used), the dovi_rpu probe is cached per binary path, and the stream handler and proxy worker pass their configured path into ServeRemuxWithDVMode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden v3 replan identity and control-plane limits - Seed failure-replan track selections from the durable current plan before overlaying the request: after an alternate-version fallback the normalized request still carries requested-edition track IDs, so a replan omitting unchanged tracks was rejected as a track/file mismatch. - Remap ID-only audio selections across edition changes (parse the ID to an index like the subtitle remap already does) instead of leaving a stale file-bound ID to fail validation. - Release the node planner reservation when a prepared remote transport rolls back after the node accepted the job; repeated failed starts could otherwise pin max-job/bandwidth budgets for the full reservation age. - Size the replan semaphore below the PostgreSQL pool via a store capacity advisor: with max_connections at or below the fixed bound, advisory-lock holders could starve the inner store queries they need to finish. - Contain shadow-planner panics with a recover boundary; it runs on a bare goroutine where an escaped panic kills the process for what is telemetry-only work. Document why the memory store's session lock is deliberately a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transcode): serialize node job teardown against reconstructs - Look up and touch manifest/segment sessions in one critical section so the idle reaper cannot unregister a job between the lookup and its liveness refresh. - Re-validate each reap candidate under the per-session lifecycle lock before closing it: Close removes the output directory, and without the lock it could race a token reconstruct and wipe the segments the fresh ffmpeg is writing. - Take the lifecycle lock in handleStop so a stop racing a RequireReady start's readiness wait blocks until registration and tears the job down, instead of 404ing and orphaning the ffmpeg until the reaper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
553 lines
20 KiB
Go
553 lines
20 KiB
Go
package models
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MediaFolder represents a row in the media_folders table.
|
|
type MediaFolder struct {
|
|
ID int
|
|
Paths []string // from media_folder_paths child table
|
|
Type string // movies, series, mixed
|
|
Name string
|
|
Enabled bool
|
|
MetadataLanguage string // ISO 639-1 code (e.g. "en", "ja")
|
|
AutoTranslateMetadata bool // AI-translate descriptions when providers lack this language
|
|
ChapterThumbnailsEnabled bool
|
|
IntroDetectionEnabled bool
|
|
// TrailerKinds is the allow-list of remote video kinds (ExtraKind values)
|
|
// fetched during metadata refresh for this library. Empty disables remote
|
|
// videos entirely.
|
|
TrailerKinds []string
|
|
PosterPath string // S3 key for library poster image
|
|
LastScannedAt *time.Time // nullable
|
|
ScanWarningCode *string
|
|
ScanWarningMessage *string
|
|
ScanWarningAt *time.Time
|
|
AllowEmptyCleanupOnce bool
|
|
SortOrder int
|
|
}
|
|
|
|
// MediaFile represents a row in the media_files table.
|
|
type MediaFile struct {
|
|
ID int
|
|
ContentID string // Sonyflake ID (nullable until matched)
|
|
EpisodeID string // FK to episodes.content_id (nullable)
|
|
ExtraID string // FK to media_extras.content_id (nullable); set only for local extras files, which keep ContentID/EpisodeID empty
|
|
SeasonNumber int // parsed from filename (nullable)
|
|
EpisodeNumber int // parsed from filename (nullable)
|
|
MediaFolderID int
|
|
CanonicalRootPath string
|
|
ObservedRootPath string
|
|
ContentGroupKey string
|
|
GroupKeyVersion int
|
|
BaseTitle string
|
|
BaseYear int
|
|
BaseType string
|
|
IdentityConfidence string
|
|
IdentityJSON []byte
|
|
FilePath string
|
|
FileSize int64
|
|
FileModifiedAt *time.Time
|
|
FileHash string // OSHash (16-char hex)
|
|
CodecVideo string // h264, hevc, av1
|
|
CodecAudio string // aac, opus, flac
|
|
Resolution string // 1080p, 2160p
|
|
AudioChannels int
|
|
HDR bool
|
|
Container string // mkv, mp4
|
|
Duration int // seconds
|
|
Bitrate int // kbps
|
|
VideoTracks []VideoTrack // JSONB
|
|
AudioTracks []AudioTrack // JSONB
|
|
SubtitleTracks []SubtitleTrack // JSONB
|
|
ExternalSubtitles []ExternalSubtitle // JSONB
|
|
Chapters []MediaChapter // JSONB; nil means not yet probed for chapters
|
|
ChapterThumbnailRetryAfter *time.Time
|
|
ChapterThumbnailFailureCount int
|
|
ChapterThumbnailLastError string
|
|
IntroStart *float64
|
|
IntroEnd *float64
|
|
CreditsStart *float64
|
|
CreditsEnd *float64
|
|
RecapStart *float64
|
|
RecapEnd *float64
|
|
PreviewStart *float64
|
|
PreviewEnd *float64
|
|
MarkersSource *string
|
|
MarkersConfidence *float64
|
|
IntroMarkersSource *string
|
|
IntroMarkersProvider *string
|
|
IntroMarkersConfidence *float64
|
|
IntroMarkersAlgorithm *string
|
|
IntroMarkersDetectedAt *time.Time
|
|
CreditsMarkersSource *string
|
|
CreditsMarkersProvider *string
|
|
CreditsMarkersConfidence *float64
|
|
CreditsMarkersAlgorithm *string
|
|
CreditsMarkersDetectedAt *time.Time
|
|
RecapMarkersSource *string
|
|
RecapMarkersProvider *string
|
|
RecapMarkersConfidence *float64
|
|
RecapMarkersAlgorithm *string
|
|
RecapMarkersDetectedAt *time.Time
|
|
PreviewMarkersSource *string
|
|
PreviewMarkersProvider *string
|
|
PreviewMarkersConfidence *float64
|
|
PreviewMarkersAlgorithm *string
|
|
PreviewMarkersDetectedAt *time.Time
|
|
EditionRaw string
|
|
EditionKey string
|
|
EditionConfidence *float64
|
|
EditionSource string
|
|
PresentationKind string
|
|
PresentationGroupKey string
|
|
PresentationPartIndex int
|
|
PresentationPartTotal int
|
|
MultiEpisodeStart int
|
|
MultiEpisodeEnd int
|
|
ProbeSource string // arrs, local
|
|
ProbeUpdatedAt *time.Time
|
|
MatchAttemptedAt *time.Time
|
|
MissingSince *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// MediaChapter represents a single media chapter derived from embedded file metadata.
|
|
type MediaChapter struct {
|
|
Index int `json:"index"`
|
|
Title string `json:"title"`
|
|
StartSeconds float64 `json:"start_seconds"`
|
|
EndSeconds float64 `json:"end_seconds"`
|
|
Source string `json:"source"`
|
|
ThumbnailPath string `json:"thumbnail_path,omitempty"`
|
|
ThumbnailThumbhash string `json:"thumbnail_thumbhash,omitempty"`
|
|
ThumbnailRetryAfter *time.Time `json:"thumbnail_retry_after,omitempty"`
|
|
ThumbnailFailedAt *time.Time `json:"thumbnail_failed_at,omitempty"`
|
|
ThumbnailLastError string `json:"thumbnail_last_error,omitempty"`
|
|
}
|
|
|
|
// OverlaySummary is the compact media badge payload shared across API surfaces.
|
|
type OverlaySummary struct {
|
|
Resolution string `json:"resolution,omitempty"`
|
|
HDR string `json:"hdr,omitempty"`
|
|
Audio string `json:"audio,omitempty"`
|
|
AudioChannels string `json:"audio_channels,omitempty"` // "Stereo", "5.1", "7.1"
|
|
VideoCodec string `json:"video_codec,omitempty"` // "H.264", "H.265", "AV1"
|
|
Container string `json:"container,omitempty"` // "MKV", "MP4"
|
|
AspectRatio string `json:"aspect_ratio,omitempty"` // "16:9", "2.39:1"
|
|
ReleaseType string `json:"release_type,omitempty"`
|
|
Edition string `json:"edition,omitempty"`
|
|
MultiAudio bool `json:"multi_audio,omitempty"` // ≥2 distinct audio languages
|
|
MultiSub bool `json:"multi_sub,omitempty"` // ≥1 subtitle track (embedded or external)
|
|
}
|
|
|
|
// PrimaryDVProfile returns the Dolby Vision profile of the first video
|
|
// track (0 when none/unprobed).
|
|
func (f *MediaFile) PrimaryDVProfile() int {
|
|
if f == nil || len(f.VideoTracks) == 0 {
|
|
return 0
|
|
}
|
|
return f.VideoTracks[0].DVProfile
|
|
}
|
|
|
|
// NormalizeVideoBitDepth returns an explicit probe value when available and
|
|
// otherwise derives the bit depth from stable ffprobe fields. FFprobe commonly
|
|
// omits bits_per_raw_sample for HEVC even though the pixel format and profile
|
|
// are conclusive (for example yuv420p10le / Main 10).
|
|
func NormalizeVideoBitDepth(explicit int, pixelFormat, profile string) int {
|
|
if explicit > 0 {
|
|
return explicit
|
|
}
|
|
|
|
pixelFormat = strings.ToLower(strings.TrimSpace(pixelFormat))
|
|
for _, candidate := range []struct {
|
|
markers []string
|
|
depth int
|
|
}{
|
|
{markers: []string{"p016", "p16", "gray16", "16le", "16be"}, depth: 16},
|
|
{markers: []string{"p014", "p14", "gray14", "14le", "14be"}, depth: 14},
|
|
{markers: []string{"p012", "p12", "gray12", "12le", "12be"}, depth: 12},
|
|
{markers: []string{"p010", "p10", "gray10", "10le", "10be"}, depth: 10},
|
|
{markers: []string{"p009", "p9", "gray9", "9le", "9be"}, depth: 9},
|
|
} {
|
|
for _, marker := range candidate.markers {
|
|
if strings.Contains(pixelFormat, marker) {
|
|
return candidate.depth
|
|
}
|
|
}
|
|
}
|
|
|
|
profile = strings.ToLower(strings.TrimSpace(profile))
|
|
for _, marker := range []string{"main 12", "main12", "12-bit", "12 bit"} {
|
|
if strings.Contains(profile, marker) {
|
|
return 12
|
|
}
|
|
}
|
|
for _, marker := range []string{"main 10", "main10", "10-bit", "10 bit"} {
|
|
if strings.Contains(profile, marker) {
|
|
return 10
|
|
}
|
|
}
|
|
|
|
switch pixelFormat {
|
|
case "yuv420p", "yuv422p", "yuv444p", "yuvj420p", "yuvj422p", "yuvj444p", "nv12", "nv21", "rgb24", "bgr24":
|
|
return 8
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// VideoTrack represents a probed video stream stored as JSONB.
|
|
type VideoTrack struct {
|
|
Title string `json:"title,omitempty"`
|
|
Codec string `json:"codec,omitempty"`
|
|
DolbyVision string `json:"dolby_vision,omitempty"`
|
|
DVProfile int `json:"dv_profile,omitempty"`
|
|
DVBLCompatID int `json:"dv_bl_compat_id,omitempty"`
|
|
DVELPresent bool `json:"dv_el_present,omitempty"`
|
|
DVEnhancementLayer string `json:"dv_enhancement_layer,omitempty"` // none, mel, fel, unknown
|
|
HDR10Plus bool `json:"hdr10_plus,omitempty"`
|
|
Profile string `json:"profile,omitempty"`
|
|
Level int `json:"level,omitempty"`
|
|
Width int `json:"width,omitempty"`
|
|
Height int `json:"height,omitempty"`
|
|
AspectRatio string `json:"aspect_ratio,omitempty"`
|
|
Interlaced bool `json:"interlaced"`
|
|
FrameRate string `json:"frame_rate,omitempty"`
|
|
Bitrate int `json:"bitrate,omitempty"`
|
|
VideoRange string `json:"video_range,omitempty"`
|
|
VideoRangeType string `json:"video_range_type,omitempty"`
|
|
ColorPrimaries string `json:"color_primaries,omitempty"`
|
|
ColorSpace string `json:"color_space,omitempty"`
|
|
ColorTransfer string `json:"color_transfer,omitempty"`
|
|
BitDepth int `json:"bit_depth,omitempty"`
|
|
PixelFormat string `json:"pixel_format,omitempty"`
|
|
ReferenceFrames int `json:"reference_frames,omitempty"`
|
|
}
|
|
|
|
// AudioTrack represents a probed audio stream stored as JSONB.
|
|
type AudioTrack struct {
|
|
Title string `json:"title,omitempty"`
|
|
EmbeddedTitle string `json:"embedded_title,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
Codec string `json:"codec,omitempty"`
|
|
Profile string `json:"profile,omitempty"`
|
|
Layout string `json:"layout,omitempty"`
|
|
Channels int `json:"channels,omitempty"`
|
|
Bitrate int `json:"bitrate,omitempty"`
|
|
SampleRate int `json:"sample_rate,omitempty"`
|
|
BitDepth int `json:"bit_depth,omitempty"`
|
|
Default bool `json:"default"`
|
|
}
|
|
|
|
// SubtitleTrack represents an embedded subtitle track stored as JSONB.
|
|
type SubtitleTrack struct {
|
|
Index int `json:"index"`
|
|
Language string `json:"language"`
|
|
Codec string `json:"codec"`
|
|
Title string `json:"title,omitempty"`
|
|
EmbeddedTitle string `json:"embedded_title,omitempty"`
|
|
Resolution string `json:"resolution,omitempty"`
|
|
Forced bool `json:"forced"`
|
|
Default bool `json:"default"`
|
|
HearingImpaired bool `json:"hearing_impaired"`
|
|
External bool `json:"external"`
|
|
FileName string `json:"file_name,omitempty"`
|
|
}
|
|
|
|
// ExternalSubtitle represents a sidecar subtitle file stored as JSONB.
|
|
type ExternalSubtitle struct {
|
|
Path string `json:"path"`
|
|
Language string `json:"language"`
|
|
Format string `json:"format"` // srt, vtt, ass, ssa, sub
|
|
Title string `json:"title,omitempty"`
|
|
EmbeddedTitle string `json:"embedded_title,omitempty"`
|
|
Resolution string `json:"resolution,omitempty"`
|
|
Forced bool `json:"forced"`
|
|
Default bool `json:"default"`
|
|
HearingImpaired bool `json:"hearing_impaired"`
|
|
}
|
|
|
|
// PersonKind identifies the role type a person has on a media item.
|
|
type PersonKind int
|
|
|
|
const (
|
|
PersonKindActor PersonKind = 1
|
|
PersonKindDirector PersonKind = 2
|
|
PersonKindWriter PersonKind = 3
|
|
PersonKindProducer PersonKind = 4
|
|
PersonKindGuestStar PersonKind = 5
|
|
PersonKindComposer PersonKind = 6
|
|
PersonKindAuthor PersonKind = 7
|
|
PersonKindNarrator PersonKind = 8
|
|
)
|
|
|
|
// String returns the Jellyfin-compatible type string for this PersonKind.
|
|
func (k PersonKind) String() string {
|
|
switch k {
|
|
case PersonKindActor:
|
|
return "Actor"
|
|
case PersonKindDirector:
|
|
return "Director"
|
|
case PersonKindWriter:
|
|
return "Writer"
|
|
case PersonKindProducer:
|
|
return "Producer"
|
|
case PersonKindGuestStar:
|
|
return "GuestStar"
|
|
case PersonKindComposer:
|
|
return "Composer"
|
|
case PersonKindAuthor:
|
|
return "Author"
|
|
case PersonKindNarrator:
|
|
return "Narrator"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// PersonKindFromJob maps a crew job title to a PersonKind.
|
|
func PersonKindFromJob(job string) PersonKind {
|
|
switch strings.ToLower(strings.TrimSpace(job)) {
|
|
case "director":
|
|
return PersonKindDirector
|
|
case "writer", "screenplay", "story", "novel":
|
|
return PersonKindWriter
|
|
case "composer", "original music composer", "music":
|
|
return PersonKindComposer
|
|
default:
|
|
return PersonKindProducer
|
|
}
|
|
}
|
|
|
|
// Person represents a deduplicated person entity.
|
|
type Person struct {
|
|
ID int64
|
|
Name string
|
|
SortName string
|
|
Bio string
|
|
BirthDate *time.Time
|
|
DeathDate *time.Time
|
|
Birthplace string
|
|
Homepage string
|
|
PhotoPath string
|
|
PhotoSourcePath string
|
|
PhotoThumbhash string
|
|
TmdbID string
|
|
ImdbID string
|
|
TvdbID string
|
|
PlexGUID string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// ItemPerson represents a person's credit on a specific media item.
|
|
type ItemPerson struct {
|
|
Person
|
|
Kind PersonKind
|
|
Character string
|
|
SortOrder int
|
|
}
|
|
|
|
// AudiobookSeriesMembership captures a book's membership in an audiobook
|
|
// series and its optional sequence number within that series.
|
|
type AudiobookSeriesMembership struct {
|
|
Name string
|
|
Index *float64
|
|
}
|
|
|
|
// MediaItem represents a row in the media_items table.
|
|
type MediaItem struct {
|
|
ContentID string // Sonyflake ID (PK)
|
|
Type string // movie, series
|
|
Title string
|
|
SortTitle string
|
|
DefaultMetadataLanguage string
|
|
OriginalTitle string
|
|
Year int
|
|
Genres []string
|
|
ContentRating string // PG-13, TV-MA
|
|
Runtime int // minutes
|
|
Overview string
|
|
Tagline string
|
|
RatingIMDB *float64
|
|
RatingTMDB *float64
|
|
RatingRTCritic *int
|
|
RatingRTAudience *int
|
|
ImdbID string
|
|
TmdbID string
|
|
TvdbID string
|
|
PosterPath string // S3 path
|
|
PosterSourcePath string // provider-origin path kept when caching rewrites PosterPath; feeds outbound embeds
|
|
PosterThumbhash string
|
|
BackdropPath string
|
|
BackdropSourcePath string
|
|
BackdropThumbhash string
|
|
LogoPath string
|
|
LogoSourcePath string
|
|
MetadataS3Path string
|
|
MetadataEtag string
|
|
SeasonCount *int // series only
|
|
MangaChapterCount *int // manga series only: loose manga_chapters rows without a volume token
|
|
MangaVolumeCount *int // manga series only: distinct non-empty volume tokens in manga_chapters
|
|
Studios []string
|
|
Networks []string
|
|
Countries []string
|
|
Keywords []string
|
|
OriginalLanguage string
|
|
ReleaseDate *string // ISO date, nullable
|
|
FirstAirDate *string // ISO date (series only), nullable
|
|
LastAirDate *string // ISO date (series only), nullable
|
|
AirTime *string // Series broadcast time (e.g. "20:00"), nullable
|
|
AirTimezone *string // Series broadcast timezone (IANA name, e.g. "America/New_York"), nullable
|
|
ShowStatus string // Series lifecycle: "returning", "ended", "cancelled", "in_production", "upcoming", or "" if unknown (series; manga uses its own domain, e.g. "Ongoing")
|
|
People []ItemPerson
|
|
AudiobookSeries []AudiobookSeriesMembership
|
|
MatchedAt *time.Time
|
|
EpisodeMetadataIncomplete bool
|
|
EpisodeMetadataLastCheckedAt *time.Time
|
|
LastRefreshed *time.Time `json:"last_refreshed,omitempty"`
|
|
RefreshFailures int `json:"refresh_failures"`
|
|
LockedFields []int `json:"locked_fields"`
|
|
Status string `json:"status"` // pending, matched, unmatched
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
AddedAt *time.Time // populated by browse queries (MIN(mil.first_seen_at))
|
|
}
|
|
|
|
// Season represents a row in the seasons table.
|
|
type Season struct {
|
|
ContentID string
|
|
SeriesID string
|
|
SeasonNumber int
|
|
Title string
|
|
DefaultMetadataLanguage string
|
|
Overview string
|
|
AirDate *time.Time
|
|
PosterPath string
|
|
PosterSourcePath string
|
|
PosterThumbhash string
|
|
MetadataS3Path string
|
|
MetadataEtag string
|
|
MetadataSource string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Episode represents a row in the episodes table.
|
|
type Episode struct {
|
|
ContentID string // episode Sonyflake ID (PK)
|
|
SeriesID string
|
|
SeasonID string // FK to seasons.content_id
|
|
SeasonNumber int
|
|
EpisodeNumber int
|
|
Title string
|
|
DefaultMetadataLanguage string
|
|
Overview string
|
|
AirDate *time.Time
|
|
Runtime int // minutes
|
|
RatingIMDB *float64
|
|
RatingTMDB *float64
|
|
ImdbID string
|
|
TmdbID string
|
|
TvdbID string
|
|
StillPath string
|
|
StillSourcePath string
|
|
StillThumbhash string
|
|
MetadataS3Path string
|
|
MetadataEtag string
|
|
MetadataSource string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// MediaItemRoot represents a row in the media_item_roots table.
|
|
// It records which content_id owns a given canonical root path within a library folder.
|
|
type MediaItemRoot struct {
|
|
MediaFolderID int
|
|
CanonicalRootPath string
|
|
ContentID string
|
|
FirstSeenAt time.Time
|
|
LastSeenAt time.Time
|
|
}
|
|
|
|
// MediaItemGroup represents a row in the media_item_groups table.
|
|
type MediaItemGroup struct {
|
|
MediaFolderID int
|
|
GroupKeyVersion int
|
|
ContentGroupKey string
|
|
ContentID string
|
|
FirstSeenAt time.Time
|
|
LastSeenAt time.Time
|
|
}
|
|
|
|
// MediaItemLibrary represents a row in the media_item_libraries junction table.
|
|
type MediaItemLibrary struct {
|
|
ContentID string
|
|
MediaFolderID int
|
|
FirstSeenAt time.Time
|
|
}
|
|
|
|
// EpisodeLibrary represents a row in the episode_libraries junction table.
|
|
type EpisodeLibrary struct {
|
|
EpisodeID string
|
|
MediaFolderID int
|
|
FirstSeenAt time.Time
|
|
}
|
|
|
|
// Localization field provenance values. Precedence when writing:
|
|
// manual beats provider beats ai — a provider refresh may overwrite an AI
|
|
// translation but never a manual edit, and AI never overwrites either
|
|
// (except provider, when the admin explicitly forces a re-translation).
|
|
const (
|
|
LocalizationSourceProvider = "provider"
|
|
LocalizationSourceAI = "ai"
|
|
LocalizationSourceManual = "manual"
|
|
)
|
|
|
|
type MediaItemLocalization struct {
|
|
ContentID string
|
|
Language string
|
|
Title string
|
|
SortTitle string
|
|
Overview string
|
|
Tagline string
|
|
PosterPath string
|
|
PosterSourcePath string
|
|
PosterThumbhash string
|
|
BackdropPath string
|
|
BackdropSourcePath string
|
|
BackdropThumbhash string
|
|
LogoPath string
|
|
LogoSourcePath string
|
|
OverviewSource string // provider | ai | manual
|
|
TaglineSource string // provider | ai | manual
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type SeasonLocalization struct {
|
|
SeasonContentID string
|
|
Language string
|
|
Title string
|
|
Overview string
|
|
PosterPath string
|
|
PosterSourcePath string
|
|
PosterThumbhash string
|
|
OverviewSource string // provider | ai | manual
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type EpisodeLocalization struct {
|
|
EpisodeContentID string
|
|
Language string
|
|
Title string
|
|
Overview string
|
|
OverviewSource string // provider | ai | manual
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|