navidrome/core/artwork/library_fs.go
Deluan ce06599288 fix(artwork): open library-backed artwork through its on-disk root
A library configured with a file:// path stored absRoot as the raw URI, so Abs
produced strings like file:/music/cover.jpg that os.Open/os.Stat reject — folder,
upload and embedded art were treated as dangling on every request, looping forever.
Normalize a file:// path to its parsed OS path (the same root os.DirFS uses);
non-local schemes are left unchanged (out of scope, per the artwork-musicfs TODO).
2026-07-23 14:08:08 -04:00

59 lines
1.6 KiB
Go

package artwork
import (
"context"
"net/url"
"path/filepath"
"strings"
"github.com/navidrome/navidrome/core/storage"
"github.com/navidrome/navidrome/model"
)
// libraryView bundles the MusicFS for a library with its absolute root path,
// so readers can open library-relative paths through FS and compose absolute
// paths (for ffmpeg, which is path-based) via Abs.
type libraryView struct {
FS storage.MusicFS
absRoot string
}
// Abs returns the absolute path for a library-relative path. Returns "" for an
// empty rel so callers (fromFFmpegTag) can treat it as "no path available".
func (v libraryView) Abs(rel string) string {
if rel == "" {
return ""
}
return filepath.Join(v.absRoot, rel)
}
// loadLibraryView resolves the MusicFS and absolute root path in a single
// library lookup.
func loadLibraryView(ctx context.Context, ds model.DataStore, libID int) (libraryView, error) {
lib, err := ds.Library(ctx).Get(libID)
if err != nil {
return libraryView{}, err
}
s, err := storage.For(lib.Path)
if err != nil {
return libraryView{}, err
}
fs, err := s.FS()
if err != nil {
return libraryView{}, err
}
return libraryView{FS: fs, absRoot: localOSRoot(lib.Path)}, nil
}
// localOSRoot maps a library path to its on-disk root so Abs() yields paths os.Open/os.Stat accept:
// a file:// URL becomes its parsed OS path (bare paths already are; non-local schemes stay unchanged).
func localOSRoot(libPath string) string {
if !strings.Contains(libPath, "://") {
return libPath
}
u, err := url.Parse(libPath)
if err != nil || u.Scheme != storage.LocalSchemaID {
return libPath
}
return u.Path
}