From b1545ae9f9afeb4cbc7736edb3df2570d6449304 Mon Sep 17 00:00:00 2001 From: rxwatcher Date: Mon, 20 Jul 2026 00:04:17 +0200 Subject: [PATCH] fix(scan): accept ebook and audio file targets --- internal/scantrigger/scantrigger.go | 12 +++-- internal/scantrigger/scantrigger_test.go | 59 ++++++++++++++++++------ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/internal/scantrigger/scantrigger.go b/internal/scantrigger/scantrigger.go index 4483c56f..d4a4e697 100644 --- a/internal/scantrigger/scantrigger.go +++ b/internal/scantrigger/scantrigger.go @@ -143,7 +143,7 @@ func normalizeTrigger(trigger string) string { // ResolveVanishedPath resolves a change for a path that no longer exists on // disk (a file deleted by an upgrade/replacement, or a removed directory) to a -// reconciling scan target. Paths with a supported video extension map to a +// reconciling scan target. Paths with a supported media extension map to a // subtree scan of their parent directory; other paths map to a subtree scan of // the path itself. The scoped scan marks the vanished files missing so stale // versions stop being offered for playback. @@ -177,7 +177,7 @@ func (r *Resolver) ResolveVanishedPath(ctx context.Context, path, trigger string trigger = normalizeTrigger(trigger) scope := cleanPath - if scanner.SupportsVideoFile(cleanPath) { + if supportsMediaFile(cleanPath) { scope = filepath.Dir(cleanPath) } if filepath.Clean(scope) == filepath.Clean(matchedRoot) { @@ -353,12 +353,18 @@ func ClassifyPath(targetPath, matchedRoot string) (string, error) { if !info.Mode().IsRegular() { return "", &RequestError{Status: http.StatusBadRequest, Code: "bad_request", Message: "Path must be a file or directory"} } - if !scanner.SupportsVideoFile(targetPath) { + if !supportsMediaFile(targetPath) { return "", &RequestError{Status: http.StatusBadRequest, Code: "bad_request", Message: "Unsupported media file extension"} } return ModeFile, nil } +func supportsMediaFile(path string) bool { + return scanner.SupportsVideoFile(path) || + scanner.SupportsAudioFile(path) || + scanner.SupportsEbookFile(path) +} + func PathWithinRoot(targetPath, rootPath string) bool { cleanTarget := filepath.Clean(targetPath) cleanRoot := filepath.Clean(rootPath) diff --git a/internal/scantrigger/scantrigger_test.go b/internal/scantrigger/scantrigger_test.go index 7b5c2b81..cb08ae35 100644 --- a/internal/scantrigger/scantrigger_test.go +++ b/internal/scantrigger/scantrigger_test.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "testing" "github.com/Silo-Server/silo-server/internal/catalog" @@ -109,26 +110,31 @@ func TestResolverRejectsMissingSubtreeAtLibraryRoot(t *testing.T) { } } -func TestResolverResolvesVanishedFileToParentSubtree(t *testing.T) { +func TestResolverResolvesVanishedMediaFileToParentSubtree(t *testing.T) { root := t.TempDir() - movieDir := filepath.Join(root, "Movie (2026)") - if err := os.Mkdir(movieDir, 0o755); err != nil { - t.Fatal(err) - } - vanished := filepath.Join(movieDir, "Movie (2026).mkv") repo := &fakeFolderRepo{folders: []*models.MediaFolder{{ ID: 20, - Name: "Movies", + Name: "Media", Enabled: true, Paths: []string{root}, }}} - target, err := NewResolver(repo).ResolveVanishedPath(context.Background(), vanished, "autoscan") - if err != nil { - t.Fatalf("ResolveVanishedPath returned error: %v", err) - } - if target.Folder == nil || target.Folder.ID != 20 || target.Mode != ModeSubtree || target.Path != movieDir { - t.Fatalf("unexpected target: %#v", target) + for _, name := range []string{"Movie.mkv", "Book.epub", "Audiobook.m4b"} { + t.Run(name, func(t *testing.T) { + mediaDir := filepath.Join(root, strings.TrimSuffix(name, filepath.Ext(name))) + if err := os.Mkdir(mediaDir, 0o755); err != nil { + t.Fatal(err) + } + vanished := filepath.Join(mediaDir, name) + + target, err := NewResolver(repo).ResolveVanishedPath(context.Background(), vanished, "autoscan") + if err != nil { + t.Fatalf("ResolveVanishedPath returned error: %v", err) + } + if target.Folder == nil || target.Folder.ID != 20 || target.Mode != ModeSubtree || target.Path != mediaDir { + t.Fatalf("unexpected target: %#v", target) + } + }) } } @@ -283,6 +289,33 @@ func TestResolverClassifiesVideoFile(t *testing.T) { } } +func TestResolverClassifiesAudioAndEbookFiles(t *testing.T) { + root := t.TempDir() + repo := &fakeFolderRepo{folders: []*models.MediaFolder{{ + ID: 25, + Name: "Media", + Enabled: true, + Paths: []string{root}, + }}} + + for _, name := range []string{"Book.epub", "Book.fb2.zip", "Audiobook.m4b"} { + t.Run(name, func(t *testing.T) { + filePath := filepath.Join(root, name) + if err := os.WriteFile(filePath, []byte("test"), 0o644); err != nil { + t.Fatal(err) + } + + target, err := NewResolver(repo).Resolve(context.Background(), Request{Path: filePath}) + if err != nil { + t.Fatalf("Resolve returned error: %v", err) + } + if target.Mode != ModeFile || target.Path != filepath.Clean(filePath) { + t.Fatalf("unexpected target: %#v", target) + } + }) + } +} + func TestResolverRejectsDisabledLibrary(t *testing.T) { root := t.TempDir() repo := &fakeFolderRepo{folders: []*models.MediaFolder{{