mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
fix: enhance playlist path normalization for cross-library support
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
f158190ea4
commit
544f912c10
@ -233,7 +233,9 @@ func normalizePathForComparison(path string) string {
|
|||||||
return strings.ToLower(norm.NFC.String(path))
|
return strings.ToLower(norm.NFC.String(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO This won't work for multiple libraries
|
// normalizePaths converts playlist file paths to library-relative paths.
|
||||||
|
// For relative paths, it resolves them to absolute paths first, then determines which
|
||||||
|
// library they belong to. This allows playlists to reference files across library boundaries.
|
||||||
func (s *playlists) normalizePaths(ctx context.Context, pls *model.Playlist, folder *model.Folder, lines []string) ([]string, error) {
|
func (s *playlists) normalizePaths(ctx context.Context, pls *model.Playlist, folder *model.Folder, lines []string) ([]string, error) {
|
||||||
libRegex, err := s.compileLibraryPaths(ctx)
|
libRegex, err := s.compileLibraryPaths(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -246,15 +248,21 @@ func (s *playlists) normalizePaths(ctx context.Context, pls *model.Playlist, fol
|
|||||||
var filePath string
|
var filePath string
|
||||||
|
|
||||||
if folder != nil && !filepath.IsAbs(line) {
|
if folder != nil && !filepath.IsAbs(line) {
|
||||||
// For relative paths, resolve them to absolute first
|
// Two-step resolution for relative paths:
|
||||||
|
// 1. Resolve relative path to absolute path based on playlist location
|
||||||
|
// Example: playlist at /music/playlists/test.m3u with line "../songs/abc.mp3"
|
||||||
|
// resolves to /music/songs/abc.mp3
|
||||||
filePath = filepath.Join(folder.AbsolutePath(), line)
|
filePath = filepath.Join(folder.AbsolutePath(), line)
|
||||||
filePath = filepath.Clean(filePath)
|
filePath = filepath.Clean(filePath)
|
||||||
// Try to find which library this resolved path belongs to
|
|
||||||
|
// 2. Determine which library this absolute path belongs to using regex matching
|
||||||
|
// This allows playlists to reference files in different libraries
|
||||||
if libPath = libRegex.FindString(filePath); libPath == "" {
|
if libPath = libRegex.FindString(filePath); libPath == "" {
|
||||||
// If not found in regex, fallback to the playlist's library
|
// If regex doesn't match any library, check if it's in the playlist's library
|
||||||
libPath = folder.LibraryPath
|
libPath = folder.LibraryPath
|
||||||
// Verify the file is actually in a library by checking if we can get a relative path
|
// Verify the file is actually in this library (reject paths with ..)
|
||||||
if _, err := filepath.Rel(libPath, filePath); err != nil {
|
rel, err := filepath.Rel(libPath, filePath)
|
||||||
|
if err != nil || strings.HasPrefix(rel, "..") {
|
||||||
log.Warn(ctx, "Path in playlist not found in any library", "path", line, "line", idx)
|
log.Warn(ctx, "Path in playlist not found in any library", "path", line, "line", idx)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,7 +117,7 @@ var _ = Describe("Playlists", func() {
|
|||||||
var tmpDir, plsDir, songsDir string
|
var tmpDir, plsDir, songsDir string
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
// Create temp directory structure once
|
// Create temp directory structure
|
||||||
tmpDir = GinkgoT().TempDir()
|
tmpDir = GinkgoT().TempDir()
|
||||||
plsDir = tmpDir + "/playlists"
|
plsDir = tmpDir + "/playlists"
|
||||||
songsDir = tmpDir + "/songs"
|
songsDir = tmpDir + "/songs"
|
||||||
@ -160,8 +160,66 @@ var _ = Describe("Playlists", func() {
|
|||||||
pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u")
|
pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u")
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(pls.Tracks).To(HaveLen(2))
|
Expect(pls.Tracks).To(HaveLen(2))
|
||||||
|
Expect(pls.Tracks[0].Path).To(Equal("abc.mp3")) // From songsDir library
|
||||||
|
Expect(pls.Tracks[1].Path).To(Equal("def.mp3")) // From plsDir library
|
||||||
|
})
|
||||||
|
|
||||||
|
It("ignores paths that point outside all libraries", func() {
|
||||||
|
// Create a temporary playlist file with path outside libraries
|
||||||
|
plsContent := "#PLAYLIST:Outside Test\n../../outside.mp3\nabc.mp3"
|
||||||
|
plsFile := plsDir + "/test.m3u"
|
||||||
|
Expect(os.WriteFile(plsFile, []byte(plsContent), 0600)).To(Succeed())
|
||||||
|
|
||||||
|
plsFolder := &model.Folder{
|
||||||
|
ID: "2",
|
||||||
|
LibraryID: 2,
|
||||||
|
LibraryPath: plsDir,
|
||||||
|
Path: "",
|
||||||
|
Name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Should only find abc.mp3, not outside.mp3
|
||||||
|
Expect(pls.Tracks).To(HaveLen(1))
|
||||||
Expect(pls.Tracks[0].Path).To(Equal("abc.mp3"))
|
Expect(pls.Tracks[0].Path).To(Equal("abc.mp3"))
|
||||||
Expect(pls.Tracks[1].Path).To(Equal("def.mp3"))
|
})
|
||||||
|
|
||||||
|
It("handles relative paths with multiple '../' components", func() {
|
||||||
|
// Create a nested structure: tmpDir/playlists/subfolder/test.m3u
|
||||||
|
subFolder := plsDir + "/subfolder"
|
||||||
|
Expect(os.Mkdir(subFolder, 0755)).To(Succeed())
|
||||||
|
|
||||||
|
// Create the media file in the subfolder directory
|
||||||
|
// The mock will return it as "def.mp3" relative to plsDir
|
||||||
|
ds.MockedMediaFile = &mockedMediaFileFromListRepo{
|
||||||
|
data: []string{
|
||||||
|
"abc.mp3", // From songsDir library
|
||||||
|
"def.mp3", // From plsDir library root
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// From subfolder, ../../songs/abc.mp3 should resolve to songs library
|
||||||
|
// ../def.mp3 should resolve to plsDir/def.mp3
|
||||||
|
plsContent := "#PLAYLIST:Nested Test\n../../songs/abc.mp3\n../def.mp3"
|
||||||
|
plsFile := subFolder + "/test.m3u"
|
||||||
|
Expect(os.WriteFile(plsFile, []byte(plsContent), 0600)).To(Succeed())
|
||||||
|
|
||||||
|
// The folder: AbsolutePath = LibraryPath + Path + Name
|
||||||
|
// So for /playlists/subfolder: LibraryPath=/playlists, Path="", Name="subfolder"
|
||||||
|
plsFolder := &model.Folder{
|
||||||
|
ID: "2",
|
||||||
|
LibraryID: 2,
|
||||||
|
LibraryPath: plsDir,
|
||||||
|
Path: "", // Empty because subfolder is directly under library root
|
||||||
|
Name: "subfolder", // The folder name
|
||||||
|
}
|
||||||
|
|
||||||
|
pls, err := ps.ImportFile(ctx, plsFolder, "test.m3u")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(pls.Tracks).To(HaveLen(2))
|
||||||
|
Expect(pls.Tracks[0].Path).To(Equal("abc.mp3")) // From songsDir library
|
||||||
|
Expect(pls.Tracks[1].Path).To(Equal("def.mp3")) // From plsDir library root
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user