diff --git a/core/artwork/housekeeping.go b/core/artwork/housekeeping.go index fea80eec6..c26b6790d 100644 --- a/core/artwork/housekeeping.go +++ b/core/artwork/housekeeping.go @@ -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 { diff --git a/core/artwork/processor.go b/core/artwork/processor.go index 94262f552..8852f70b8 100644 --- a/core/artwork/processor.go +++ b/core/artwork/processor.go @@ -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/ diff --git a/core/artwork/resolve.go b/core/artwork/resolve.go index af151ef4a..7bc5115d0 100644 --- a/core/artwork/resolve.go +++ b/core/artwork/resolve.go @@ -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 } diff --git a/persistence/artwork_queue_repository.go b/persistence/artwork_queue_repository.go index eb5f21ecd..4a6a68eb1 100644 --- a/persistence/artwork_queue_repository.go +++ b/persistence/artwork_queue_repository.go @@ -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) { diff --git a/persistence/artwork_repository.go b/persistence/artwork_repository.go index 622f47bfe..9ac32b42e 100644 --- a/persistence/artwork_repository.go +++ b/persistence/artwork_repository.go @@ -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})