refactor(artwork): deduplicate purge loop, backfill table, and extGate alias

This commit is contained in:
Deluan 2026-07-22 12:27:55 -04:00
parent c57496d50d
commit 87095fab08
5 changed files with 47 additions and 63 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
)
// FingerprintPropertyKey is the model.PropertyRepository key Backfill compares against
@ -46,36 +47,36 @@ func Backfill(ctx context.Context, ds model.DataStore) (bool, error) {
return false, nil
}
artists, err := ds.Artist(ctx).GetAll()
if err != nil {
return false, err
// Artists first: few entities, most external-dependent, so they get queue headstart.
kinds := []struct {
kind string
fetch func() ([]string, error)
}{
{"ar", func() ([]string, error) {
as, err := ds.Artist(ctx).GetAll()
return slice.Map(as, func(a model.Artist) string { return a.ID }), err
}},
{"al", func() ([]string, error) {
as, err := ds.Album(ctx).GetAll()
return slice.Map(as, func(a model.Album) string { return a.ID }), err
}},
{"pl", func() ([]string, error) {
ps, err := ds.Playlist(ctx).GetAll()
return slice.Map(ps, func(p model.Playlist) string { return p.ID }), err
}},
{"ra", func() ([]string, error) {
rs, err := ds.Radio(ctx).GetAll()
return slice.Map(rs, func(r model.Radio) string { return r.ID }), err
}},
}
if err := enqueueBackfillKind(ctx, ds, "ar", idsOf(artists, func(a model.Artist) string { return a.ID })); err != nil {
return false, err
}
albums, err := ds.Album(ctx).GetAll()
if err != nil {
return false, err
}
if err := enqueueBackfillKind(ctx, ds, "al", idsOf(albums, func(a model.Album) string { return a.ID })); err != nil {
return false, err
}
playlists, err := ds.Playlist(ctx).GetAll()
if err != nil {
return false, err
}
if err := enqueueBackfillKind(ctx, ds, "pl", idsOf(playlists, func(p model.Playlist) string { return p.ID })); err != nil {
return false, err
}
radios, err := ds.Radio(ctx).GetAll()
if err != nil {
return false, err
}
if err := enqueueBackfillKind(ctx, ds, "ra", idsOf(radios, func(r model.Radio) string { return r.ID })); err != nil {
return false, err
for _, k := range kinds {
ids, err := k.fetch()
if err != nil {
return false, err
}
if err := enqueueBackfillKind(ctx, ds, k.kind, ids); err != nil {
return false, err
}
}
if err := props.Put(FingerprintPropertyKey, current); err != nil {
@ -98,14 +99,6 @@ func enqueueBackfillKind(ctx context.Context, ds model.DataStore, kind string, i
return ds.ArtworkQueue(ctx).Enqueue(items...)
}
func idsOf[T any](items []T, id func(T) string) []string {
ids := make([]string, len(items))
for i, it := range items {
ids[i] = id(it)
}
return ids
}
// EnqueueStaleAbsentAll requeues absent-state entries older than staleAbsentAge, across
// every artwork-bearing kind, for the periodic recheck job.
func EnqueueStaleAbsentAll(ctx context.Context, ds model.DataStore) error {

View File

@ -38,7 +38,7 @@ type workerDeps struct {
store *ImageStore
prov external.Provider
ffmpeg ffmpeg.FFmpeg
extGate func(func() (io.ReadCloser, string, error)) (io.ReadCloser, string, error)
extGate extGateFunc
}
// processItem resolves one queue item end to end: find an image, hash/decode/

View File

@ -39,7 +39,7 @@ func passthroughExtGate(f func() (io.ReadCloser, string, error)) (io.ReadCloser,
}
// resolveItem walks the kind's priority chain and returns the first hit.
func resolveItem(ctx context.Context, ds model.DataStore, prov external.Provider, ffmpeg ffmpeg.FFmpeg, item model.ArtworkQueueItem, extGate func(func() (io.ReadCloser, string, error)) (io.ReadCloser, string, error)) (resolution, error) {
func resolveItem(ctx context.Context, ds model.DataStore, prov external.Provider, ffmpeg ffmpeg.FFmpeg, item model.ArtworkQueueItem, extGate extGateFunc) (resolution, error) {
if extGate == nil {
extGate = passthroughExtGate
}
@ -290,12 +290,10 @@ func resolveEmbedded(ctx context.Context, lib libraryView, ffm ffmpeg.FFmpeg, em
return resolution{}, false
}
abs := lib.Abs(embedRel)
mtime := mtimeViaFS(lib.FS, embedRel)
if r, _, _ := fromTag(ctx, lib.FS, embedRel)(); r != nil {
return resolution{reader: r, source: "embedded", sourcePath: abs, refMtime: mtime}, true
}
if r, _, _ := fromFFmpegTag(ctx, ffm, abs)(); r != nil {
return resolution{reader: r, source: "embedded", sourcePath: abs, refMtime: mtime}, true
for _, sf := range []sourceFunc{fromTag(ctx, lib.FS, embedRel), fromFFmpegTag(ctx, ffm, abs)} {
if r, _, _ := sf(); r != nil {
return resolution{reader: r, source: "embedded", sourcePath: abs, refMtime: mtimeViaFS(lib.FS, embedRel)}, true
}
}
return resolution{}, false
}

View File

@ -78,19 +78,7 @@ func (r *artworkQueueRepository) DeleteIfUnchanged(kind, id, imageType string, r
// PurgeDangling removes queue rows whose entity no longer exists, per kind.
func (r *artworkQueueRepository) PurgeDangling() (int64, error) {
var total int64
for kind, table := range danglingItemArtworkKinds {
del := Delete(r.tableName).Where(And{
Eq{"item_kind": kind},
Expr("item_id NOT IN (SELECT id FROM " + table + ")"),
})
c, err := r.executeSQL(del)
if err != nil {
return total, err
}
total += c
}
return total, nil
return purgeDangling(r.executeSQL, r.tableName)
}
func (r *artworkQueueRepository) Count() (int64, error) {

View File

@ -122,14 +122,15 @@ var danglingItemArtworkKinds = map[string]string{
"ra": "radio",
}
func (r *artworkRepository) PurgeDanglingItemArtwork() (int64, error) {
// purgeDangling deletes rows in table whose owning entity is gone, one statement per kind.
func purgeDangling(execute func(Sqlizer) (int64, error), table string) (int64, error) {
var total int64
for kind, table := range danglingItemArtworkKinds {
del := Delete(itemArtworkTable).Where(And{
for kind, entityTable := range danglingItemArtworkKinds {
del := Delete(table).Where(And{
Eq{"item_kind": kind},
Expr("item_id NOT IN (SELECT id FROM " + table + ")"),
Expr("item_id NOT IN (SELECT id FROM " + entityTable + ")"),
})
c, err := r.items.executeSQL(del)
c, err := execute(del)
if err != nil {
return total, err
}
@ -138,6 +139,10 @@ func (r *artworkRepository) PurgeDanglingItemArtwork() (int64, error) {
return total, nil
}
func (r *artworkRepository) PurgeDanglingItemArtwork() (int64, error) {
return purgeDangling(r.items.executeSQL, itemArtworkTable)
}
func (r *artworkRepository) GetItemArtwork(kind, id, imageType string) (*model.ItemArtwork, error) {
sel := Select("*").From(itemArtworkTable).
Where(Eq{"item_kind": kind, "item_id": id, "image_type": imageType})