refactor(scanner): move scanner.ScanTarget to model.ScanTarget

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-11-11 15:33:00 -05:00
parent 63c3a95814
commit 54f19c598a
12 changed files with 47 additions and 44 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/utils/pl"
@ -72,7 +73,7 @@ func runScanner(ctx context.Context) {
pls := core.NewPlaylists(ds)
// Parse targets if provided
var scanTargets []scanner.ScanTarget
var scanTargets []model.ScanTarget
if targets != "" {
var err error
scanTargets, err = parseTargets(targets)
@ -96,7 +97,7 @@ func runScanner(ctx context.Context) {
}
// parseTargets parses the comma-separated targets string into ScanTarget structs
func parseTargets(targetsStr string) ([]scanner.ScanTarget, error) {
func parseTargets(targetsStr string) ([]model.ScanTarget, error) {
targets := strings.Split(targetsStr, ",")
return scanner.ParseTargets(targets)
}

View File

@ -1,7 +1,7 @@
package cmd
import (
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -12,9 +12,9 @@ var _ = Describe("parseTargets", func() {
targets, err := parseTargets("1:Music/Rock,2:Jazz,3:Classical/Beethoven")
Expect(err).ToNot(HaveOccurred())
Expect(targets).To(HaveLen(3))
Expect(targets[0]).To(Equal(scanner.ScanTarget{LibraryID: 1, FolderPath: "Music/Rock"}))
Expect(targets[1]).To(Equal(scanner.ScanTarget{LibraryID: 2, FolderPath: "Jazz"}))
Expect(targets[2]).To(Equal(scanner.ScanTarget{LibraryID: 3, FolderPath: "Classical/Beethoven"}))
Expect(targets[0]).To(Equal(model.ScanTarget{LibraryID: 1, FolderPath: "Music/Rock"}))
Expect(targets[1]).To(Equal(model.ScanTarget{LibraryID: 2, FolderPath: "Jazz"}))
Expect(targets[2]).To(Equal(model.ScanTarget{LibraryID: 3, FolderPath: "Classical/Beethoven"}))
})
It("returns error for empty string", func() {

View File

@ -90,3 +90,14 @@ type FolderRepository interface {
MarkMissing(missing bool, ids ...string) error
GetTouchedWithPlaylists() (FolderCursor, error)
}
// ScanTarget represents a specific folder within a library to be scanned.
// NOTE: This struct is used as a map key, so it should only contain comparable types.
type ScanTarget struct {
LibraryID int
FolderPath string // Relative path within the library, or "" for entire library
}
func (st ScanTarget) String() string {
return fmt.Sprintf("%d:%s", st.LibraryID, st.FolderPath)
}

View File

@ -28,21 +28,10 @@ var (
ErrAlreadyScanning = errors.New("already scanning")
)
// ScanTarget represents a specific folder within a library to be scanned.
// NOTE: This struct is used as a map key, so it should only contain comparable types.
type ScanTarget struct {
LibraryID int
FolderPath string // Relative path within the library, or "" for entire library
}
func (st ScanTarget) String() string {
return fmt.Sprintf("%d:%s", st.LibraryID, st.FolderPath)
}
// 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))
func ParseTargets(libFolders []string) ([]model.ScanTarget, error) {
targets := make([]model.ScanTarget, 0, len(libFolders))
for _, part := range libFolders {
part = strings.TrimSpace(part)
@ -67,7 +56,7 @@ func ParseTargets(libFolders []string) ([]ScanTarget, error) {
return nil, fmt.Errorf("invalid library ID %q", libIDStr)
}
targets = append(targets, ScanTarget{
targets = append(targets, model.ScanTarget{
LibraryID: libID,
FolderPath: folderPath,
})
@ -85,7 +74,7 @@ type Scanner interface {
ScanAll(ctx context.Context, fullScan bool) (warnings []string, err error)
// ScanFolders scans specific library/folder pairs, recursing into subdirectories.
// If targets is nil, it scans all libraries. This is a blocking operation.
ScanFolders(ctx context.Context, fullScan bool, targets []ScanTarget) (warnings []string, err error)
ScanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget) (warnings []string, err error)
Status(context.Context) (*StatusInfo, error)
}
@ -125,7 +114,7 @@ func (s *controller) getScanner() scanner {
// CallScan starts an in-process scan of specific library/folder pairs.
// If targets is empty, it scans all libraries.
// This is meant to be called from the command line (see cmd/scan.go).
func CallScan(ctx context.Context, ds model.DataStore, pls core.Playlists, fullScan bool, targets []ScanTarget) (<-chan *ProgressInfo, error) {
func CallScan(ctx context.Context, ds model.DataStore, pls core.Playlists, fullScan bool, targets []model.ScanTarget) (<-chan *ProgressInfo, error) {
release, err := lockScan(ctx)
if err != nil {
return nil, err
@ -161,7 +150,7 @@ type ProgressInfo struct {
// This allows for swapping between in-process and external scanners.
type scanner interface {
// scanFolders performs the actual scanning of folders. If targets is nil, it scans all libraries.
scanFolders(ctx context.Context, fullScan bool, targets []ScanTarget, progress chan<- *ProgressInfo)
scanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget, progress chan<- *ProgressInfo)
}
type controller struct {
@ -272,7 +261,7 @@ func (s *controller) ScanAll(requestCtx context.Context, fullScan bool) ([]strin
return s.ScanFolders(requestCtx, fullScan, nil)
}
func (s *controller) ScanFolders(requestCtx context.Context, fullScan bool, targets []ScanTarget) ([]string, error) {
func (s *controller) ScanFolders(requestCtx context.Context, fullScan bool, targets []model.ScanTarget) ([]string, error) {
release, err := lockScan(requestCtx)
if err != nil {
return nil, err

View File

@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
)
@ -24,11 +25,11 @@ import (
// process will forward them to the caller.
type scannerExternal struct{}
func (s *scannerExternal) scanFolders(ctx context.Context, fullScan bool, targets []ScanTarget, progress chan<- *ProgressInfo) {
func (s *scannerExternal) scanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget, progress chan<- *ProgressInfo) {
s.scan(ctx, fullScan, targets, progress)
}
func (s *scannerExternal) scan(ctx context.Context, fullScan bool, targets []ScanTarget, progress chan<- *ProgressInfo) {
func (s *scannerExternal) scan(ctx context.Context, fullScan bool, targets []model.ScanTarget, progress chan<- *ProgressInfo) {
exe, err := os.Executable()
if err != nil {
progress <- &ProgressInfo{Error: fmt.Sprintf("failed to get executable path: %s", err)}
@ -46,7 +47,7 @@ func (s *scannerExternal) scan(ctx context.Context, fullScan bool, targets []Sca
// Add targets if provided
if len(targets) > 0 {
targetsStr := strings.Join(slice.Map(targets, func(t ScanTarget) string { return t.String() }), ",")
targetsStr := strings.Join(slice.Map(targets, func(t model.ScanTarget) string { return t.String() }), ",")
args = append(args, "--targets", targetsStr)
log.Debug(ctx, "Spawning external scanner process with targets", "fullScan", fullScan, "path", exe, "targets", targetsStr)
} else {

View File

@ -47,7 +47,7 @@ func (s *scanState) sendError(err error) {
s.sendProgress(&ProgressInfo{Error: err.Error()})
}
func (s *scannerImpl) scanFolders(ctx context.Context, fullScan bool, targets []ScanTarget, progress chan<- *ProgressInfo) {
func (s *scannerImpl) scanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget, progress chan<- *ProgressInfo) {
startTime := time.Now()
state := scanState{

View File

@ -736,7 +736,7 @@ var _ = Describe("Scanner", Ordered, func() {
// (lib is already created with the path "fake:///music")
// Scan only the "rock" and "jazz" folders (including their subdirectories)
targets := []scanner.ScanTarget{
targets := []model.ScanTarget{
{LibraryID: lib.ID, FolderPath: "rock"},
{LibraryID: lib.ID, FolderPath: "jazz"},
}

View File

@ -146,7 +146,7 @@ var _ = Describe("Selective Scan - Deleted Child Folders", Ordered, func() {
// Run selective scan on the parent folder (Artist)
// This simulates what the watcher does when a child folder is deleted
_, err := s.ScanFolders(ctx, false, []scanner.ScanTarget{
_, err := s.ScanFolders(ctx, false, []model.ScanTarget{
{LibraryID: lib.ID, FolderPath: "The Beatles"},
})
Expect(err).ToNot(HaveOccurred())
@ -208,7 +208,7 @@ var _ = Describe("Selective Scan - Deleted Child Folders", Ordered, func() {
})
// Run selective scan on parent
_, err = s.ScanFolders(ctx, false, []scanner.ScanTarget{
_, err = s.ScanFolders(ctx, false, []model.ScanTarget{
{LibraryID: lib.ID, FolderPath: "The Beatles"},
})
Expect(err).ToNot(HaveOccurred())

View File

@ -73,7 +73,7 @@ func (w *watcher) Run(ctx context.Context) error {
// Main scan triggering loop
trigger := time.NewTimer(w.triggerWait)
trigger.Stop()
targets := make(map[ScanTarget]struct{})
targets := make(map[model.ScanTarget]struct{})
for {
select {
case <-trigger.C:
@ -90,13 +90,13 @@ func (w *watcher) Run(ctx context.Context) error {
}
// Convert targets map to slice
targetSlice := make([]ScanTarget, 0, len(targets))
targetSlice := make([]model.ScanTarget, 0, len(targets))
for target := range targets {
targetSlice = append(targetSlice, target)
}
// Clear targets for next batch
targets = make(map[ScanTarget]struct{})
targets = make(map[model.ScanTarget]struct{})
go func() {
_, err := w.scanner.ScanFolders(ctx, false, targetSlice)
@ -121,7 +121,7 @@ func (w *watcher) Run(ctx context.Context) error {
folderPath := notification.FolderPath
// If already scheduled for scan, skip
target := ScanTarget{LibraryID: lib.ID, FolderPath: folderPath}
target := model.ScanTarget{LibraryID: lib.ID, FolderPath: folderPath}
if _, exists := targets[target]; exists {
continue
}

View File

@ -351,7 +351,7 @@ type ScanAllCall struct {
type ScanFoldersCall struct {
FullScan bool
Targets []ScanTarget
Targets []model.ScanTarget
}
func NewMockScanner() *mockScanner {
@ -370,12 +370,12 @@ func (m *mockScanner) ScanAll(_ context.Context, fullScan bool) ([]string, error
return nil, nil
}
func (m *mockScanner) ScanFolders(_ context.Context, fullScan bool, targets []ScanTarget) ([]string, error) {
func (m *mockScanner) ScanFolders(_ context.Context, fullScan bool, targets []model.ScanTarget) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
// Make a copy of targets to avoid race conditions
targetsCopy := make([]ScanTarget, len(targets))
targetsCopy := make([]model.ScanTarget, len(targets))
copy(targetsCopy, targets)
m.scanFoldersCalls = append(m.scanFoldersCalls, ScanFoldersCall{

View File

@ -6,6 +6,7 @@ import (
"time"
"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"
@ -47,7 +48,7 @@ func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) {
fullScan := p.BoolOr("fullScan", false)
// Parse optional path parameters for selective scanning
var targets []scanner.ScanTarget
var targets []model.ScanTarget
if pathParams, err := p.Strings("path"); err == nil && len(pathParams) > 0 {
targets, err = scanner.ParseTargets(pathParams)
if err != nil {

View File

@ -292,7 +292,7 @@ type mockScanner struct {
// ScanFolders tracking
scanFoldersCalled bool
scanFoldersFullScan bool
scanFoldersTargets []scanner.ScanTarget
scanFoldersTargets []model.ScanTarget
scanFoldersError error
scanFoldersWarnings []string
@ -310,14 +310,14 @@ func (m *mockScanner) ScanAll(ctx context.Context, fullScan bool) ([]string, err
return m.scanAllWarnings, m.scanAllError
}
func (m *mockScanner) ScanFolders(ctx context.Context, fullScan bool, targets []scanner.ScanTarget) ([]string, error) {
func (m *mockScanner) ScanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.scanFoldersCalled = true
m.scanFoldersFullScan = fullScan
// Make a copy of targets to avoid race conditions
m.scanFoldersTargets = make([]scanner.ScanTarget, len(targets))
m.scanFoldersTargets = make([]model.ScanTarget, len(targets))
copy(m.scanFoldersTargets, targets)
return m.scanFoldersWarnings, m.scanFoldersError
}
@ -357,11 +357,11 @@ func (m *mockScanner) getScanFoldersFullScan() bool {
return m.scanFoldersFullScan
}
func (m *mockScanner) getScanFoldersTargets() []scanner.ScanTarget {
func (m *mockScanner) getScanFoldersTargets() []model.ScanTarget {
m.mu.Lock()
defer m.mu.Unlock()
// Return a copy to avoid race conditions
targets := make([]scanner.ScanTarget, len(m.scanFoldersTargets))
targets := make([]model.ScanTarget, len(m.scanFoldersTargets))
copy(targets, m.scanFoldersTargets)
return targets
}