diff --git a/cmd/scan.go b/cmd/scan.go index a2b510d56..41d281070 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -76,7 +76,7 @@ func runScanner(ctx context.Context) { var scanTargets []model.ScanTarget if targets != "" { var err error - scanTargets, err = scanner.ParseTargets(strings.Split(targets, ",")) + scanTargets, err = model.ParseTargets(strings.Split(targets, ",")) if err != nil { log.Fatal(ctx, "Failed to parse targets", err) } diff --git a/model/scanner.go b/model/scanner.go index 12a386ea0..389c77f87 100644 --- a/model/scanner.go +++ b/model/scanner.go @@ -3,6 +3,8 @@ package model import ( "context" "fmt" + "strconv" + "strings" "time" ) @@ -36,3 +38,44 @@ type Scanner interface { ScanFolders(ctx context.Context, fullScan bool, targets []ScanTarget) (warnings []string, err error) Status(context.Context) (*ScannerStatus, error) } + +// ParseTargets parses scan targets strings into ScanTarget structs. +// Example: []string{"1:Music/Rock", "2:Classical"} +func ParseTargets(libFolders []string) ([]ScanTarget, error) { + targets := make([]ScanTarget, 0, len(libFolders)) + + for _, part := range libFolders { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + // Split by the first colon + colonIdx := strings.Index(part, ":") + if colonIdx == -1 { + return nil, fmt.Errorf("invalid target format: %q (expected libraryID:folderPath)", part) + } + + libIDStr := part[:colonIdx] + folderPath := part[colonIdx+1:] + + libID, err := strconv.Atoi(libIDStr) + if err != nil { + return nil, fmt.Errorf("invalid library ID %q: %w", libIDStr, err) + } + if libID <= 0 { + return nil, fmt.Errorf("invalid library ID %q", libIDStr) + } + + targets = append(targets, ScanTarget{ + LibraryID: libID, + FolderPath: folderPath, + }) + } + + if len(targets) == 0 { + return nil, fmt.Errorf("no valid targets found") + } + + return targets, nil +} diff --git a/scanner/controller.go b/scanner/controller.go index 8e308f51d..b42246a50 100644 --- a/scanner/controller.go +++ b/scanner/controller.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" - "strings" "sync/atomic" "time" @@ -28,47 +26,6 @@ var ( ErrAlreadyScanning = errors.New("already scanning") ) -// ParseTargets parses scan targets strings into ScanTarget structs. -// Example: []string{"1:Music/Rock", "2:Classical"} -func ParseTargets(libFolders []string) ([]model.ScanTarget, error) { - targets := make([]model.ScanTarget, 0, len(libFolders)) - - for _, part := range libFolders { - part = strings.TrimSpace(part) - if part == "" { - continue - } - - // Split by the first colon - colonIdx := strings.Index(part, ":") - if colonIdx == -1 { - return nil, fmt.Errorf("invalid target format: %q (expected libraryID:folderPath)", part) - } - - libIDStr := part[:colonIdx] - folderPath := part[colonIdx+1:] - - libID, err := strconv.Atoi(libIDStr) - if err != nil { - return nil, fmt.Errorf("invalid library ID %q: %w", libIDStr, err) - } - if libID <= 0 { - return nil, fmt.Errorf("invalid library ID %q", libIDStr) - } - - targets = append(targets, model.ScanTarget{ - LibraryID: libID, - FolderPath: folderPath, - }) - } - - if len(targets) == 0 { - return nil, fmt.Errorf("no valid targets found") - } - - return targets, nil -} - func New(rootCtx context.Context, ds model.DataStore, cw artwork.CacheWarmer, broker events.Broker, pls core.Playlists, m metrics.Metrics) model.Scanner { c := &controller{ diff --git a/scanner/controller_test.go b/scanner/controller_test.go index 929fa09ba..4bcb681ea 100644 --- a/scanner/controller_test.go +++ b/scanner/controller_test.go @@ -57,7 +57,7 @@ var _ = Describe("Controller", func() { var _ = Describe("ParseTargets", func() { It("parses multiple entries in slice", func() { - targets, err := scanner.ParseTargets([]string{"1:Music/Rock", "1:Music/Jazz", "2:Classical"}) + targets, err := model.ParseTargets([]string{"1:Music/Rock", "1:Music/Jazz", "2:Classical"}) Expect(err).ToNot(HaveOccurred()) Expect(targets).To(HaveLen(3)) Expect(targets[0].LibraryID).To(Equal(1)) @@ -69,7 +69,7 @@ var _ = Describe("ParseTargets", func() { }) It("handles empty folder paths", func() { - targets, err := scanner.ParseTargets([]string{"1:", "2:"}) + targets, err := model.ParseTargets([]string{"1:", "2:"}) Expect(err).ToNot(HaveOccurred()) Expect(targets).To(HaveLen(2)) Expect(targets[0].FolderPath).To(Equal("")) @@ -77,7 +77,7 @@ var _ = Describe("ParseTargets", func() { }) It("trims whitespace from entries", func() { - targets, err := scanner.ParseTargets([]string{" 1:Music/Rock", " 2:Classical "}) + targets, err := model.ParseTargets([]string{" 1:Music/Rock", " 2:Classical "}) Expect(err).ToNot(HaveOccurred()) Expect(targets).To(HaveLen(2)) Expect(targets[0].LibraryID).To(Equal(1)) @@ -87,13 +87,13 @@ var _ = Describe("ParseTargets", func() { }) It("skips empty strings", func() { - targets, err := scanner.ParseTargets([]string{"1:Music/Rock", "", "2:Classical"}) + targets, err := model.ParseTargets([]string{"1:Music/Rock", "", "2:Classical"}) Expect(err).ToNot(HaveOccurred()) Expect(targets).To(HaveLen(2)) }) It("handles paths with colons", func() { - targets, err := scanner.ParseTargets([]string{"1:C:/Music/Rock", "2:/path:with:colons"}) + targets, err := model.ParseTargets([]string{"1:C:/Music/Rock", "2:/path:with:colons"}) Expect(err).ToNot(HaveOccurred()) Expect(targets).To(HaveLen(2)) Expect(targets[0].FolderPath).To(Equal("C:/Music/Rock")) @@ -101,37 +101,37 @@ var _ = Describe("ParseTargets", func() { }) It("returns error for invalid format without colon", func() { - _, err := scanner.ParseTargets([]string{"1Music/Rock"}) + _, err := model.ParseTargets([]string{"1Music/Rock"}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("invalid target format")) }) It("returns error for non-numeric library ID", func() { - _, err := scanner.ParseTargets([]string{"abc:Music/Rock"}) + _, err := model.ParseTargets([]string{"abc:Music/Rock"}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("invalid library ID")) }) It("returns error for negative library ID", func() { - _, err := scanner.ParseTargets([]string{"-1:Music/Rock"}) + _, err := model.ParseTargets([]string{"-1:Music/Rock"}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("invalid library ID")) }) It("returns error for zero library ID", func() { - _, err := scanner.ParseTargets([]string{"0:Music/Rock"}) + _, err := model.ParseTargets([]string{"0:Music/Rock"}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("invalid library ID")) }) It("returns error for empty input", func() { - _, err := scanner.ParseTargets([]string{}) + _, err := model.ParseTargets([]string{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no valid targets found")) }) It("returns error for all empty strings", func() { - _, err := scanner.ParseTargets([]string{"", " ", ""}) + _, err := model.ParseTargets([]string{"", " ", ""}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no valid targets found")) }) diff --git a/server/subsonic/library_scanning.go b/server/subsonic/library_scanning.go index fe7de8fdd..c9dd64968 100644 --- a/server/subsonic/library_scanning.go +++ b/server/subsonic/library_scanning.go @@ -9,7 +9,6 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" - "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils/req" ) @@ -51,7 +50,7 @@ func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) { // Parse optional target parameters for selective scanning var targets []model.ScanTarget if targetParams, err := p.Strings("target"); err == nil && len(targetParams) > 0 { - targets, err = scanner.ParseTargets(targetParams) + targets, err = model.ParseTargets(targetParams) if err != nil { return nil, newError(responses.ErrorGeneric, fmt.Sprintf("Invalid target parameter: %v", err)) }