From f158190ea431176bc2dd54c7a6c85633890f6f9d Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 6 Nov 2025 20:41:54 -0500 Subject: [PATCH] fix: handle cross-library relative paths in playlists Playlists can now reference songs in other libraries using relative paths. Previously, relative paths like '../Songs/abc.mp3' would not resolve correctly when pointing to files in a different library than the playlist file. The fix resolves relative paths to absolute paths first, then checks which library they belong to using the library regex. This allows playlists to reference files across library boundaries while maintaining backward compatibility with existing single-library relative paths. Fixes #4617 --- core/playlists.go | 13 ++++++++++- core/playlists_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/core/playlists.go b/core/playlists.go index f98179f88..ca8a4add7 100644 --- a/core/playlists.go +++ b/core/playlists.go @@ -246,8 +246,19 @@ func (s *playlists) normalizePaths(ctx context.Context, pls *model.Playlist, fol var filePath string if folder != nil && !filepath.IsAbs(line) { - libPath = folder.LibraryPath + // For relative paths, resolve them to absolute first filePath = filepath.Join(folder.AbsolutePath(), line) + filePath = filepath.Clean(filePath) + // Try to find which library this resolved path belongs to + if libPath = libRegex.FindString(filePath); libPath == "" { + // If not found in regex, fallback to 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 { + log.Warn(ctx, "Path in playlist not found in any library", "path", line, "line", idx) + continue + } + } } else { cleanLine := filepath.Clean(line) if libPath = libRegex.FindString(cleanLine); libPath != "" { diff --git a/core/playlists_test.go b/core/playlists_test.go index fb42f9c9f..376166531 100644 --- a/core/playlists_test.go +++ b/core/playlists_test.go @@ -112,6 +112,58 @@ var _ = Describe("Playlists", func() { Expect(err.Error()).To(ContainSubstring("line 19, column 1: invalid character '\\n'")) }) }) + + Describe("Cross-library relative paths", func() { + var tmpDir, plsDir, songsDir string + + BeforeEach(func() { + // Create temp directory structure once + tmpDir = GinkgoT().TempDir() + plsDir = tmpDir + "/playlists" + songsDir = tmpDir + "/songs" + Expect(os.Mkdir(plsDir, 0755)).To(Succeed()) + Expect(os.Mkdir(songsDir, 0755)).To(Succeed()) + + // Setup two different libraries with paths matching our temp structure + mockLibRepo.SetData([]model.Library{ + {ID: 1, Path: songsDir}, + {ID: 2, Path: plsDir}, + }) + + // Create a mock media file repository that returns files for both libraries + // Note: The paths are relative to their respective library roots + ds.MockedMediaFile = &mockedMediaFileFromListRepo{ + data: []string{ + "abc.mp3", // This is songs/abc.mp3 relative to songsDir + "def.mp3", // This is playlists/def.mp3 relative to plsDir + }, + } + ps = NewPlaylists(ds) + }) + + It("handles relative paths that reference files in other libraries", func() { + // Create a temporary playlist file with relative path + plsContent := "#PLAYLIST:Cross Library Test\n../songs/abc.mp3\ndef.mp3" + plsFile := plsDir + "/test.m3u" + Expect(os.WriteFile(plsFile, []byte(plsContent), 0600)).To(Succeed()) + + // Playlist is in the Playlists library folder + // Important: Path should be relative to LibraryPath, and Name is the folder name + plsFolder := &model.Folder{ + ID: "2", + LibraryID: 2, + LibraryPath: plsDir, + Path: "", + 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")) + Expect(pls.Tracks[1].Path).To(Equal("def.mp3")) + }) + }) }) Describe("ImportM3U", func() {