Files
silo-server/internal/scanner/types.go
T
Quick 4a4cc57069 fix(scanner): stop hiding media when a library root is offline
A scan that cannot read a library root found no files there, so every
cataloged file under it was marked missing. Catalog reads all filter on
missing_since IS NULL, so marking is equivalent to deletion from a user's
point of view: the title leaves browse, search and next-up, and playback
answers "Source media file is missing" for media that is intact on disk.

The dead-root protection already existed but only guarded the destructive
operations. protectedConfiguredRoots was computed *after* the marking loop
in scanPaths, and applyScopedScan received the protected set but applied it
only to its force-delete branch. So an unreachable root could not lose its
rows, but could still have its entire catalog hidden until the next
successful scan.

On a CephFS deployment whose per-library subvolume mounts flap, this marked
190 present files missing in a single day — 15% of all missing-flagged rows
were files sitting untouched on disk, some flagged more than 20 hours after
their last write.

Hoist the probe above the marking loop and skip files under an unreachable
or suspect-empty root in both the folder and scoped paths. Pass the
unreachable set to the walked-scope call too: a nested child mount can die
under a healthy parent, and its rows are inside the parent's scope.

An offline root tells us nothing about whether its files exist. The only
safe reading is to leave them alone and let the next good scan decide.

Genuine deletions under a reachable root are unaffected and still marked
and swept on the same schedule as before.

Report the count as ScanResult.MissingSkippedProtected and log it, so an
operator can tell "my library shrank" from "my mount dropped".

Known gap: a suspect-empty *nested child* root under a healthy parent is
still marked missing, because the suspect set is not resolved until after
the walk loop. Unreachable roots — the case observed in production — are
covered.
2026-07-25 07:02:29 +00:00

183 lines
5.6 KiB
Go

package scanner
// ScanResult contains the outcome of scanning a media folder.
type ScanResult struct {
New int
Updated int
Unchanged int
Missing int
FilesDeleted int
MembershipsRemoved int
ItemsDeleted int
Errors int
EmptyRootGuarded bool
// MissingSkippedProtected counts files the scan did not find on disk but
// left alone because they sit under an unreachable or suspect-empty root.
// A non-zero value means the library is partly offline, not partly gone.
MissingSkippedProtected int
// UnreachableRoots lists configured library roots that failed the
// reachability probe at scan start. Files under them are left untouched:
// not marked missing, not trashed, and not purged. A root that does not
// answer tells us nothing about whether its files still exist, and
// marking them missing would hide working titles from every catalog read
// until the next successful scan.
UnreachableRoots []string
// SuspectEmptyRoots lists roots that probed reachable but were literally
// empty directories while the library still holds cataloged files under
// them — the signature of a lost mount exposing its bare mountpoint
// directory. They receive the same exemptions as UnreachableRoots until
// the operator confirms cleanup or the files return.
SuspectEmptyRoots []string
RootObservations []RootObservation
}
// FileHints contains the OSHash gathered during scanning.
type FileHints struct {
FileHash string // OSHash from xattr or computed
}
// ProbeData contains media file technical information from ffprobe.
type ProbeData struct {
CodecVideo string
CodecAudio string
Resolution string // 1080p, 2160p, etc.
AudioChannels int
HDR bool
Container string
Duration int // seconds
Bitrate int // kbps
VideoTracks []VideoTrackInfo
AudioTracks []AudioTrackInfo
SubtitleTracks []SubtitleTrackInfo
Chapters []ChapterInfo
FormatTags map[string]string // format-level tags from ffprobe (title, artist, album, date, etc.)
}
// VideoTrackInfo describes a probed video track.
type VideoTrackInfo struct {
Title string
Codec string
DolbyVision string
DVProfile int
DVBLCompatID int
DVELPresent bool
DVEnhancementLayer string
HDR10Plus bool
Profile string
Level int
Width int
Height int
AspectRatio string
Interlaced bool
FrameRate string
Bitrate int
VideoRange string
VideoRangeType string
ColorRange string
ColorPrimaries string
ColorSpace string
ColorTransfer string
BitDepth int
PixelFormat string
ReferenceFrames int
}
// AudioTrackInfo describes a probed audio track.
type AudioTrackInfo struct {
Title string
EmbeddedTitle string
Language string
Codec string
Profile string
Layout string
Channels int
Bitrate int
SampleRate int
BitDepth int
Default bool
}
// SubtitleTrackInfo describes an embedded subtitle track from probing.
type SubtitleTrackInfo struct {
Index int
Language string
Codec string
Title string
EmbeddedTitle string
Resolution string
Forced bool
Default bool
HearingImpaired bool
}
// ExternalSubtitleInfo describes a discovered sidecar subtitle file.
type ExternalSubtitleInfo struct {
Path string
Language string
Format string // srt, vtt, ass, ssa, sub
Title string
Forced bool
}
// ChapterInfo describes an embedded chapter extracted from ffprobe.
type ChapterInfo struct {
Index int
Title string
StartSeconds float64
EndSeconds float64
Source string
}
// IntroCreditsMarkers contains intro/credits timing from S3 markers.
type IntroCreditsMarkers struct {
IntroStart *float64
IntroEnd *float64
CreditsStart *float64
CreditsEnd *float64
}
// MarkerUpdate is the narrow marker-only update payload shared by scanner and analyzers.
type MarkerUpdate struct {
IntroStart *float64
IntroEnd *float64
CreditsStart *float64
CreditsEnd *float64
RecapStart *float64
RecapEnd *float64
PreviewStart *float64
PreviewEnd *float64
MarkersSource string
MarkersProvider *string
MarkersConfidence *float64
MarkersAlgorithm string
// Optional per-segment provenance overrides. When set for a segment,
// UpsertMarkers writes these source/provider/confidence/algorithm values
// for that segment instead of the shared Markers* fields above. Used by
// merged multi-provider results where each segment may come from a
// different provider/source class; left nil for single-source writes
// (scanner/s3/local).
IntroProvenance *SegmentProvenance
CreditsProvenance *SegmentProvenance
RecapProvenance *SegmentProvenance
PreviewProvenance *SegmentProvenance
}
// SegmentProvenance is a per-segment attribution override for MarkerUpdate.
// When Source is empty, the shared MarkerUpdate.MarkersSource applies.
type SegmentProvenance struct {
Source string
Provider *string
Confidence *float64
Algorithm string
}
// HasAnySegment reports whether the update would write at least one segment.
// An update with no segment bounds set is a no-op and skipped by UpsertMarkers.
func (u MarkerUpdate) HasAnySegment() bool {
return u.IntroStart != nil || u.IntroEnd != nil ||
u.CreditsStart != nil || u.CreditsEnd != nil ||
u.RecapStart != nil || u.RecapEnd != nil ||
u.PreviewStart != nil || u.PreviewEnd != nil
}