Files
silo-server/internal/models/media.go
T
14ffc91dfb [codex] Expand provider image cache queue (#176)
* feat(metadata): expand provider image cache queue

* fix(metadata): harden provider image cache queue

Addresses bug-review feedback from Codex/CodeRabbit on the metadata image
cache pipeline. All findings validated against the code before fixing;
false positives (rows/connection deadlock, PhotoSourcePath merge coupling)
were confirmed non-issues and left unchanged.

- Honor metadata.cache_images for the background processor. The
  cache_metadata_images task was registered whenever S3 was configured,
  so merely enabling object storage downloaded the entire provider-artwork
  catalog even with caching disabled. Add ImageCacheProcessor.SetEnabled,
  gate RunOnce/RunUntilIdle on it, and wire it (with hot reload) from
  cfg.Metadata.CacheImages in main.go.
- Guard terminal job updates with lease ownership. EnqueueBatch can
  repurpose a running row with a new source; MarkSucceeded/MarkFailed
  keyed on id alone let a stale worker finalize the replacement job and
  drop the new artwork. Thread locked_by through and add
  status='running' AND locked_by=$n guards.
- Avoid uploading stale jobs onto the live artwork key. Verify the
  target still references the job's source (CurrentTargetSourcePath)
  before CacheImage, so a job whose source an admin/refresh already
  replaced cannot overwrite the deterministic storage object.
- COALESCE nullable external IDs in EnqueueExistingProviderArtwork. A
  NULL tmdb_id/tvdb_id/imdb_id on any candidate failed the scan and
  aborted the whole cache run; matches the existing item_repo pattern.
- Stop re-downloading the catalog every 30 days. Discovery now skips
  targets whose *_path is already a cached relative path, making the
  cached row the durable dedup marker instead of the prunable job row.
- Decouple catalog sweeps from queue draining. RunOnce no longer runs
  discovery per batch; RunUntilIdle sweeps only when the queue drains and
  throttles full sweeps to every 15m, so idle installs stop full-scanning
  every entity table each minute.
- Requeue claimed-but-unstarted jobs on cancellation. Acquire the
  semaphore before spawning workers and RequeueClaimed any jobs not yet
  started, instead of leaving them locked until the 15m lease expires.
- Skip the backoff sleep after the final upload attempt in
  putObjectWithRetry (saves ~1.5s on permanent failures).
- Add the s3/file/local/upload/generated exclusion to the seasons and
  episodes backfill in migration 20260617184537 for consistency with the
  later migration (the bad backfill was inert downstream, but the
  asymmetry is removed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:07:58 -04:00

491 lines
18 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
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)
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)
}
// 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"`
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"`
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", or "" if unknown (series only)
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
}