diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index ae0976ec..9a903cc4 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -8,13 +8,12 @@ import ( "image/draw" "image/png" "io" - "math/rand" "time" "github.com/disintegration/imaging" + "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" - "golang.org/x/exp/slices" ) type playlistArtworkReader struct { @@ -44,18 +43,16 @@ func (a *playlistArtworkReader) LastUpdated() time.Time { } func (a *playlistArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { - var ff []sourceFunc - pl, err := a.a.ds.Playlist(ctx).GetWithTracks(a.pl.ID, false) - if err == nil { - ff = append(ff, a.fromGeneratedTile(ctx, pl.Tracks)) + ff := []sourceFunc{ + a.fromGeneratedTile(ctx), + fromAlbumPlaceholder(), } - ff = append(ff, fromAlbumPlaceholder()) return selectImageReader(ctx, a.artID, ff...) } -func (a *playlistArtworkReader) fromGeneratedTile(ctx context.Context, tracks model.PlaylistTracks) sourceFunc { +func (a *playlistArtworkReader) fromGeneratedTile(ctx context.Context) sourceFunc { return func() (io.ReadCloser, string, error) { - tiles, err := a.loadTiles(ctx, tracks) + tiles, err := a.loadTiles(ctx) if err != nil { return nil, "", err } @@ -64,19 +61,21 @@ func (a *playlistArtworkReader) fromGeneratedTile(ctx context.Context, tracks mo } } -func compactIDs(tracks model.PlaylistTracks) []model.ArtworkID { - slices.SortFunc(tracks, func(a, b model.PlaylistTrack) bool { return a.AlbumID < b.AlbumID }) - tracks = slices.CompactFunc(tracks, func(a, b model.PlaylistTrack) bool { return a.AlbumID == b.AlbumID }) - ids := slice.Map(tracks, func(e model.PlaylistTrack) model.ArtworkID { - return e.AlbumCoverArtID() +func toArtworkIDs(albumIDs []string) []model.ArtworkID { + return slice.Map(albumIDs, func(id string) model.ArtworkID { + al := model.Album{ID: id} + return al.CoverArtID() }) - rand.Seed(time.Now().UnixNano()) - rand.Shuffle(len(ids), func(i, j int) { ids[i], ids[j] = ids[j], ids[i] }) - return ids } -func (a *playlistArtworkReader) loadTiles(ctx context.Context, t model.PlaylistTracks) ([]image.Image, error) { - ids := compactIDs(t) +func (a *playlistArtworkReader) loadTiles(ctx context.Context) ([]image.Image, error) { + tracksRepo := a.a.ds.Playlist(ctx).Tracks(a.pl.ID, false) + albumIds, err := tracksRepo.GetAlbumIDs(model.QueryOptions{Max: 4, Sort: "random()"}) + if err != nil { + log.Error(ctx, "Error getting album IDs for playlist", "id", a.pl.ID, "name", a.pl.Name, err) + return nil, err + } + ids := toArtworkIDs(albumIds) var tiles []image.Image for len(tiles) < 4 { diff --git a/core/playlists.go b/core/playlists.go index a538986d..fd943389 100644 --- a/core/playlists.go +++ b/core/playlists.go @@ -205,7 +205,7 @@ func (s *playlists) Update(ctx context.Context, playlistID string, pls.AddTracks(idsToAdd) } else { if len(idsToAdd) > 0 { - _, err = repo.Tracks(playlistID).Add(idsToAdd) + _, err = repo.Tracks(playlistID, true).Add(idsToAdd) if err != nil { return err } @@ -232,7 +232,7 @@ func (s *playlists) Update(ctx context.Context, playlistID string, } // Special case: The playlist is now empty if len(idxToRemove) > 0 && len(pls.Tracks) == 0 { - if err = repo.Tracks(playlistID).DeleteAll(); err != nil { + if err = repo.Tracks(playlistID, true).DeleteAll(); err != nil { return err } } diff --git a/model/playlist.go b/model/playlist.go index 085d3f3c..f9f7288d 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -110,7 +110,7 @@ type PlaylistRepository interface { GetAll(options ...QueryOptions) (Playlists, error) FindByPath(path string) (*Playlist, error) Delete(id string) error - Tracks(playlistId string) PlaylistTrackRepository + Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository } type PlaylistTrack struct { @@ -133,6 +133,7 @@ func (plt PlaylistTracks) MediaFiles() MediaFiles { type PlaylistTrackRepository interface { ResourceRepository GetAll(options ...QueryOptions) (PlaylistTracks, error) + GetAlbumIDs(options ...QueryOptions) ([]string, error) Add(mediaFileIds []string) (int, error) AddAlbums(albumIds []string) (int, error) AddArtists(artistIds []string) (int, error) diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 08830058..b5aa4f90 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -16,7 +16,7 @@ type playlistTrackRepository struct { playlistRepo *playlistRepository } -func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTrackRepository { +func (r *playlistRepository) Tracks(playlistId string, refreshSmartPlaylist bool) model.PlaylistTrackRepository { p := &playlistTrackRepository{} p.playlistRepo = r p.playlistId = playlistId @@ -30,7 +30,9 @@ func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTrackReposi if err != nil { return nil } - r.refreshSmartPlaylist(pls) + if refreshSmartPlaylist { + r.refreshSmartPlaylist(pls) + } p.playlist = pls return p } @@ -70,6 +72,18 @@ func (r *playlistTrackRepository) GetAll(options ...model.QueryOptions) (model.P return tracks, err } +func (r *playlistTrackRepository) GetAlbumIDs(options ...model.QueryOptions) ([]string, error) { + sql := r.newSelect(options...).Columns("distinct mf.album_id"). + Join("media_file mf on mf.id = media_file_id"). + Where(Eq{"playlist_id": r.playlistId}) + var ids []string + err := r.queryAll(sql, &ids) + if err != nil { + return nil, err + } + return ids, nil +} + func (r *playlistTrackRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) { return r.GetAll(r.parseRestOptions(options...)) } diff --git a/server/nativeapi/playlists.go b/server/nativeapi/playlists.go index 19ebdc71..494e4dcf 100644 --- a/server/nativeapi/playlists.go +++ b/server/nativeapi/playlists.go @@ -25,7 +25,7 @@ func getPlaylist(ds model.DataStore) http.HandlerFunc { constructor := func(ctx context.Context) rest.Repository { plsRepo := ds.Playlist(ctx) plsId := chi.URLParam(req, "playlistId") - return plsRepo.Tracks(plsId) + return plsRepo.Tracks(plsId, true) } handler(constructor).ServeHTTP(res, req) @@ -77,7 +77,7 @@ func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc { playlistId := utils.ParamString(r, ":playlistId") ids := r.URL.Query()["id"] err := ds.WithTx(func(tx model.DataStore) error { - tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId) + tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId, true) return tracksRepo.Delete(ids...) }) if len(ids) == 1 && errors.Is(err, model.ErrNotFound) { @@ -125,7 +125,7 @@ func addToPlaylist(ds model.DataStore) http.HandlerFunc { http.Error(w, err.Error(), http.StatusBadRequest) return } - tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId) + tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId, true) count, c := 0, 0 if c, err = tracksRepo.Add(payload.Ids); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -179,7 +179,7 @@ func reorderItem(ds model.DataStore) http.HandlerFunc { http.Error(w, err.Error(), http.StatusBadRequest) return } - tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId) + tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId, true) err = tracksRepo.Reorder(id, newPos) if errors.Is(err, rest.ErrPermissionDenied) { http.Error(w, err.Error(), http.StatusForbidden)