Optimize playlist cover generation

This commit is contained in:
Deluan 2023-01-13 21:33:49 -05:00 committed by Deluan Quintão
parent c46a2a5f5f
commit 16c869ec86
5 changed files with 42 additions and 28 deletions

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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...))
}

View File

@ -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)