refactor(persistence): collapse the four hydrateArtwork copies into one generic

album/artist/playlist/radio each carried the same eight lines, differing
only in element type and model.Kind. hydrateItems takes a ref callback
yielding an item's id and the ItemImage to fill, which is all that varied.

The len()==0 guards drop out: hydrateItemImages already short-circuits an
empty id list, and both loops are no-ops on an empty slice. applyItemImage
stays as-is; hydrateMediaFileArtwork and its own specs still use it.
This commit is contained in:
Deluan 2026-07-27 21:58:54 -04:00
parent 8c65931ba7
commit 88d4e8f9de
5 changed files with 23 additions and 34 deletions

View File

@ -259,14 +259,8 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e
}
func (r *albumRepository) hydrateArtwork(albums model.Albums) {
if len(albums) == 0 {
return
}
ids := slice.Map(albums, func(a model.Album) string { return a.ID })
infos := hydrateItemImages(r.ctx, r.db, model.KindAlbumArtwork, ids)
for i := range albums {
applyItemImage(infos, albums[i].ID, &albums[i].ItemImage)
}
hydrateItems(r.ctx, r.db, model.KindAlbumArtwork, albums,
func(a *model.Album) (string, *model.ItemImage) { return a.ID, &a.ItemImage })
}
// GetAllIDs returns the IDs of GetAll's row set, skipping its column projection and JSON decoding.

View File

@ -279,14 +279,8 @@ func (r *artistRepository) GetAllIDs(options ...model.QueryOptions) ([]string, e
// hydrateArtwork fills each artist's ImageHash/ImageAbsent from one batched item_artwork lookup.
func (r *artistRepository) hydrateArtwork(artists model.Artists) {
if len(artists) == 0 {
return
}
ids := slice.Map(artists, func(a model.Artist) string { return a.ID })
infos := hydrateItemImages(r.ctx, r.db, model.KindArtistArtwork, ids)
for i := range artists {
applyItemImage(infos, artists[i].ID, &artists[i].ItemImage)
}
hydrateItems(r.ctx, r.db, model.KindArtistArtwork, artists,
func(a *model.Artist) (string, *model.ItemImage) { return a.ID, &a.ItemImage })
}
func (r *artistRepository) GetCursor(options ...model.QueryOptions) (model.ArtistCursor, error) {

View File

@ -74,6 +74,21 @@ func applyItemImage(infos map[string]model.ItemArtworkInfo, id string, img *mode
}
}
// hydrateItems fills every item's embedded ItemImage from one batched lookup. ref yields an
// item's id and the ItemImage to fill, which is all that differs between the entity kinds.
func hydrateItems[T any](ctx context.Context, db dbx.Builder, kind model.Kind, items []T,
ref func(*T) (string, *model.ItemImage)) {
ids := make([]string, len(items))
for i := range items {
ids[i], _ = ref(&items[i])
}
infos := hydrateItemImages(ctx, db, kind, ids)
for i := range items {
id, img := ref(&items[i])
applyItemImage(infos, id, img)
}
}
// hydrateMediaFileArtwork mirrors MediaFile.CoverArtID: an embedded-eligible file with resolved own
// art uses it, else it falls back to the album's.
func hydrateMediaFileArtwork(ctx context.Context, db dbx.Builder, mfs model.MediaFiles) {

View File

@ -13,7 +13,6 @@ import (
"github.com/deluan/rest"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
"github.com/pocketbase/dbx"
)
@ -185,14 +184,8 @@ func (r *playlistRepository) findBy(sql Sqlizer) (*model.Playlist, error) {
}
func (r *playlistRepository) hydrateArtwork(playlists model.Playlists) {
if len(playlists) == 0 {
return
}
ids := slice.Map(playlists, func(p model.Playlist) string { return p.ID })
infos := hydrateItemImages(r.ctx, r.db, model.KindPlaylistArtwork, ids)
for i := range playlists {
applyItemImage(infos, playlists[i].ID, &playlists[i].ItemImage)
}
hydrateItems(r.ctx, r.db, model.KindPlaylistArtwork, playlists,
func(p *model.Playlist) (string, *model.ItemImage) { return p.ID, &p.ItemImage })
}
func (r *playlistRepository) GetAll(options ...model.QueryOptions) (model.Playlists, error) {

View File

@ -10,7 +10,6 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/slice"
"github.com/pocketbase/dbx"
)
@ -76,14 +75,8 @@ func (r *radioRepository) GetAll(options ...model.QueryOptions) (model.Radios, e
// hydrateArtwork fills each radio's ImageHash/ImageAbsent from one batched item_artwork lookup.
func (r *radioRepository) hydrateArtwork(radios model.Radios) {
if len(radios) == 0 {
return
}
ids := slice.Map(radios, func(rd model.Radio) string { return rd.ID })
infos := hydrateItemImages(r.ctx, r.db, model.KindRadioArtwork, ids)
for i := range radios {
applyItemImage(infos, radios[i].ID, &radios[i].ItemImage)
}
hydrateItems(r.ctx, r.db, model.KindRadioArtwork, radios,
func(rd *model.Radio) (string, *model.ItemImage) { return rd.ID, &rd.ItemImage })
}
// GetAllIDs returns just the radio IDs. Used by bulk enumeration (artwork backfill).