From af29461e3a21de9b4aafbc741c5b8b0af36d1ced Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 30 Apr 2026 11:38:21 +0300 Subject: task ea: wire filesystem scanner into app and admin rescan endpoint - Add scanner.Scanner and mediaRoot fields to adminService. - Update NewAdminService signature to accept scanner and mediaRoot. - Implement TriggerRescan delegation to injected scanner. - Wire probe.NewFFProber, thumb.NewFFmpegGenerator, and scanner.NewFSScanner in cmd/mediaplayer/main.go. - Add tests for TriggerRescan delegation, error propagation, and nil scanner. --- cmd/mediaplayer/main.go | 10 ++++++- internal/service/admin.go | 26 ++++++++++++------ internal/service/admin_test.go | 60 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/cmd/mediaplayer/main.go b/cmd/mediaplayer/main.go index fcc83e7..cb99869 100644 --- a/cmd/mediaplayer/main.go +++ b/cmd/mediaplayer/main.go @@ -15,8 +15,11 @@ import ( "codeberg.org/snonux/play/internal/api" "codeberg.org/snonux/play/internal/auth" "codeberg.org/snonux/play/internal/clock" + "codeberg.org/snonux/play/internal/probe" "codeberg.org/snonux/play/internal/repository" + "codeberg.org/snonux/play/internal/scanner" "codeberg.org/snonux/play/internal/service" + "codeberg.org/snonux/play/internal/thumb" ) func main() { @@ -48,7 +51,12 @@ func main() { sm := auth.NewSessionManager(store, clk, time.Duration(cfg.SessionTimeoutHours)*time.Hour) mediaSvc := service.NewMediaService(store, clk, cfg.MediaRoot) - adminSvc := service.NewAdminService(store, clk, hasher) + + prober := probe.NewFFProber() + thumbGen := thumb.NewFFmpegGenerator() + fsScanner := scanner.NewFSScanner(store, prober, thumbGen, clk, cfg.MediaRoot) + adminSvc := service.NewAdminService(store, clk, hasher, fsScanner, cfg.MediaRoot) + progressSvc := service.NewProgressService(store, clk) staticFS := http.Dir("web") diff --git a/internal/service/admin.go b/internal/service/admin.go index 853ee68..fe0d508 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -8,21 +8,26 @@ import ( "codeberg.org/snonux/play/internal/clock" "codeberg.org/snonux/play/internal/model" "codeberg.org/snonux/play/internal/repository" + "codeberg.org/snonux/play/internal/scanner" ) // adminService is the concrete implementation of AdminService. type adminService struct { - store repository.AdminServiceStore - clock clock.Clock - hasher auth.Hasher + store repository.AdminServiceStore + clock clock.Clock + hasher auth.Hasher + scanner scanner.Scanner + mediaRoot string } // NewAdminService creates a concrete AdminService. -func NewAdminService(store repository.AdminServiceStore, clk clock.Clock, hasher auth.Hasher) AdminService { +func NewAdminService(store repository.AdminServiceStore, clk clock.Clock, hasher auth.Hasher, sc scanner.Scanner, mediaRoot string) AdminService { return &adminService{ - store: store, - clock: clk, - hasher: hasher, + store: store, + clock: clk, + hasher: hasher, + scanner: sc, + mediaRoot: mediaRoot, } } @@ -31,7 +36,12 @@ func (s *adminService) ListTrash(ctx context.Context) ([]model.Media, error) { } func (s *adminService) TriggerRescan(ctx context.Context) error { - // No-op; scanner will be wired later. + if s.scanner == nil { + return fmt.Errorf("scanner not configured") + } + if err := s.scanner.Scan(ctx, s.mediaRoot); err != nil { + return fmt.Errorf("scan failed: %w", err) + } return nil } diff --git a/internal/service/admin_test.go b/internal/service/admin_test.go index c24c82c..bc6e9c5 100644 --- a/internal/service/admin_test.go +++ b/internal/service/admin_test.go @@ -9,6 +9,17 @@ import ( "codeberg.org/snonux/play/internal/repository" ) +type fakeScanner struct { + scanFunc func(ctx context.Context, root string) error +} + +func (f *fakeScanner) Scan(ctx context.Context, root string) error { + if f.scanFunc != nil { + return f.scanFunc(ctx, root) + } + return nil +} + type fakeHasher struct { fixed string err error @@ -33,7 +44,7 @@ func TestAdminService_ListTrash(t *testing.T) { }, }, } - svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}) + svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") items, err := svc.ListTrash(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -45,10 +56,43 @@ func TestAdminService_ListTrash(t *testing.T) { func TestAdminService_TriggerRescan(t *testing.T) { ctx := context.Background() - svc := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}) + var scannedRoot string + sc := &fakeScanner{ + scanFunc: func(_ context.Context, root string) error { + scannedRoot = root + return nil + }, + } + svc := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}, sc, "/media") if err := svc.TriggerRescan(ctx); err != nil { t.Fatalf("unexpected error: %v", err) } + if scannedRoot != "/media" { + t.Fatalf("expected root %q, got %q", "/media", scannedRoot) + } +} + +func TestAdminService_TriggerRescan_Error(t *testing.T) { + ctx := context.Background() + sc := &fakeScanner{ + scanFunc: func(_ context.Context, _ string) error { + 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") + } +} + +func TestAdminService_TriggerRescan_NilScanner(t *testing.T) { + ctx := context.Background() + svc := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") + err := svc.TriggerRescan(ctx) + if err == nil { + t.Fatal("expected error when scanner is nil") + } } func TestAdminService_ListUsers(t *testing.T) { @@ -60,7 +104,7 @@ func TestAdminService_ListUsers(t *testing.T) { }, }, } - svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}) + svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") users, err := svc.ListUsers(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -104,7 +148,7 @@ func TestAdminService_CreateUser(t *testing.T) { }, } hasher := &fakeHasher{fixed: "hashed", err: tt.hashErr} - svc := NewAdminService(store, newMockClock(), hasher) + svc := NewAdminService(store, newMockClock(), hasher, nil, "") user, err := svc.CreateUser(ctx, "alice", "secret", false) if tt.wantErr { if err == nil { @@ -133,7 +177,7 @@ func TestAdminService_DeleteUser(t *testing.T) { }, }, } - svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}) + svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") if err := svc.DeleteUser(ctx, 1); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -156,7 +200,7 @@ func TestAdminService_ListPermissions(t *testing.T) { }, }, } - svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}) + svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") perms, err := svc.ListPermissions(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -177,7 +221,7 @@ func TestAdminService_GrantPermission(t *testing.T) { }, }, } - svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}) + svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") if err := svc.GrantPermission(ctx, 1, 2, model.RoleViewer); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -200,7 +244,7 @@ func TestAdminService_RevokePermission(t *testing.T) { }, }, } - svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}) + svc := NewAdminService(store, newMockClock(), &fakeHasher{fixed: "hash"}, nil, "") if err := svc.RevokePermission(ctx, 1, 2); err != nil { t.Fatalf("unexpected error: %v", err) } -- cgit v1.2.3