mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-21 08:47:16 +02:00
The seen map was keyed by the full ArtworkID, which embeds the client token's LastUpdate: every scan bump rotated the key, so the same-bytes dedup (and its cheap version re-persist path) never fired in production and stale entries accumulated forever. The key now zeroes LastUpdate, making the map bounded by entity count and restoring the tested dedup behavior. Cleanups from the same review: the base83 encoder is now exported from the blurhash package and the DTO's duplicate copy is deleted; the always-set blurHashes field lost its test-shaped nil guards; clearIfStored dropped its inert version parameter (cleared rows never have their timestamp read); the teeReader done flag is replaced by nilling the callback; worker-era comments (async, cache-miss recompute) were updated to the inline design; and the e2e specs assert directly instead of polling, since the hash is persisted before the read helpers return.
115 lines
3.8 KiB
Go
115 lines
3.8 KiB
Go
package model
|
|
|
|
import (
|
|
"iter"
|
|
"maps"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/consts"
|
|
)
|
|
|
|
type Artist struct {
|
|
Annotations `structs:"-"`
|
|
|
|
ID string `structs:"id" json:"id"`
|
|
|
|
// Data based on tags
|
|
Name string `structs:"name" json:"name"`
|
|
SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"`
|
|
OrderArtistName string `structs:"order_artist_name" json:"orderArtistName,omitempty"`
|
|
MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty"`
|
|
|
|
// Data calculated from files
|
|
Stats map[Role]ArtistStats `structs:"-" json:"stats,omitempty"`
|
|
Size int64 `structs:"-" json:"size,omitempty"`
|
|
AlbumCount int `structs:"-" json:"albumCount,omitempty"`
|
|
SongCount int `structs:"-" json:"songCount,omitempty"`
|
|
|
|
// Data imported from external sources
|
|
Biography string `structs:"biography" json:"biography,omitempty"`
|
|
SmallImageUrl string `structs:"small_image_url" json:"smallImageUrl,omitempty"`
|
|
MediumImageUrl string `structs:"medium_image_url" json:"mediumImageUrl,omitempty"`
|
|
LargeImageUrl string `structs:"large_image_url" json:"largeImageUrl,omitempty"`
|
|
ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty"`
|
|
SimilarArtists Artists `structs:"similar_artists" json:"-"`
|
|
ExternalInfoUpdatedAt *time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt,omitempty"`
|
|
|
|
Missing bool `structs:"missing" json:"missing"`
|
|
|
|
UploadedImage string `structs:"uploaded_image" json:"uploadedImage,omitempty"`
|
|
|
|
CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"`
|
|
UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
|
|
|
|
BlurHash string `structs:"-" json:"blurHash,omitempty" hash:"ignore"`
|
|
BlurHashUpdatedAt *time.Time `structs:"-" json:"-" hash:"ignore"`
|
|
}
|
|
|
|
type ArtistStats struct {
|
|
SongCount int `json:"songCount"`
|
|
AlbumCount int `json:"albumCount"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func (a Artist) ArtistImageUrl() string {
|
|
if a.LargeImageUrl != "" {
|
|
return a.LargeImageUrl
|
|
}
|
|
if a.MediumImageUrl != "" {
|
|
return a.MediumImageUrl
|
|
}
|
|
return a.SmallImageUrl
|
|
}
|
|
|
|
func (a Artist) CoverArtID() ArtworkID {
|
|
return artworkIDFromArtist(a)
|
|
}
|
|
|
|
// ArtworkUpdatedAt is the artist's artwork version. ExternalInfoUpdatedAt is deliberately
|
|
// excluded: it bumps on every agent TTL refresh even when the image is unchanged, and actual
|
|
// image changes are caught by hashing the served bytes instead.
|
|
func (a Artist) ArtworkUpdatedAt() time.Time {
|
|
if a.UpdatedAt == nil {
|
|
return time.Time{}
|
|
}
|
|
return *a.UpdatedAt
|
|
}
|
|
|
|
func (a Artist) UploadedImagePath() string {
|
|
return UploadedImagePath(consts.EntityArtist, a.UploadedImage)
|
|
}
|
|
|
|
// Roles returns the roles this artist has participated in., based on the Stats field
|
|
func (a Artist) Roles() []Role {
|
|
return slices.Collect(maps.Keys(a.Stats))
|
|
}
|
|
|
|
type Artists []Artist
|
|
|
|
type ArtistIndex struct {
|
|
ID string
|
|
Artists Artists
|
|
}
|
|
type ArtistIndexes []ArtistIndex
|
|
|
|
type ArtistCursor iter.Seq2[Artist, error]
|
|
|
|
type ArtistRepository interface {
|
|
CountAll(options ...QueryOptions) (int64, error)
|
|
Exists(id string) (bool, error)
|
|
Put(m *Artist, colsToUpdate ...string) error
|
|
UpdateExternalInfo(a *Artist) error
|
|
UpdateBlurHash(id string, blurHash string, artworkUpdatedAt time.Time) error
|
|
Get(id string) (*Artist, error)
|
|
GetAll(options ...QueryOptions) (Artists, error)
|
|
GetCursor(options ...QueryOptions) (ArtistCursor, error)
|
|
GetIndex(includeMissing bool, libraryIds []int, roles ...Role) (ArtistIndexes, error)
|
|
|
|
// The following methods are used exclusively by the scanner:
|
|
RefreshPlayCounts() (int64, error)
|
|
RefreshStats(allArtists bool) (int64, error)
|
|
|
|
AnnotatedRepository
|
|
SearchableRepository[Artists]
|
|
}
|