package library import ( "context" "errors" "net/http" "github.com/google/uuid" "pipu/api/internal/auth" "pipu/api/internal/platform/apperr" ) type ScanStore interface { GetSource(context.Context, uuid.UUID) (Source, error) RetryFailedAnalysis(context.Context) (int, error) } type ScanStarter interface { Start(Source, string, bool) error } // ScanService owns explicit and scheduled scan starts. type ScanService struct { store ScanStore scanner ScanStarter board *ProgressBoard root func(Source) (string, error) } // StartScan запускает скан источника в фоне. // RetryAnalysis возвращает в очередь треки, которые анализатор не смог обработать (например, пока диск // не был смонтирован). func (s *ScanService) RetryAnalysis(ctx context.Context) (int, error) { if err := requirePerm(ctx, auth.PermManageLibrary); err != nil { return 0, err } return s.store.RetryFailedAnalysis(ctx) } func (s *ScanService) StartScan(ctx context.Context, id uuid.UUID, full bool) (Progress, error) { if err := requirePerm(ctx, auth.PermManageLibrary); err != nil { return Progress{}, err } src, err := s.store.GetSource(ctx, id) if err != nil { return Progress{}, notFound(err) } if err := s.startScan(src, full); err != nil { return Progress{}, err } p, _ := s.board.Get(id) return p, nil } func (s *ScanService) startScan(src Source, full bool) error { if src.Status != "mounted" { return apperr.New(http.StatusConflict, CodeSourceOffline, "source is not connected") } root, err := s.root(src) if err != nil { return err } if err := s.scanner.Start(src, root, full); errors.Is(err, ErrScanRunning) { return apperr.Conflict(CodeScanRunning, "scan is already running") } else if err != nil { return err } return nil }