summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 12:54:32 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 12:54:32 +0300
commit862badce297a51b9ab264eace9aa39e38761d5d0 (patch)
tree70d072bbe81e5de9e25969984194f34e33a3d8d6 /internal
parent8e08f588bd59a2483be721d5664463535ed1d956 (diff)
Make rescan asynchronous in background goroutine
TriggerRescan now returns immediately and runs the actual scan in a 30-minute timeout background goroutine. This prevents long scans from tying up or timing out the HTTP request. Update related tests to wait on a channel for the async scan to complete.
Diffstat (limited to 'internal')
-rw-r--r--internal/service/admin.go16
-rw-r--r--internal/service/admin_test.go12
2 files changed, 22 insertions, 6 deletions
diff --git a/internal/service/admin.go b/internal/service/admin.go
index a775bed..673bf37 100644
--- a/internal/service/admin.go
+++ b/internal/service/admin.go
@@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
+ "time"
"codeberg.org/snonux/player/internal/auth"
"codeberg.org/snonux/player/internal/clock"
@@ -39,10 +40,17 @@ func (s *adminService) TriggerRescan(ctx context.Context) error {
if s.scanner == nil {
return fmt.Errorf("scanner not configured")
}
- // Use a background context so the scan isn't canceled when the HTTP request finishes.
- if err := s.scanner.Scan(context.Background(), s.mediaRoot); err != nil {
- return fmt.Errorf("scan failed: %w", err)
- }
+ // Run the scan in a background goroutine so the HTTP request
+ // returns immediately and the scan continues asynchronously.
+ go func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
+ defer cancel()
+ if err := s.scanner.Scan(ctx, s.mediaRoot); err != nil {
+ fmt.Printf("[rescan] scan failed: %v\n", err)
+ } else {
+ fmt.Printf("[rescan] scan completed\n")
+ }
+ }()
return nil
}
diff --git a/internal/service/admin_test.go b/internal/service/admin_test.go
index bef8ca2..ac73ce2 100644
--- a/internal/service/admin_test.go
+++ b/internal/service/admin_test.go
@@ -57,9 +57,11 @@ func TestAdminService_ListTrash(t *testing.T) {
func TestAdminService_TriggerRescan(t *testing.T) {
ctx := context.Background()
var scannedRoot string
+ done := make(chan struct{})
sc := &fakeScanner{
scanFunc: func(_ context.Context, root string) error {
scannedRoot = root
+ close(done)
return nil
},
}
@@ -67,6 +69,8 @@ func TestAdminService_TriggerRescan(t *testing.T) {
if err := svc.TriggerRescan(ctx); err != nil {
t.Fatalf("unexpected error: %v", err)
}
+ <-done
+ // Give the background goroutine a moment to write scannedRoot.
if scannedRoot != "/media" {
t.Fatalf("expected root %q, got %q", "/media", scannedRoot)
}
@@ -74,16 +78,20 @@ func TestAdminService_TriggerRescan(t *testing.T) {
func TestAdminService_TriggerRescan_Error(t *testing.T) {
ctx := context.Background()
+ done := make(chan struct{})
sc := &fakeScanner{
scanFunc: func(_ context.Context, _ string) error {
+ close(done)
return errors.New("scan failed")
},
}
svc := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}, sc, "/media")
err := svc.TriggerRescan(ctx)
- if err == nil {
- t.Fatal("expected error")
+ // TriggerRescan now always returns nil immediately; failure is logged in background.
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
}
+ <-done
}
func TestAdminService_TriggerRescan_NilScanner(t *testing.T) {