mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Entity-level artwork queries now take a typed model.Kind instead of a bare prefix string. GetItemArtwork, DeleteForItem(s), GetInfoForItems, EnqueueStaleAbsent, hydrateItemImages, enqueueBackfillKind and artwork.Refresh convert to the prefix string only at the two real boundaries: the SQL item_kind column (kind.Prefix() inside each repo) and external string inputs (a new model.ParseKind for the nativeapi URL param, which also validates it). The Backfill/stale-absent kind slices, the resolve.go dispatch switch, and the kind→resource / kind→table lookup maps now use the Kind vars directly. The queue lifecycle methods (MarkFailed/Delete*) keep string kinds — they operate on a dequeued item's raw ItemKind column, which stays a string field, always populated via kind.Prefix(). Removes every bare "al"/"ar"/… prefix literal from non-test code (27 -> 0); behavior is unchanged.
219 lines
6.3 KiB
Go
219 lines
6.3 KiB
Go
package persistence
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"time"
|
|
|
|
. "github.com/Masterminds/squirrel"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/pocketbase/dbx"
|
|
)
|
|
|
|
const (
|
|
itemArtworkTable = "item_artwork"
|
|
artworkBatchSize = 200
|
|
)
|
|
|
|
type itemArtworkSQL struct {
|
|
sqlRepository
|
|
}
|
|
|
|
type artworkRepository struct {
|
|
sqlRepository
|
|
items itemArtworkSQL
|
|
}
|
|
|
|
func NewArtworkRepository(ctx context.Context, db dbx.Builder) model.ArtworkRepository {
|
|
r := &artworkRepository{}
|
|
r.ctx = ctx
|
|
r.db = db
|
|
r.tableName = "artwork"
|
|
r.items.ctx = ctx
|
|
r.items.db = db
|
|
r.items.tableName = itemArtworkTable
|
|
return r
|
|
}
|
|
|
|
func (r *artworkRepository) GetImage(hash string) (*model.Artwork, error) {
|
|
sel := Select("*").From(r.tableName).Where(Eq{"hash": hash})
|
|
var res model.Artwork
|
|
if err := r.queryOne(sel, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
return &res, nil
|
|
}
|
|
|
|
func (r *artworkRepository) PutImage(a *model.Artwork) error {
|
|
// created_at is the last-acquisition-write time the prune grace window keys on.
|
|
a.CreatedAt = time.Now()
|
|
values, err := toSQLArgs(*a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// created_at=excluded.created_at: reacquiring an orphan must reset the prune grace window.
|
|
ins := Insert(r.tableName).SetMap(values).Suffix(`ON CONFLICT (hash) DO UPDATE SET mime=excluded.mime, width=excluded.width,
|
|
height=excluded.height, size_bytes=excluded.size_bytes, blur_hash=excluded.blur_hash, created_at=excluded.created_at`)
|
|
_, err = r.executeSQL(ins)
|
|
return err
|
|
}
|
|
|
|
func (r *artworkRepository) GetImages(hashes []string) (map[string]model.Artwork, error) {
|
|
res := map[string]model.Artwork{}
|
|
for chunk := range slices.Chunk(hashes, artworkBatchSize) {
|
|
sel := Select("*").From(r.tableName).Where(Eq{"hash": chunk})
|
|
var all []model.Artwork
|
|
if err := r.queryAll(sel, &all); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, a := range all {
|
|
res[a.Hash] = a
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (r *artworkRepository) GetAllMimes() (map[string]string, error) {
|
|
sel := Select("hash", "mime").From(r.tableName)
|
|
var rows []struct {
|
|
Hash string
|
|
Mime string
|
|
}
|
|
if err := r.queryAll(sel, &rows); err != nil {
|
|
return nil, err
|
|
}
|
|
res := make(map[string]string, len(rows))
|
|
for _, row := range rows {
|
|
res[row.Hash] = row.Mime
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (r *artworkRepository) GetOrphanHashes(createdBefore time.Time) ([]string, error) {
|
|
sel := Select("hash").From(r.tableName).
|
|
Where(And{
|
|
Lt{"created_at": createdBefore},
|
|
Expr("hash NOT IN (SELECT hash FROM " + itemArtworkTable + " WHERE hash <> '')"),
|
|
})
|
|
var hashes []string
|
|
err := r.queryAllSlice(sel, &hashes)
|
|
return hashes, err
|
|
}
|
|
|
|
func (r *artworkRepository) DeleteOrphans(createdBefore time.Time, hashes []string) error {
|
|
for chunk := range slices.Chunk(hashes, artworkBatchSize) {
|
|
del := Delete(r.tableName).Where(And{
|
|
Eq{"hash": chunk},
|
|
Lt{"created_at": createdBefore},
|
|
Expr("hash NOT IN (SELECT hash FROM " + itemArtworkTable + " WHERE hash <> '')"),
|
|
})
|
|
if _, err := r.executeSQL(del); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// danglingItemArtworkKinds maps an artwork kind to the table that owns the entity.
|
|
var danglingItemArtworkKinds = map[model.Kind]string{
|
|
model.KindAlbumArtwork: "album",
|
|
model.KindArtistArtwork: "artist",
|
|
model.KindPlaylistArtwork: "playlist",
|
|
model.KindRadioArtwork: "radio",
|
|
model.KindMediaFileArtwork: "media_file",
|
|
}
|
|
|
|
// 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, entityTable := range danglingItemArtworkKinds {
|
|
del := Delete(table).Where(And{
|
|
Eq{"item_kind": kind.Prefix()},
|
|
Expr("item_id NOT IN (SELECT id FROM " + entityTable + ")"),
|
|
})
|
|
c, err := execute(del)
|
|
if err != nil {
|
|
return total, err
|
|
}
|
|
total += c
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (r *artworkRepository) PurgeDanglingItemArtwork() (int64, error) {
|
|
return purgeDangling(r.items.executeSQL, itemArtworkTable)
|
|
}
|
|
|
|
func (r *artworkRepository) GetItemArtwork(kind model.Kind, id, imageType string) (*model.ItemArtwork, error) {
|
|
sel := Select("*").From(itemArtworkTable).
|
|
Where(Eq{"item_kind": kind.Prefix(), "item_id": id, "image_type": imageType})
|
|
var res model.ItemArtwork
|
|
if err := r.items.queryOne(sel, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
return &res, nil
|
|
}
|
|
|
|
func (r *artworkRepository) PutItemArtwork(ia *model.ItemArtwork) error {
|
|
if ia.ImageType == "" {
|
|
ia.ImageType = model.ImageTypePrimary
|
|
}
|
|
ia.UpdatedAt = time.Now()
|
|
// PutItemArtwork records the outcome of an attempt, so an unset attempted_at is now.
|
|
if ia.AttemptedAt.IsZero() {
|
|
ia.AttemptedAt = ia.UpdatedAt
|
|
}
|
|
values, err := toSQLArgs(*ia)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ins := Insert(itemArtworkTable).SetMap(values).Suffix(`ON CONFLICT (item_kind, item_id, image_type) DO UPDATE SET
|
|
hash=excluded.hash, source=excluded.source, source_path=excluded.source_path, ref_mtime=excluded.ref_mtime,
|
|
attempted_at=excluded.attempted_at, updated_at=excluded.updated_at`)
|
|
_, err = r.items.executeSQL(ins)
|
|
return err
|
|
}
|
|
|
|
func (r *artworkRepository) DeleteForItem(kind model.Kind, id string) error {
|
|
return r.items.delete(Eq{"item_kind": kind.Prefix(), "item_id": id})
|
|
}
|
|
|
|
func (r *artworkRepository) DeleteForItems(kind model.Kind, ids []string) error {
|
|
for chunk := range slices.Chunk(ids, artworkBatchSize) {
|
|
if err := r.items.delete(Eq{"item_kind": kind.Prefix(), "item_id": chunk}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *artworkRepository) GetInfoForItems(kind model.Kind, ids []string) (map[string]model.ItemArtworkInfo, error) {
|
|
res := map[string]model.ItemArtworkInfo{}
|
|
for chunk := range slices.Chunk(ids, artworkBatchSize) {
|
|
sel := Select("ia.item_id", "ia.hash", "COALESCE(a.blur_hash, '') as blur_hash").
|
|
From(itemArtworkTable + " ia").
|
|
LeftJoin("artwork a ON a.hash = ia.hash").
|
|
Where(And{
|
|
Eq{"ia.item_kind": kind.Prefix()},
|
|
Eq{"ia.image_type": model.ImageTypePrimary},
|
|
Eq{"ia.item_id": chunk},
|
|
})
|
|
var rows []struct {
|
|
ItemID string
|
|
Hash string
|
|
BlurHash string
|
|
}
|
|
if err := r.items.queryAll(sel, &rows); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range rows {
|
|
res[row.ItemID] = model.ItemArtworkInfo{
|
|
ItemID: row.ItemID, Hash: row.Hash, BlurHash: row.BlurHash,
|
|
}
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
var _ model.ArtworkRepository = (*artworkRepository)(nil)
|