navidrome/core/artwork/library_fs.go
Deluan f92507b152 fix(artwork): preserve the drive when normalizing Windows file:// library paths
url.Parse puts the volume of file://C:/Music in Host, not Path, so localOSRoot dropped
it and returned /Music — os.Open/os.Stat then failed and folder/embedded art on Windows
looped as dangling. Rejoin the host volume, matching core/storage/local's newLocalStorage.
2026-07-23 14:08:08 -04:00

65 lines
1.8 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
}
// Windows drive URLs (file://C:/Music) put the volume in Host; rejoin it, matching
// core/storage/local's newLocalStorage so os.Open/os.Stat get a valid path.
if filepath.VolumeName(u.Host) != "" {
return filepath.Join(u.Host, u.Path)
}
return u.Path
}