Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,10 @@ func (s *Store) AnalyzeInBackground(trackID uint32, filePath string, onDone func
}
}

if _, err := os.Stat(filePath); err != nil {
log.Printf("lazy-analysis: track %d not analyzed, its file is missing: %s", trackID, filePath)
return
}
log.Printf("lazy-analysis: analyzing track %d (%s)...", trackID, filepath.Base(filePath))
s.SetStatus(fmt.Sprintf("Analyzing: %s", filepath.Base(filePath)))
r, err := AnalyzeTrack(filePath)
Expand Down
67 changes: 65 additions & 2 deletions api/analysis_queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package api

import (
"os"
"path/filepath"
"testing"

"github.com/vynulldev/vynull/analysis"
Expand All @@ -14,9 +16,11 @@ import (
// analysis, and the web UI queues the rows on screen in between; those queued
// jobs used to overwrite the imported grids.
func TestQueuedAnalysisKeepsImportedResult(t *testing.T) {
dir := t.TempDir()
one, two := audioFile(t, dir, "one.flac"), audioFile(t, dir, "two.flac")
lib := library.New()
lib.AddTrack(&library.Track{ID: 1, Title: "Imported", FilePath: "/music/one.flac"})
lib.AddTrack(&library.Track{ID: 2, Title: "New", FilePath: "/music/two.flac"})
lib.AddTrack(&library.Track{ID: 1, Title: "Imported", FilePath: one})
lib.AddTrack(&library.Track{ID: 2, Title: "New", FilePath: two})
store := analysis.NewStore()
analyzed := map[uint32]int{}
store.Importer = func(id uint32, _ string) *analysis.Result {
Expand Down Expand Up @@ -51,3 +55,62 @@ func TestQueuedAnalysisKeepsImportedResult(t *testing.T) {
t.Errorf("track 2 (no analysis): want analyzed once, got %+v after %d run(s)", got, analyzed[2])
}
}

// audioFile creates an (empty) audio file for a queue test; only its
// presence matters, since the tests' Importer stands in for analysis.
func audioFile(t *testing.T, dir, name string) string {
t.Helper()
p := filepath.Join(dir, name)
if err := os.WriteFile(p, nil, 0o644); err != nil {
t.Fatal(err)
}
return p
}

// A track whose file is missing is not queued: the web UI asks for its
// waveform on every render, and each request used to start another ffmpeg
// run that failed. The track is flagged as missing, and queued again once
// its file is back.
func TestMissingFileIsNotQueuedForAnalysis(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "gone.mp3")
lib := library.New()
lib.AddTrack(&library.Track{ID: 1, Title: "Gone", FilePath: path})
srv := &Server{Library: lib, Analysis: analysis.NewStore()}
srv.analyzeOnce.Do(func() { srv.analyzeCh = make(chan analyzeJob, 4) })

for i := 0; i < 3; i++ {
if srv.tryQueueAnalysis(1, "") {
t.Fatalf("request %d queued a track whose file is missing", i+1)
}
}
if len(srv.analyzeCh) != 0 {
t.Errorf("%d job(s) queued, want 0", len(srv.analyzeCh))
}
if !lib.Track(1).FileMissing {
t.Errorf("track not flagged as missing")
}

audioFile(t, dir, "gone.mp3")
if !srv.tryQueueAnalysis(1, "") {
t.Errorf("track not queued once its file is back")
}
}

// On-demand analysis (the track drawer's waveform and grid requests) skips a
// missing file instead of running ffmpeg for it each time.
func TestMissingFileIsNotAnalyzedOnDemand(t *testing.T) {
path := filepath.Join(t.TempDir(), "gone.mp3")
lib := library.New()
lib.AddTrack(&library.Track{ID: 1, Title: "Gone", FilePath: path})
store := analysis.NewStore()
ran := 0
store.Importer = func(uint32, string) *analysis.Result { ran++; return nil }
srv := &Server{Library: lib, Analysis: store}
if r := srv.getOrAnalyze(1); r != nil || ran != 0 {
t.Errorf("getOrAnalyze = %v after %d analysis attempt(s), want nil and none", r, ran)
}
if !lib.Track(1).FileMissing {
t.Errorf("track not flagged as missing")
}
}
29 changes: 29 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2075,11 +2075,35 @@ func (s *Server) tryQueueAnalysis(trackID uint32, filePath string) bool {
s.applyAnalysisToTrack(trackID, r)
return false
}
// A missing file can't be analyzed. Without this check, every waveform
// request for the track (the web UI asks again on each render) queued
// another ffmpeg run that failed the same way.
if s.audioMissing(trackID, filePath) {
s.queuedAnalyses.Delete(trackID)
return false
}
s.Analysis.IncPending()
s.queueAnalysis(trackID, filePath)
return true
}

// audioMissing reports whether a track's audio file is absent, flagging the
// library track as missing when it wasn't already, so the web UI shows it.
// The file is checked each time, so one that reappears is analyzed then.
func (s *Server) audioMissing(trackID uint32, filePath string) bool {
if _, err := os.Stat(filePath); err == nil {
return false
}
if s.Library != nil {
if t := s.Library.Track(trackID); t != nil && !t.FileMissing {
t.FileMissing = true
s.Library.Save()
log.Printf("api: track %d's file is missing, not analyzing: %s", trackID, filePath)
}
}
return true
}

func (s *Server) analysisWorker() {
for job := range s.analyzeCh {
// Jobs are queued for tracks with no analysis yet. One may have
Expand Down Expand Up @@ -2268,6 +2292,11 @@ func (s *Server) getOrAnalyze(trackID uint32) *analysis.Result {
return r // disk cache was valid after SetPath
}

// A missing file can't be analyzed; don't run ffmpeg for it on every
// drawer or waveform request (cached analysis above is still served).
if s.audioMissing(trackID, filePath) {
return nil
}
log.Printf("api: on-demand analysis for track %d: %s", trackID, filePath)
var r *analysis.Result
var err error
Expand Down
Loading