summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 12:40:30 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 12:40:30 +0300
commit27dbaf2b2d5895a091d04aa1c852abb91a619e2d (patch)
tree846ba964ee6f0fba52ccb2b1750d44b21c454efb /internal
parentbadc2543eaa319657ecc97453fbdca58679ecb87 (diff)
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
Diffstat (limited to 'internal')
-rw-r--r--internal/api/middleware.go15
-rw-r--r--internal/scanner/scanner.go14
-rw-r--r--internal/scanner/scanner_test.go13
-rw-r--r--internal/service/admin.go3
4 files changed, 38 insertions, 7 deletions
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