mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(scanner): move scanner.ScanTarget to model.ScanTarget
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
63c3a95814
commit
54f19c598a
@ -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)
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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"},
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user