mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
A full scan re-imports every track, so the per-entity artwork enqueue in persistChanges fired for every album and artist in the library — measured as 100% of both on a live instance, and ~8.5k artists / ~16.7k Deezer fetches on a 96k-file library. Re-importing a track is no evidence the art changed: artist art has no track-content source at all, and the albums re-derived to identical hashes across three consecutive scans. Enqueue through EnqueueIfMissing on a full scan, which anti-joins item_artwork so only entities that never resolved are queued. Incremental scans keep the unconditional Enqueue, where a re-import does mean the file changed. Doing this at the enqueue site rather than skipping it wholesale keeps a first scan filling in covers as it walks, instead of stalling every cover until the scan ends. EnqueueMissing grows a priority argument and runs once at end of scan, so an entity phase 1 never saw still resolves, at Scan priority rather than dropping to Recheck behind the backfill.
137 lines
5.7 KiB
Go
137 lines
5.7 KiB
Go
package model
|
|
|
|
import "time"
|
|
|
|
// Artwork is one unique image, identified by the XXH3-64 hash of its bytes.
|
|
type Artwork struct {
|
|
Hash string `structs:"hash"`
|
|
Mime string `structs:"mime"`
|
|
Width int `structs:"width"`
|
|
Height int `structs:"height"`
|
|
SizeBytes int64 `structs:"size_bytes"`
|
|
BlurHash string `structs:"blur_hash"`
|
|
CreatedAt time.Time `structs:"created_at"`
|
|
}
|
|
|
|
const ImageTypePrimary = "primary"
|
|
|
|
// ItemImage is per-entity artwork state hydrated at query time; never persisted.
|
|
type ItemImage struct {
|
|
ImageHash string `structs:"-" json:"imageHash,omitempty"`
|
|
ImageAbsent bool `structs:"-" json:"imageAbsent,omitempty"`
|
|
BlurHash string `structs:"-" json:"blurHash,omitempty"`
|
|
// A blurhash carries no aspect ratio, so clients need these to shape the placeholder.
|
|
ImageWidth int `structs:"-" json:"imageWidth,omitempty"`
|
|
ImageHeight int `structs:"-" json:"imageHeight,omitempty"`
|
|
}
|
|
|
|
// AspectRatio is the image's width/height, or nil when the image or its dimensions are unknown.
|
|
func (i ItemImage) AspectRatio() *float64 {
|
|
if i.ImageAbsent || i.ImageWidth <= 0 || i.ImageHeight <= 0 {
|
|
return nil
|
|
}
|
|
return new(float64(i.ImageWidth) / float64(i.ImageHeight))
|
|
}
|
|
|
|
// ItemArtwork is an entity's resolved artwork state. Hash=="" means known absent.
|
|
type ItemArtwork struct {
|
|
ItemKind string `structs:"item_kind"`
|
|
ItemID string `structs:"item_id"`
|
|
ImageType string `structs:"image_type"`
|
|
Hash string `structs:"hash"`
|
|
Source string `structs:"source"`
|
|
// SourcePath is the backing file (folder/upload: the image; embedded: the audio file); "" otherwise.
|
|
SourcePath string `structs:"source_path"`
|
|
// RefMtime is SourcePath's mtime (unix-nanoseconds) at resolution; 0 when there is no SourcePath.
|
|
RefMtime int64 `structs:"ref_mtime"`
|
|
// Nullable in the schema, but every insert must set them: these non-pointer fields cannot scan NULL.
|
|
AttemptedAt time.Time `structs:"attempted_at"`
|
|
UpdatedAt time.Time `structs:"updated_at"`
|
|
}
|
|
|
|
// ItemArtworkInfo is the list-hydration projection (item_artwork joined with artwork).
|
|
type ItemArtworkInfo struct {
|
|
ItemID string
|
|
Hash string
|
|
BlurHash string
|
|
Width int
|
|
Height int
|
|
}
|
|
|
|
// Absent reports a known-absent artwork state (resolved, no image).
|
|
func (i ItemArtworkInfo) Absent() bool { return i.Hash == "" }
|
|
|
|
// Image projects the hydration entry onto the entity-facing struct.
|
|
func (i ItemArtworkInfo) Image() ItemImage {
|
|
return ItemImage{
|
|
ImageHash: i.Hash,
|
|
ImageAbsent: i.Absent(),
|
|
BlurHash: i.BlurHash,
|
|
ImageWidth: i.Width,
|
|
ImageHeight: i.Height,
|
|
}
|
|
}
|
|
|
|
type ArtworkQueueItem struct {
|
|
ItemKind string `structs:"item_kind"`
|
|
ItemID string `structs:"item_id"`
|
|
ImageType string `structs:"image_type"`
|
|
Priority int `structs:"priority"`
|
|
Attempts int `structs:"attempts"`
|
|
RetryAt time.Time `structs:"retry_at"`
|
|
EnqueuedAt time.Time `structs:"enqueued_at"`
|
|
}
|
|
|
|
// Queue priorities: higher drains first.
|
|
const (
|
|
ArtworkPriorityRecheck = 0
|
|
ArtworkPriorityBackfill = 10
|
|
ArtworkPriorityScan = 50
|
|
ArtworkPriorityBump = 100
|
|
)
|
|
|
|
type ArtworkRepository interface {
|
|
GetImage(hash string) (*Artwork, error)
|
|
PutImage(a *Artwork) error
|
|
GetImages(hashes []string) (map[string]Artwork, error)
|
|
// GetOrphanHashes returns hashes referenced by no item_artwork row and older than cutoff.
|
|
GetOrphanHashes(createdBefore time.Time) ([]string, error)
|
|
// DeleteOrphans deletes the given hashes only if still unreferenced and older than cutoff.
|
|
DeleteOrphans(createdBefore time.Time, hashes []string) error
|
|
GetItemArtwork(kind Kind, id, imageType string) (*ItemArtwork, error)
|
|
PutItemArtwork(ia *ItemArtwork) error
|
|
DeleteForItem(kind Kind, id string) error
|
|
DeleteForItems(kind Kind, ids []string) error
|
|
// GetInfoForItems hydrates a page in one batched query.
|
|
GetInfoForItems(kind Kind, ids []string) (map[string]ItemArtworkInfo, error)
|
|
// GetAllMimes returns hash -> current mime for every stored artwork.
|
|
GetAllMimes() (map[string]string, error)
|
|
// PurgeDanglingItemArtwork removes state rows whose entity no longer exists.
|
|
PurgeDanglingItemArtwork() (int64, error)
|
|
}
|
|
|
|
type ArtworkQueueRepository interface {
|
|
// Enqueue upserts; an existing row keeps the higher priority and has its retry_at reset.
|
|
Enqueue(items ...ArtworkQueueItem) error
|
|
// EnqueueBump upserts like Enqueue but preserves an existing row's retry_at, so a
|
|
// request-triggered read-through never resets a failed resolution's backoff.
|
|
EnqueueBump(items ...ArtworkQueueItem) error
|
|
// EnqueueStaleAbsent inserts queue rows (priority Recheck) for absent states older than cutoff.
|
|
EnqueueStaleAbsent(kind Kind, attemptedBefore time.Time) (int64, error)
|
|
// EnqueueAllMissing inserts queue rows for all entities with no item_artwork row, at the given priority.
|
|
EnqueueAllMissing(kind Kind, priority int) (int64, error)
|
|
// EnqueueIfMissing inserts only for items with no item_artwork row yet.
|
|
EnqueueIfMissing(items ...ArtworkQueueItem) error
|
|
// DequeueBatch returns up to n items with retry_at <= now, priority desc, enqueued_at asc.
|
|
// Restricted to the given kinds when any are passed, so one kind cannot block another's drain.
|
|
DequeueBatch(n int, kinds ...string) ([]ArtworkQueueItem, error)
|
|
// MarkFailedIfUnchanged applies the failure backoff only while retry_at still matches
|
|
// seenRetryAt, so a concurrent re-enqueue keeps its fresh eligibility.
|
|
MarkFailedIfUnchanged(kind, id, imageType string, seenRetryAt, retryAt time.Time) error
|
|
// DeleteIfUnchanged deletes only while retry_at still matches, sparing a concurrent re-enqueue.
|
|
DeleteIfUnchanged(kind, id, imageType string, retryAt time.Time) error
|
|
Count() (int64, error)
|
|
// PurgeDangling removes queue rows whose entity no longer exists.
|
|
PurgeDangling() (int64, error)
|
|
}
|