mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat: Support relative playlist paths in smartlists
Signed-off-by: David <dvedvick@gmail.com>
This commit is contained in:
parent
c222efacd2
commit
7fd3fc3442
@ -333,9 +333,9 @@ func inList(m map[string]any, negate bool) (sql string, args []any, err error) {
|
|||||||
}
|
}
|
||||||
if negate {
|
if negate {
|
||||||
return "media_file.id NOT IN (" + subQText + ")", subQArgs, nil
|
return "media_file.id NOT IN (" + subQText + ")", subQArgs, nil
|
||||||
} else {
|
|
||||||
return "media_file.id IN (" + subQText + ")", subQArgs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "media_file.id IN (" + subQText + ")", subQArgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractPlaylistField(inputRule any, field string) (values []string) {
|
func extractPlaylistField(inputRule any, field string) (values []string) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -117,6 +118,42 @@ func (pls Playlist) UploadedImagePath() string {
|
|||||||
return UploadedImagePath(consts.EntityPlaylist, pls.UploadedImage)
|
return UploadedImagePath(consts.EntityPlaylist, pls.UploadedImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pls Playlist) NormalizeChildPaths() {
|
||||||
|
if pls.Rules.Expression == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizePlaylistPaths(pls.Rules.Expression, pls.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePlaylistPaths(inputRule any, referencingPlaylistPath string) {
|
||||||
|
switch rule := inputRule.(type) {
|
||||||
|
case criteria.Any:
|
||||||
|
for _, rules := range rule {
|
||||||
|
normalizePlaylistPaths(rules, referencingPlaylistPath)
|
||||||
|
}
|
||||||
|
case criteria.All:
|
||||||
|
for _, rules := range rule {
|
||||||
|
normalizePlaylistPaths(rules, referencingPlaylistPath)
|
||||||
|
}
|
||||||
|
case criteria.InPlaylist:
|
||||||
|
dir := filepath.Dir(referencingPlaylistPath)
|
||||||
|
if path, ok := rule["path"].(string); ok {
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
rule["path"] = filepath.Clean(filepath.Join(dir, path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case criteria.NotInPlaylist:
|
||||||
|
dir := filepath.Dir(referencingPlaylistPath)
|
||||||
|
if path, ok := rule["path"].(string); ok {
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
rule["path"] = filepath.Clean(filepath.Join(dir, path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type Playlists []Playlist
|
type Playlists []Playlist
|
||||||
|
|
||||||
type PlaylistRepository interface {
|
type PlaylistRepository interface {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package model_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/criteria"
|
||||||
"github.com/navidrome/navidrome/tests"
|
"github.com/navidrome/navidrome/tests"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
@ -43,4 +44,83 @@ var _ = Describe("Playlist", func() {
|
|||||||
Expect(pls.ToM3U8()).To(Equal(expected))
|
Expect(pls.ToM3U8()).To(Equal(expected))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("NormalizeChildPaths()", func() {
|
||||||
|
It("normalizes file paths", func() {
|
||||||
|
pls := model.Playlist{Rules: &criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/test/my-test-path.m3u"},
|
||||||
|
criteria.InPlaylist{"path": "../my-test-path.m3u"},
|
||||||
|
criteria.NotInPlaylist{"path": "/not-test/not-my-test-path.m3u"},
|
||||||
|
criteria.Any{
|
||||||
|
criteria.InPlaylist{"path": "../../in-the-test.nsp"},
|
||||||
|
criteria.NotInPlaylist{"path": "./sibling.nsp"},
|
||||||
|
criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/other-root/other.m3u"},
|
||||||
|
criteria.NotInPlaylist{"path": "../../../out-of-containment.nsp"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Path: "/test/nested/my-playlist.nsp"}
|
||||||
|
|
||||||
|
pls.NormalizeChildPaths()
|
||||||
|
Expect(pls.Rules).Should(BeEquivalentTo(&criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/test/my-test-path.m3u"},
|
||||||
|
criteria.InPlaylist{"path": "/test/my-test-path.m3u"},
|
||||||
|
criteria.NotInPlaylist{"path": "/not-test/not-my-test-path.m3u"},
|
||||||
|
criteria.Any{
|
||||||
|
criteria.InPlaylist{"path": "/in-the-test.nsp"},
|
||||||
|
criteria.NotInPlaylist{"path": "/test/nested/sibling.nsp"},
|
||||||
|
criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/other-root/other.m3u"},
|
||||||
|
criteria.NotInPlaylist{"path": "/out-of-containment.nsp"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("normalizes various file paths", func() {
|
||||||
|
// Absolute path
|
||||||
|
pls := model.Playlist{ID: "123"}
|
||||||
|
pls.Rules = &criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/test/my-test-path.m3u"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pls.NormalizeChildPaths()
|
||||||
|
Expect(pls.Rules).NotTo(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles relative paths correctly", func() {
|
||||||
|
pls := model.Playlist{ID: "123", Path: "/test/my-playlist.m3u"}
|
||||||
|
pls.Rules = &criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "../my-test-path.m3u"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pls.NormalizeChildPaths()
|
||||||
|
Expect(pls.Rules).Should(BeEquivalentTo(&criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/my-test-path.m3u"},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("ignores non-path entries", func() {
|
||||||
|
pls := model.Playlist{ID: "123"}
|
||||||
|
pls.Rules = &criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.InPlaylist{"path": "/not-test/not-my-test-path.m3u"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pls.NormalizeChildPaths()
|
||||||
|
Expect(pls.Rules).NotTo(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -230,15 +230,6 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
rules := *pls.Rules
|
rules := *pls.Rules
|
||||||
|
|
||||||
// If the playlist depends on other playlists, recursively refresh them first
|
// If the playlist depends on other playlists, recursively refresh them first
|
||||||
childPlaylistPaths := rules.ChildPlaylistPaths()
|
|
||||||
for _, path := range childPlaylistPaths {
|
|
||||||
childPls, err := r.FindByPath(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(r.ctx, "Error loading child playlist", "id", pls.ID, "childId", path, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
r.refreshSmartPlaylist(childPls)
|
|
||||||
}
|
|
||||||
childPlaylistIds := rules.ChildPlaylistIds()
|
childPlaylistIds := rules.ChildPlaylistIds()
|
||||||
for _, id := range childPlaylistIds {
|
for _, id := range childPlaylistIds {
|
||||||
childPls, err := r.Get(id)
|
childPls, err := r.Get(id)
|
||||||
@ -249,6 +240,18 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
r.refreshSmartPlaylist(childPls)
|
r.refreshSmartPlaylist(childPls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pls.NormalizeChildPaths()
|
||||||
|
childPlaylistPaths := rules.ChildPlaylistPaths()
|
||||||
|
for _, path := range childPlaylistPaths {
|
||||||
|
log.Info(r.ctx, "Loading child playlist", "id", pls.ID, "childId", path, err)
|
||||||
|
childPls, err := r.FindByPath(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(r.ctx, "Error loading child playlist", "id", pls.ID, "childId", path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.refreshSmartPlaylist(childPls)
|
||||||
|
}
|
||||||
|
|
||||||
sq := Select("row_number() over (order by "+rules.OrderBy()+") as id", "'"+pls.ID+"' as playlist_id", "media_file.id as media_file_id").
|
sq := Select("row_number() over (order by "+rules.OrderBy()+") as id", "'"+pls.ID+"' as playlist_id", "media_file.id as media_file_id").
|
||||||
From("media_file").LeftJoin("annotation on ("+
|
From("media_file").LeftJoin("annotation on ("+
|
||||||
"annotation.item_id = media_file.id"+
|
"annotation.item_id = media_file.id"+
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user