diff --git a/core/playlists.go b/core/playlists.go index 164221697..3cd8f3d18 100644 --- a/core/playlists.go +++ b/core/playlists.go @@ -324,26 +324,30 @@ func newPathResolver(ctx context.Context, ds model.DataStore) (*pathResolver, er } // resolvePath determines the absolute path and library path for a playlist entry. +// For absolute paths, it uses them directly. +// For relative paths, it resolves them relative to the playlist's folder location. +// Example: playlist at /music/playlists/test.m3u with line "../songs/abc.mp3" +// +// resolves to /music/songs/abc.mp3 func (r *pathResolver) resolvePath(line string, folder *model.Folder) pathResolution { + var absolutePath string if folder != nil && !filepath.IsAbs(line) { - return r.resolveRelativePath(line, folder) + // Resolve relative path to absolute path based on playlist location + absolutePath = filepath.Clean(filepath.Join(folder.AbsolutePath(), line)) + } else { + // Use absolute path directly after cleaning + absolutePath = filepath.Clean(line) } - return r.resolveAbsolutePath(line) + + return r.findInLibraries(absolutePath) } -// resolveRelativePath handles relative paths by converting them to absolute paths -// and finding their library location. This enables cross-library playlist references. -func (r *pathResolver) resolveRelativePath(line string, folder *model.Folder) pathResolution { - // Step 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 - absolutePath := filepath.Clean(filepath.Join(folder.AbsolutePath(), line)) - - // Step 2: Determine which library this absolute path belongs to +// findInLibraries matches an absolute path against all known libraries and returns +// a pathResolution with the library information. Returns an invalid resolution if +// the path is not found in any library. +func (r *pathResolver) findInLibraries(absolutePath string) pathResolution { libID, libPath := r.matcher.findLibraryForPath(absolutePath) if libID == 0 { - // Path not found in any library - this should not happen as the playlist's - // own library should have been matched above return pathResolution{valid: false} } return pathResolution{ @@ -354,22 +358,6 @@ func (r *pathResolver) resolveRelativePath(line string, folder *model.Folder) pa } } -// resolveAbsolutePath handles absolute paths by matching them against library paths. -func (r *pathResolver) resolveAbsolutePath(line string) pathResolution { - cleanPath := filepath.Clean(line) - libID, libPath := r.matcher.findLibraryForPath(cleanPath) - - if libID == 0 { - return pathResolution{valid: false} - } - return pathResolution{ - absolutePath: cleanPath, - libraryPath: libPath, - libraryID: libID, - valid: true, - } -} - // resolvePaths converts playlist file paths to library-qualified paths (format: "libraryID:relativePath"). // 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. diff --git a/core/playlists_internal_test.go b/core/playlists_internal_test.go index b06a004d2..e5abb225d 100644 --- a/core/playlists_internal_test.go +++ b/core/playlists_internal_test.go @@ -245,93 +245,95 @@ var _ = Describe("pathResolver", func() { }) }) - Describe("resolveAbsolutePath", func() { - It("resolves path within a library", func() { - resolution := resolver.resolveAbsolutePath("/music/track.mp3") + Describe("resolvePath", func() { + Context("With absolute paths", func() { + It("resolves path within a library", func() { + resolution := resolver.resolvePath("/music/track.mp3", nil) - Expect(resolution.valid).To(BeTrue()) - Expect(resolution.libraryID).To(Equal(1)) - Expect(resolution.libraryPath).To(Equal("/music")) - Expect(resolution.absolutePath).To(Equal("/music/track.mp3")) + Expect(resolution.valid).To(BeTrue()) + Expect(resolution.libraryID).To(Equal(1)) + Expect(resolution.libraryPath).To(Equal("/music")) + Expect(resolution.absolutePath).To(Equal("/music/track.mp3")) + }) + + It("resolves path to the longest matching library", func() { + resolution := resolver.resolvePath("/music-classical/track.mp3", nil) + + Expect(resolution.valid).To(BeTrue()) + Expect(resolution.libraryID).To(Equal(2)) + Expect(resolution.libraryPath).To(Equal("/music-classical")) + }) + + It("returns invalid resolution for path outside libraries", func() { + resolution := resolver.resolvePath("/videos/movie.mp4", nil) + + Expect(resolution.valid).To(BeFalse()) + }) + + It("cleans the path before matching", func() { + resolution := resolver.resolvePath("/music//artist/../artist/track.mp3", nil) + + Expect(resolution.valid).To(BeTrue()) + Expect(resolution.absolutePath).To(Equal("/music/artist/track.mp3")) + }) }) - It("resolves path to the longest matching library", func() { - resolution := resolver.resolveAbsolutePath("/music-classical/track.mp3") + Context("With relative paths", func() { + It("resolves relative path within same library", func() { + folder := &model.Folder{ + Path: "playlists", + LibraryPath: "/music", + LibraryID: 1, + } - Expect(resolution.valid).To(BeTrue()) - Expect(resolution.libraryID).To(Equal(2)) - Expect(resolution.libraryPath).To(Equal("/music-classical")) - }) + resolution := resolver.resolvePath("../songs/track.mp3", folder) - It("returns invalid resolution for path outside libraries", func() { - resolution := resolver.resolveAbsolutePath("/videos/movie.mp4") + Expect(resolution.valid).To(BeTrue()) + Expect(resolution.libraryID).To(Equal(1)) + Expect(resolution.absolutePath).To(Equal("/music/songs/track.mp3")) + }) - Expect(resolution.valid).To(BeFalse()) - }) + It("resolves relative path to different library", func() { + folder := &model.Folder{ + Path: "playlists", + LibraryPath: "/music", + LibraryID: 1, + } - It("cleans the path before matching", func() { - resolution := resolver.resolveAbsolutePath("/music//artist/../artist/track.mp3") + // Path goes up and into a different library + resolution := resolver.resolvePath("../../podcasts/episode.mp3", folder) - Expect(resolution.valid).To(BeTrue()) - Expect(resolution.absolutePath).To(Equal("/music/artist/track.mp3")) - }) - }) + Expect(resolution.valid).To(BeTrue()) + Expect(resolution.libraryID).To(Equal(3)) + Expect(resolution.libraryPath).To(Equal("/podcasts")) + }) - Describe("resolveRelativePath", func() { - It("resolves relative path within same library", func() { - folder := &model.Folder{ - Path: "playlists", - LibraryPath: "/music", - LibraryID: 1, - } + It("uses matcher to find correct library for resolved path", func() { + folder := &model.Folder{ + Path: "playlists", + LibraryPath: "/music", + LibraryID: 1, + } - resolution := resolver.resolveRelativePath("../songs/track.mp3", folder) + // This relative path resolves to music-classical library + resolution := resolver.resolvePath("../../music-classical/track.mp3", folder) - Expect(resolution.valid).To(BeTrue()) - Expect(resolution.libraryID).To(Equal(1)) - Expect(resolution.absolutePath).To(Equal("/music/songs/track.mp3")) - }) + Expect(resolution.valid).To(BeTrue()) + Expect(resolution.libraryID).To(Equal(2)) + Expect(resolution.libraryPath).To(Equal("/music-classical")) + }) - It("resolves relative path to different library", func() { - folder := &model.Folder{ - Path: "playlists", - LibraryPath: "/music", - LibraryID: 1, - } + It("returns invalid for relative paths escaping all libraries", func() { + folder := &model.Folder{ + Path: "playlists", + LibraryPath: "/music", + LibraryID: 1, + } - // Path goes up and into a different library - resolution := resolver.resolveRelativePath("../../podcasts/episode.mp3", folder) + resolution := resolver.resolvePath("../../../../etc/passwd", folder) - Expect(resolution.valid).To(BeTrue()) - Expect(resolution.libraryID).To(Equal(3)) - Expect(resolution.libraryPath).To(Equal("/podcasts")) - }) - - It("uses matcher to find correct library for resolved path", func() { - folder := &model.Folder{ - Path: "playlists", - LibraryPath: "/music", - LibraryID: 1, - } - - // This relative path resolves to music-classical library - resolution := resolver.resolveRelativePath("../../music-classical/track.mp3", folder) - - Expect(resolution.valid).To(BeTrue()) - Expect(resolution.libraryID).To(Equal(2)) - Expect(resolution.libraryPath).To(Equal("/music-classical")) - }) - - It("returns invalid for relative paths escaping all libraries", func() { - folder := &model.Folder{ - Path: "playlists", - LibraryPath: "/music", - LibraryID: 1, - } - - resolution := resolver.resolveRelativePath("../../../../etc/passwd", folder) - - Expect(resolution.valid).To(BeFalse()) + Expect(resolution.valid).To(BeFalse()) + }) }) })