From 8e08f588bd59a2483be721d5664463535ed1d956 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 1 May 2026 12:49:18 +0300 Subject: Add cover image detection for audio files in scanner During scanning, when an audio file is found in a directory that also contains an image file (jpg/png/gif), the scanner now assigns that image as the audio file's thumbnail_path. This is common for audiobooks which have cover.jpg alongside the audio files. Changes: - scanner: two-pass walk to collect sibling images, then pair with audio - CSS: add .row-cover style for audio items with covers - JS: render audio rows with cover images when thumbnail_path is set --- internal/scanner/scanner.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) (limited to 'internal/scanner') diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index cbf68da..8b54df8 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -106,7 +106,22 @@ func (s *FSScanner) scanSet(ctx context.Context, root, setPath string) error { existing[m.RelPath] = true } - // Walk set directory recursively. + // First pass: gather images per directory so we can pair them with audio files. + // Key: parent dir path; Value: relative path to the first image found there. + coverImages := make(map[string]string) + _ = s.fs.WalkDir(setPath, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || !isImageFile(path) { + return nil + } + relPath, _ := filepath.Rel(setPath, path) + dir := filepath.Dir(path) + if _, ok := coverImages[dir]; !ok { + coverImages[dir] = relPath + } + return nil + }) + + // Second pass: walk set directory recursively for media files. walkErr := s.fs.WalkDir(setPath, func(path string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk %q: %w", path, err) @@ -153,6 +168,12 @@ func (s *FSScanner) scanSet(ctx context.Context, root, setPath string) error { fmt.Printf("[scanner] skipping thumbnail for %q: %v\n", path, err) thumbnailPath = "" } + } else if mediaType == model.MediaTypeAudio { + // Use a sibling image as the cover/thumbnail if one exists. + dir := filepath.Dir(path) + if coverRel, ok := coverImages[dir]; ok { + thumbnailPath = filepath.Join(setPath, coverRel) + } } media := &model.Media{ @@ -186,6 +207,21 @@ var mediaExtensions = map[string]struct{}{ ".mp3": {}, ".flac": {}, ".wav": {}, ".aac": {}, ".ogg": {}, ".m4a": {}, ".opus": {}, } +// imageExtensions lists file extensions recognized as cover/artwork images. +var imageExtensions = map[string]struct{}{ + ".jpg": {}, ".jpeg": {}, ".png": {}, ".gif": {}, +} + +func isImageFile(path string) bool { + base := filepath.Base(path) + if strings.HasPrefix(base, ".") { + return false + } + ext := strings.ToLower(filepath.Ext(path)) + _, ok := imageExtensions[ext] + return ok +} + func isMediaFile(path string) bool { base := filepath.Base(path) // Skip macOS resource fork files (._*) -- cgit v1.2.3