diff --git a/server/subsonic/library_scanning.go b/server/subsonic/library_scanning.go index c9dd6496..bac27f82 100644 --- a/server/subsonic/library_scanning.go +++ b/server/subsonic/library_scanning.go @@ -80,7 +80,9 @@ func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) { } } + fastScanCompleted := make(chan struct{}) go func() { + defer close(fastScanCompleted) start := time.Now() var err error @@ -99,5 +101,35 @@ func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) { log.Info(ctx, "On-demand scan complete", "user", loggedUser.UserName, "elapsed", time.Since(start)) }() + // Wait briefly for the scanner to start and update its status, so the response + // reflects the current scan (not stale data from a previous scan). + const ( + pollInterval = 50 * time.Millisecond + pollTimeout = 3 * time.Second + ) + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + timer := time.NewTimer(pollTimeout) + defer timer.Stop() + +loop: + for { + status, err := api.scanner.Status(ctx) + if err == nil && status.Scanning { + break + } + select { + case <-fastScanCompleted: + log.Info(ctx, "Fast scan completed", "user", loggedUser.UserName) + break loop + case <-timer.C: + log.Warn(ctx, "Timed out waiting for scanner to start; response may be stale") + break loop + case <-ctx.Done(): + return nil, newError(responses.ErrorGeneric, "Request cancelled while waiting for scanner to start") + case <-ticker.C: + } + } + return api.GetScanStatus(r) } diff --git a/server/subsonic/library_scanning_test.go b/server/subsonic/library_scanning_test.go index d8eba296..c62c156b 100644 --- a/server/subsonic/library_scanning_test.go +++ b/server/subsonic/library_scanning_test.go @@ -365,6 +365,66 @@ var _ = Describe("LibraryScanning", func() { Expect(targets[0].LibraryID).To(Equal(1)) Expect(targets[0].FolderPath).To(Equal("")) }) + + It("returns correct scanType in response when fullScan=false", func() { + // Setup mock to update status when scan starts (simulating the real scanner) + ms.SetScanStatusFunc(func(fullScan bool, targets []model.ScanTarget) *model.ScannerStatus { + scanType := "quick" + if fullScan { + scanType = "full" + } + return &model.ScannerStatus{ + Scanning: true, + ScanType: scanType, + } + }) + + ctx := request.WithUser(context.Background(), model.User{ + ID: "admin-id", + IsAdmin: true, + }) + + r := httptest.NewRequest("GET", "/rest/startScan", nil) + r = r.WithContext(ctx) + + response, err := api.StartScan(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + Expect(response.ScanStatus).ToNot(BeNil()) + Expect(response.ScanStatus.Scanning).To(BeTrue()) + Expect(response.ScanStatus.ScanType).To(Equal("quick")) + }) + + It("returns correct scanType in response when fullScan=true", func() { + // Setup mock to update status when scan starts (simulating the real scanner) + ms.SetScanStatusFunc(func(fullScan bool, targets []model.ScanTarget) *model.ScannerStatus { + scanType := "quick" + if fullScan { + scanType = "full" + } + return &model.ScannerStatus{ + Scanning: true, + ScanType: scanType, + } + }) + + ctx := request.WithUser(context.Background(), model.User{ + ID: "admin-id", + IsAdmin: true, + }) + + r := httptest.NewRequest("GET", "/rest/startScan?fullScan=true", nil) + r = r.WithContext(ctx) + + response, err := api.StartScan(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(response).ToNot(BeNil()) + Expect(response.ScanStatus).ToNot(BeNil()) + Expect(response.ScanStatus.Scanning).To(BeTrue()) + Expect(response.ScanStatus.ScanType).To(Equal("full")) + }) }) Describe("GetScanStatus", func() { diff --git a/tests/mock_scanner.go b/tests/mock_scanner.go index 52396723..495e8fe5 100644 --- a/tests/mock_scanner.go +++ b/tests/mock_scanner.go @@ -14,6 +14,7 @@ type MockScanner struct { scanFoldersCalls []ScanFoldersCall scanningStatus bool statusResponse *model.ScannerStatus + scanStatusFunc func(fullScan bool, targets []model.ScanTarget) *model.ScannerStatus } type ScanAllCall struct { @@ -38,6 +39,13 @@ func (m *MockScanner) ScanAll(_ context.Context, fullScan bool) ([]string, error m.scanAllCalls = append(m.scanAllCalls, ScanAllCall{FullScan: fullScan}) + // Simulate the scanner updating its status when the scan starts + if m.scanStatusFunc != nil { + m.statusResponse = m.scanStatusFunc(fullScan, nil) + } else { + m.scanningStatus = true + } + return nil, nil } @@ -54,6 +62,13 @@ func (m *MockScanner) ScanFolders(_ context.Context, fullScan bool, targets []mo Targets: targetsCopy, }) + // Simulate the scanner updating its status when the scan starts + if m.scanStatusFunc != nil { + m.statusResponse = m.scanStatusFunc(fullScan, targetsCopy) + } else { + m.scanningStatus = true + } + return nil, nil } @@ -118,3 +133,11 @@ func (m *MockScanner) SetStatusResponse(status *model.ScannerStatus) { defer m.mu.Unlock() m.statusResponse = status } + +// SetScanStatusFunc sets a function that will be called when ScanAll/ScanFolders is invoked, +// simulating the scanner updating its status when the scan starts. +func (m *MockScanner) SetScanStatusFunc(fn func(fullScan bool, targets []model.ScanTarget) *model.ScannerStatus) { + m.mu.Lock() + defer m.mu.Unlock() + m.scanStatusFunc = fn +}