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))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
libRegex, err := s.compileLibraryPaths(ctx)
|
||||
if err != nil {
|
||||
@ -246,15 +248,21 @@ func (s *playlists) normalizePaths(ctx context.Context, pls *model.Playlist, fol
|
||||
var filePath string
|
||||
|
||||
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.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 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
|
||||
// Verify the file is actually in a library by checking if we can get a relative path
|
||||
if _, err := filepath.Rel(libPath, filePath); err != nil {
|
||||
// Verify the file is actually in this library (reject paths with ..)
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ var _ = Describe("Playlists", func() {
|
||||
var tmpDir, plsDir, songsDir string
|
||||
|
||||
BeforeEach(func() {
|
||||
// Create temp directory structure once
|
||||
// Create temp directory structure
|
||||
tmpDir = GinkgoT().TempDir()
|
||||
plsDir = tmpDir + "/playlists"
|
||||
songsDir = tmpDir + "/songs"
|
||||
@ -160,8 +160,66 @@ var _ = Describe("Playlists", func() {
|
||||
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
|
||||
})
|
||||
|
||||
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[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