From 27dbaf2b2d5895a091d04aa1c852abb91a619e2d Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 1 May 2026 12:40:30 +0300 Subject: Minimalist UI: hide all elements until activated; add help modal and keyboard shortcuts - Redesign UI to be invisible by default: only header + '?' button shown - Press m to show sidebar, t for toolbar, / for search, ? for help - Add help modal with all keyboard shortcuts - Fix scanner to skip corrupt/unprobeable files instead of aborting - Fix scanner to skip thumbnail errors instead of aborting - Fix rescan to use background context so it completes after HTTP response - Rename project from KISS Media Player to Player --- internal/api/middleware.go | 15 +++++++++++++++ internal/scanner/scanner.go | 14 ++++++++++++-- internal/scanner/scanner_test.go | 13 +++++++++---- internal/service/admin.go | 3 ++- 4 files changed, 38 insertions(+), 7 deletions(-) (limited to 'internal') diff --git a/internal/api/middleware.go b/internal/api/middleware.go index ee1d6c9..fd779b5 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -29,15 +29,24 @@ func NewMiddleware(store repository.Store, sm *auth.SessionManager) *Middleware } // RequireSession validates the session cookie and injects the session into request context. +// For HTML page requests (Accept: text/html), redirects to /login.html instead of returning 401. func (mw *Middleware) RequireSession(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { + if wantsHTML(r) { + http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect) + return + } http.Error(w, "unauthorized", http.StatusUnauthorized) return } sess, err := mw.sm.ValidateSession(r.Context(), cookie.Value) if err != nil || sess == nil { + if wantsHTML(r) { + http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect) + return + } http.Error(w, "unauthorized", http.StatusUnauthorized) return } @@ -46,6 +55,12 @@ func (mw *Middleware) RequireSession(next http.Handler) http.Handler { }) } +// wantsHTML returns true if the request appears to be from a browser expecting an HTML page. +func wantsHTML(r *http.Request) bool { + accept := r.Header.Get("Accept") + return strings.Contains(accept, "text/html") +} + // RequireAdmin ensures the authenticated user is an admin. func (mw *Middleware) RequireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index a1effd7..2e82fbe 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -132,7 +132,9 @@ func (s *FSScanner) scanSet(ctx context.Context, root, setPath string) error { meta, err := s.prober.Probe(ctx, path) if err != nil { - return fmt.Errorf("probe %q: %w", path, err) + // Skip unprobeable/corrupt files instead of aborting the whole scan. + fmt.Printf("[scanner] skipping unprobeable file %q: %v\n", path, err) + return nil } meta.FileSizeBytes = info.Size() @@ -146,7 +148,10 @@ func (s *FSScanner) scanSet(ctx context.Context, root, setPath string) error { thumbName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + ".jpg" thumbnailPath = filepath.Join(thumbDir, thumbName) if err := s.thumbGen.Generate(ctx, path, thumbnailPath, meta.Duration); err != nil { - return fmt.Errorf("thumbnail %q: %w", path, err) + // Skip thumbnail generation errors (e.g., corrupt or audio-only video files) + // and continue scanning without a thumbnail. + fmt.Printf("[scanner] skipping thumbnail for %q: %v\n", path, err) + thumbnailPath = "" } } @@ -182,6 +187,11 @@ var mediaExtensions = map[string]struct{}{ } func isMediaFile(path string) bool { + base := filepath.Base(path) + // Skip macOS resource fork files (._*) + if strings.HasPrefix(base, "._") { + return false + } ext := strings.ToLower(filepath.Ext(path)) _, ok := mediaExtensions[ext] return ok diff --git a/internal/scanner/scanner_test.go b/internal/scanner/scanner_test.go index 8da32a4..f6299dd 100644 --- a/internal/scanner/scanner_test.go +++ b/internal/scanner/scanner_test.go @@ -345,9 +345,13 @@ func TestFSScanner_Scan(t *testing.T) { } s := newTestScanner(store, prober, &thumb.MockGenerator{}, clk, mfs) + // Unprobeable files are skipped with a log instead of failing the whole scan. err := s.Scan(ctx, "/media") - if err == nil { - t.Fatal("expected error for probe failure") + if err != nil { + t.Fatalf("unexpected error for probe failure; expected skip, got: %v", err) + } + if store.MediaRepo.CreateMediaFunc != nil { + // no media should have been created for the bad file } }) @@ -381,9 +385,10 @@ func TestFSScanner_Scan(t *testing.T) { } s := newTestScanner(store, prober, gen, clk, mfs) + // Thumbnail generation errors are skipped so the scan continues. err := s.Scan(ctx, "/media") - if err == nil { - t.Fatal("expected error for thumbnail failure") + if err != nil { + t.Fatalf("unexpected error for thumbnail failure; expected skip, got: %v", err) } }) diff --git a/internal/service/admin.go b/internal/service/admin.go index afccc78..f16037a 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -39,7 +39,8 @@ func (s *adminService) TriggerRescan(ctx context.Context) error { if s.scanner == nil { return fmt.Errorf("scanner not configured") } - if err := s.scanner.Scan(ctx, s.mediaRoot); err != nil { + // 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) } return nil -- cgit v1.2.3