From b60043aa1282bbeb616d910e2cc2ba4060c22b03 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 14 Nov 2025 16:45:25 -0500 Subject: [PATCH] refactor(scan): streamline folder entry creation and update handling Signed-off-by: Deluan --- scanner/folder_entry.go | 8 +++----- scanner/folder_entry_test.go | 23 ++++++++++++++++------- scanner/phase_1_folders.go | 12 ++++++++++-- scanner/walk_dir_tree.go | 2 +- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/scanner/folder_entry.go b/scanner/folder_entry.go index fc68cb561..9d8d0c571 100644 --- a/scanner/folder_entry.go +++ b/scanner/folder_entry.go @@ -15,9 +15,7 @@ import ( "github.com/navidrome/navidrome/utils/chrono" ) -func newFolderEntry(job *scanJob, path string) *folderEntry { - id := model.FolderID(job.lib, path) - info := job.popLastUpdate(id) +func newFolderEntry(job *scanJob, id, path string, updTime time.Time, hash string) *folderEntry { f := &folderEntry{ id: id, job: job, @@ -25,8 +23,8 @@ func newFolderEntry(job *scanJob, path string) *folderEntry { audioFiles: make(map[string]fs.DirEntry), imageFiles: make(map[string]fs.DirEntry), albumIDMap: make(map[string]string), - updTime: info.UpdatedAt, - prevHash: info.Hash, + updTime: updTime, + prevHash: hash, } return f } diff --git a/scanner/folder_entry_test.go b/scanner/folder_entry_test.go index c6d1b2ce4..f3b715b8b 100644 --- a/scanner/folder_entry_test.go +++ b/scanner/folder_entry_test.go @@ -40,9 +40,8 @@ var _ = Describe("folder_entry", func() { UpdatedAt: time.Now().Add(-30 * time.Minute), Hash: "previous-hash", } - job.lastUpdates[folderID] = updateInfo - entry := newFolderEntry(job, path) + entry := newFolderEntry(job, folderID, path, updateInfo.UpdatedAt, updateInfo.Hash) Expect(entry.id).To(Equal(folderID)) Expect(entry.job).To(Equal(job)) @@ -55,13 +54,18 @@ var _ = Describe("folder_entry", func() { }) It("creates a new folder entry with zero time when no previous update exists", func() { - entry := newFolderEntry(job, path) + folderID := model.FolderID(lib, path) + emptyInfo := model.FolderUpdateInfo{} + + entry := newFolderEntry(job, folderID, path, emptyInfo.UpdatedAt, emptyInfo.Hash) Expect(entry.updTime).To(BeZero()) Expect(entry.prevHash).To(BeEmpty()) }) + }) - It("removes the lastUpdate from the job after popping", func() { + Describe("createFolderEntry", func() { + It("removes the lastUpdate from the job after creation", func() { folderID := model.FolderID(lib, path) updateInfo := model.FolderUpdateInfo{ UpdatedAt: time.Now().Add(-30 * time.Minute), @@ -69,8 +73,10 @@ var _ = Describe("folder_entry", func() { } job.lastUpdates[folderID] = updateInfo - newFolderEntry(job, path) + entry := job.createFolderEntry(path) + Expect(entry.updTime).To(Equal(updateInfo.UpdatedAt)) + Expect(entry.prevHash).To(Equal(updateInfo.Hash)) Expect(job.lastUpdates).ToNot(HaveKey(folderID)) }) }) @@ -79,7 +85,8 @@ var _ = Describe("folder_entry", func() { var entry *folderEntry BeforeEach(func() { - entry = newFolderEntry(job, path) + folderID := model.FolderID(lib, path) + entry = newFolderEntry(job, folderID, path, time.Time{}, "") }) Describe("hasNoFiles", func() { @@ -458,7 +465,9 @@ var _ = Describe("folder_entry", func() { Describe("integration scenarios", func() { It("handles complete folder lifecycle", func() { // Create new folder entry - entry := newFolderEntry(job, "music/rock/album") + folderPath := "music/rock/album" + folderID := model.FolderID(lib, folderPath) + entry := newFolderEntry(job, folderID, folderPath, time.Time{}, "") // Initially new and has no files Expect(entry.isNew()).To(BeTrue()) diff --git a/scanner/phase_1_folders.go b/scanner/phase_1_folders.go index 4a687b310..46c56ba6f 100644 --- a/scanner/phase_1_folders.go +++ b/scanner/phase_1_folders.go @@ -53,8 +53,8 @@ type scanJob struct { lib model.Library fs storage.MusicFS cw artwork.CacheWarmer - lastUpdates map[string]model.FolderUpdateInfo - targetFolders []string // Specific folders to scan (including all descendants) + lastUpdates map[string]model.FolderUpdateInfo // Holds last update info for all (DB) folders in this library + targetFolders []string // Specific folders to scan (including all descendants) lock sync.Mutex numFolders atomic.Int64 } @@ -85,6 +85,8 @@ func newScanJob(ctx context.Context, ds model.DataStore, cw artwork.CacheWarmer, }, nil } +// popLastUpdate retrieves and removes the last update info for the given folder ID +// This is used to track which folders have been found during the walk_dir_tree func (j *scanJob) popLastUpdate(folderID string) model.FolderUpdateInfo { j.lock.Lock() defer j.lock.Unlock() @@ -94,6 +96,12 @@ func (j *scanJob) popLastUpdate(folderID string) model.FolderUpdateInfo { return lastUpdate } +func (j *scanJob) createFolderEntry(path string) *folderEntry { + id := model.FolderID(j.lib, path) + info := j.popLastUpdate(id) + return newFolderEntry(j, id, path, info.UpdatedAt, info.Hash) +} + // phaseFolders represents the first phase of the scanning process, which is responsible // for scanning all libraries and importing new or updated files. This phase involves // traversing the directory tree of each library, identifying new or modified media files, diff --git a/scanner/walk_dir_tree.go b/scanner/walk_dir_tree.go index 1fbe7477d..a50825b5b 100644 --- a/scanner/walk_dir_tree.go +++ b/scanner/walk_dir_tree.go @@ -100,7 +100,7 @@ func loadDir(ctx context.Context, job *scanJob, dirPath string, checker *IgnoreC } // Now that we know the folder exists, create the entry (which removes it from lastUpdates) - folder = newFolderEntry(job, dirPath) + folder = job.createFolderEntry(dirPath) folder.modTime = dirInfo.ModTime() dir, err := job.fs.Open(dirPath)