Files
silo-server/internal/models/media.go
T
7f4f609948 feat(scanner): capture audio stream profile metadata (#337)
* feat(scanner): capture audio stream profile metadata

Store ffprobe audio stream profiles in the existing audio track metadata JSON. This mirrors the video track profile field and is additive for clients that want to display or inspect codec profile details.

* feat(catalogseed): carry audio profile in seed export records

Keep AudioTrackRecord in parity with VideoTrackRecord, which already
mirrors the video profile field, so seed exports do not drop the new
audio profile metadata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Silo Contributor <silo-contributor@example.invalid>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:38:14 -04:00

506 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
// 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
}
// 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"`
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
}