summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 12:49:18 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 12:49:18 +0300
commit8e08f588bd59a2483be721d5664463535ed1d956 (patch)
tree475b012b50ebf1ac55aaa1746ee2f15781a5d503 /internal
parent7e73839aef344012bb11c85d951794039b9933f5 (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.go38
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 (._*)