mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat(playlists): add Evaluate method to PlaylistRepository
Refactor refreshSmartPlaylist into guard checks + core evaluation logic (doRefreshSmartPlaylist). Add public Evaluate method that bypasses ownership/delay guards for system-level evaluation (e.g., background processing after scanner import). Part of #4539
This commit is contained in:
parent
356b0716b6
commit
759ab26b19
@ -131,6 +131,7 @@ type PlaylistRepository interface {
|
|||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
|
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
|
||||||
GetPlaylists(mediaFileId string) (Playlists, error)
|
GetPlaylists(mediaFileId string) (Playlists, error)
|
||||||
|
Evaluate(id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaylistTrack struct {
|
type PlaylistTrack struct {
|
||||||
|
|||||||
@ -215,6 +215,10 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return r.doRefreshSmartPlaylist(pls, usr.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *playlistRepository) doRefreshSmartPlaylist(pls *model.Playlist, userID string) bool {
|
||||||
log.Debug(r.ctx, "Refreshing smart playlist", "playlist", pls.Name, "id", pls.ID)
|
log.Debug(r.ctx, "Refreshing smart playlist", "playlist", pls.Name, "id", pls.ID)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
@ -244,11 +248,11 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
From("media_file").LeftJoin("annotation on ("+
|
From("media_file").LeftJoin("annotation on ("+
|
||||||
"annotation.item_id = media_file.id"+
|
"annotation.item_id = media_file.id"+
|
||||||
" AND annotation.item_type = 'media_file'"+
|
" AND annotation.item_type = 'media_file'"+
|
||||||
" AND annotation.user_id = ?)", usr.ID)
|
" AND annotation.user_id = ?)", userID)
|
||||||
|
|
||||||
// Conditionally join album/artist annotation tables only when referenced by criteria or sort
|
// Conditionally join album/artist annotation tables only when referenced by criteria or sort
|
||||||
requiredJoins := rules.RequiredJoins()
|
requiredJoins := rules.RequiredJoins()
|
||||||
sq = r.addSmartPlaylistAnnotationJoins(sq, requiredJoins, usr.ID)
|
sq = r.addSmartPlaylistAnnotationJoins(sq, requiredJoins, userID)
|
||||||
|
|
||||||
// Only include media files from libraries the user has access to
|
// Only include media files from libraries the user has access to
|
||||||
sq = r.applyLibraryFilter(sq, "media_file")
|
sq = r.applyLibraryFilter(sq, "media_file")
|
||||||
@ -261,8 +265,8 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
LeftJoin("annotation on ("+
|
LeftJoin("annotation on ("+
|
||||||
"annotation.item_id = media_file.id"+
|
"annotation.item_id = media_file.id"+
|
||||||
" AND annotation.item_type = 'media_file'"+
|
" AND annotation.item_type = 'media_file'"+
|
||||||
" AND annotation.user_id = ?)", usr.ID)
|
" AND annotation.user_id = ?)", userID)
|
||||||
countSq = r.addSmartPlaylistAnnotationJoins(countSq, exprJoins, usr.ID)
|
countSq = r.addSmartPlaylistAnnotationJoins(countSq, exprJoins, userID)
|
||||||
countSq = r.applyLibraryFilter(countSq, "media_file")
|
countSq = r.applyLibraryFilter(countSq, "media_file")
|
||||||
countSq = countSq.Where(rules)
|
countSq = countSq.Where(rules)
|
||||||
|
|
||||||
@ -310,6 +314,18 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playlistRepository) Evaluate(id string) error {
|
||||||
|
pls, err := r.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !pls.IsSmartPlaylist() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.doRefreshSmartPlaylist(pls, pls.OwnerID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *playlistRepository) addSmartPlaylistAnnotationJoins(sq SelectBuilder, joins criteria.JoinType, userID string) SelectBuilder {
|
func (r *playlistRepository) addSmartPlaylistAnnotationJoins(sq SelectBuilder, joins criteria.JoinType, userID string) SelectBuilder {
|
||||||
if joins.Has(criteria.JoinAlbumAnnotation) {
|
if joins.Has(criteria.JoinAlbumAnnotation) {
|
||||||
sq = sq.LeftJoin("annotation AS album_annotation ON ("+
|
sq = sq.LeftJoin("annotation AS album_annotation ON ("+
|
||||||
|
|||||||
@ -254,6 +254,57 @@ var _ = Describe("PlaylistRepository", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("Evaluate", func() {
|
||||||
|
var testPlaylistID string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
if testPlaylistID != "" {
|
||||||
|
_ = repo.Delete(testPlaylistID)
|
||||||
|
testPlaylistID = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("evaluates a smart playlist and sets EvaluatedAt and SongCount", func() {
|
||||||
|
rules := &criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.Contains{"title": "Day"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
newPls := model.Playlist{Name: "Evaluate Test", OwnerID: "userid", Rules: rules}
|
||||||
|
Expect(repo.Put(&newPls)).To(Succeed())
|
||||||
|
testPlaylistID = newPls.ID
|
||||||
|
|
||||||
|
Expect(repo.Evaluate(newPls.ID)).To(Succeed())
|
||||||
|
|
||||||
|
saved, err := repo.Get(newPls.ID)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(saved.EvaluatedAt).ToNot(BeNil())
|
||||||
|
Expect(*saved.EvaluatedAt).To(BeTemporally("~", time.Now(), 2*time.Second))
|
||||||
|
Expect(saved.SongCount).To(BeNumerically(">", 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("is a no-op for non-smart playlists", func() {
|
||||||
|
newPls := model.Playlist{Name: "Regular Playlist", OwnerID: "userid"}
|
||||||
|
Expect(repo.Put(&newPls)).To(Succeed())
|
||||||
|
testPlaylistID = newPls.ID
|
||||||
|
|
||||||
|
Expect(repo.Evaluate(newPls.ID)).To(Succeed())
|
||||||
|
|
||||||
|
saved, err := repo.Get(newPls.ID)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(saved.EvaluatedAt).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound for a non-existent playlist ID", func() {
|
||||||
|
err := repo.Evaluate("nonexistent-id")
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Describe("Playlist Track Sorting", func() {
|
Describe("Playlist Track Sorting", func() {
|
||||||
var testPlaylistID string
|
var testPlaylistID string
|
||||||
|
|
||||||
|
|||||||
@ -108,4 +108,11 @@ func (m *MockPlaylistRepo) CountAll(_ ...model.QueryOptions) (int64, error) {
|
|||||||
return int64(len(m.Data)), nil
|
return int64(len(m.Data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockPlaylistRepo) Evaluate(_ string) error {
|
||||||
|
if m.Err {
|
||||||
|
return errors.New("error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ model.PlaylistRepository = (*MockPlaylistRepo)(nil)
|
var _ model.PlaylistRepository = (*MockPlaylistRepo)(nil)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user