diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-01 12:49:18 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-01 12:49:18 +0300 |
| commit | 8e08f588bd59a2483be721d5664463535ed1d956 (patch) | |
| tree | 475b012b50ebf1ac55aaa1746ee2f15781a5d503 /internal | |
| parent | 7e73839aef344012bb11c85d951794039b9933f5 (diff) | |
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
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/scanner/scanner.go | 38 |
1 files changed, 37 insertions, 1 deletions
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 (._*) |
