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
|
||||
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
|
||||
GetPlaylists(mediaFileId string) (Playlists, error)
|
||||
Evaluate(id string) error
|
||||
}
|
||||
|
||||
type PlaylistTrack struct {
|
||||
|
||||
@ -215,6 +215,10 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
||||
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)
|
||||
start := time.Now()
|
||||
|
||||
@ -244,11 +248,11 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
||||
From("media_file").LeftJoin("annotation on ("+
|
||||
"annotation.item_id = media_file.id"+
|
||||
" 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
|
||||
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
|
||||
sq = r.applyLibraryFilter(sq, "media_file")
|
||||
@ -261,8 +265,8 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
||||
LeftJoin("annotation on ("+
|
||||
"annotation.item_id = media_file.id"+
|
||||
" AND annotation.item_type = 'media_file'"+
|
||||
" AND annotation.user_id = ?)", usr.ID)
|
||||
countSq = r.addSmartPlaylistAnnotationJoins(countSq, exprJoins, usr.ID)
|
||||
" AND annotation.user_id = ?)", userID)
|
||||
countSq = r.addSmartPlaylistAnnotationJoins(countSq, exprJoins, userID)
|
||||
countSq = r.applyLibraryFilter(countSq, "media_file")
|
||||
countSq = countSq.Where(rules)
|
||||
|
||||
@ -310,6 +314,18 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
||||
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 {
|
||||
if joins.Has(criteria.JoinAlbumAnnotation) {
|
||||
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() {
|
||||
var testPlaylistID string
|
||||
|
||||
|
||||
@ -108,4 +108,11 @@ func (m *MockPlaylistRepo) CountAll(_ ...model.QueryOptions) (int64, error) {
|
||||
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user