From f5bf05bbc0480a7edcd098a8e40c6c16af5d5f68 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 24 Oct 2026 09:33:49 +0000 Subject: Fix the shadow run: resolve audio paths, and survive a bad file The first real shadow run died on its first batch. Two faults, and the second is the one worth keeping. store.Episode.Path is relative to the audio directory. Detection joins it before handing it to the decoder and this did not, so ffmpeg went looking in the working directory and found nothing. Then that single failure aborted the whole show's learning. Every other per-item loop in this server isolates one item's failure -- the refresh sweep, the poll loop, the detection pass all say so in their own comments -- and this one propagated instead. An unreadable episode is now counted and stepped over. Counted once per episode rather than once per failure: a file that cannot be read fails both the matching pass and the harvesting pass, and reporting it twice would overstate the damage. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- server/cmd/listend/main.go | 12 +++++------- server/cmd/listend/shadow.go | 13 ++++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) (limited to 'server/cmd') diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go index a556f5e..a4400d2 100644 --- a/server/cmd/listend/main.go +++ b/server/cmd/listend/main.go @@ -77,7 +77,6 @@ func main() { keep: *keep, mirror: &mirror.Mirror{AudioDir: audioDir, Probe: decoder}, decoder: decoder, - audioDir: audioDir, requests: make(chan request, 64), wakeups: make(chan struct{}, 1), } @@ -154,12 +153,11 @@ type worker struct { // signatures is the library the shadow run learns into. Nothing it holds is published yet. signatures *signature.Library signaturePath string - keep int - mirror *mirror.Mirror - decoder decode.Decoder - audioDir string - requests chan request - wakeups chan struct{} + keep int + mirror *mirror.Mirror + decoder decode.Decoder + requests chan request + wakeups chan struct{} } // wake asks for a poll now. Coalescing, so a burst of subscription updates is one poll. diff --git a/server/cmd/listend/shadow.go b/server/cmd/listend/shadow.go index e9376f1..7531115 100644 --- a/server/cmd/listend/shadow.go +++ b/server/cmd/listend/shadow.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "path/filepath" "sort" "time" @@ -68,7 +69,7 @@ func (w *worker) runShadow(ctx context.Context, show *store.Show, episodes []*st batch.Episodes = append(batch.Episodes, ep) } - report, err := signature.NewShadow(w.signatures, audioFor(w.decoder, episodes)).Run(ctx, batch) + report, err := signature.NewShadow(w.signatures, audioFor(w.decoder, w.mirror.AudioDir, episodes)).Run(ctx, batch) if err != nil { log.Printf("%s: shadow run failed: %v", show.Title, err) return @@ -118,10 +119,16 @@ const shadowMinOccasions = 2 // // The paths come from the batch rather than from a lookup: these are the episodes detection // just finished with, so their files are the ones it read and are certainly still there. -func audioFor(decoder decode.Decoder, episodes []*store.Episode) signature.Audio { +// +// store.Episode.Path is relative to the audio directory, which is what detection joins it with +// before handing it to the decoder. Passing it raw sent ffmpeg looking in the working directory, +// and the first real shadow run died on it. +func audioFor(decoder decode.Decoder, audioDir string, episodes []*store.Episode) signature.Audio { paths := make(map[string]string, len(episodes)) for _, e := range episodes { - paths[e.ID] = e.Path + if e.Path != "" { + paths[e.ID] = filepath.Join(audioDir, e.Path) + } } return episodeAudio{decoder: decoder, pathOf: func(id string) string { return paths[id] }} } -- cgit v1.2.3