mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
* refactor(persistence): adopt generic deluan/rest repository API Pin deluan/rest to the refactor branch. REST-facing repository methods take a context and return typed values. Drop DataStore.Resource and ResourceRepository; the native API names typed repositories directly through a per-request adapter that later commits remove. * refactor(persistence): base repository helpers take a context * refactor(persistence): LibraryRepository takes a context per call * refactor(persistence): PropertyRepository takes a context per call * refactor(persistence): UserPropsRepository takes a context per call * refactor(persistence): TranscodingRepository takes a context per call * refactor(persistence): ShareRepository takes a context per call * refactor(persistence): PlayerRepository takes a context per call * refactor(persistence): RadioRepository takes a context per call * refactor(persistence): PlayQueueRepository takes a context per call * refactor(persistence): Tag and Genre repositories take a context per call * refactor(persistence): PluginRepository takes a context per call * refactor(persistence): Scrobble repositories take a context per call * refactor(persistence): FolderRepository takes a context per call * refactor(persistence): Artwork repositories take a context per call * refactor(persistence): UserRepository takes a context per call * refactor(persistence): ArtistRepository takes a context per call ReadAll no longer rewrites the shared sort mappings for the role filter; it works on a per-call copy. * test(persistence): assert artist role sort sanitization in ReadAll * refactor(persistence): AlbumRepository takes a context per call * test(persistence): pass the test context to album repository helpers * refactor(persistence): MediaFileRepository takes a context per call * refactor(persistence): Playlist repositories take a context per call * refactor(persistence): build all repositories once per store * refactor(core): REST repository wrappers are built once * refactor(persistence): repositories are stateless Remove the context field from the base repository and the per-request REST adapter. Enable the containedctx linter so no repository can hold a request context again. * chore(lint): skip containedctx in test files * refactor: share simplifications from the stateless repositories sweep Add deleteOwnedAll on sqlRepository and use it in player/share Delete to remove the duplicated bulk-delete loop; have Share.Repository() return model.ShareRepository so subsonic sharing.go drops its repeated type assertions. * chore(core): assert REST wrappers implement Persistable * chore: reformat imports * perf(persistence): build repositories on first use Each transaction store used to construct all 21 repositories up front, paying for filter and sort mapping setup the block never touched. Fields are now sync.OnceValue thunks, so a store only builds what it uses. * fix(persistence): clean plugin references per deleted user A bulk user delete that fails on a later id had already removed the earlier rows but skipped their plugin cleanup. Cleanup now runs right after each successful delete. * fix(core): unload disabled plugins even when a user delete fails A bulk delete can fail on a later id after earlier users were removed and their plugins auto-disabled. The wrapper returned before unloading, leaving those plugins running until the next successful delete or a restart. * chore(deps): pin deluan/rest to v1.0.1 Replaces the pseudo-version of the refactor branch with the tagged release. REST error messages now name the bare type (Artist, not model.Artist). * test: use the spec context instead of context.Background() Replace the context.Background()/context.TODO() calls this branch added to tests with the spec's ctx, GinkgoT().Context(), or t/b.Context(), so repository calls are bound to the running spec's lifetime. * test: declare the spec context once per Describe Set ctx from GinkgoT().Context() first in each top-level BeforeEach and reuse it, building user contexts on top of it instead of repeating inline calls.
182 lines
8.8 KiB
Go
182 lines
8.8 KiB
Go
package model
|
|
|
|
import (
|
|
"context"
|
|
"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"`
|
|
ThumbHash string `structs:"thumb_hash"`
|
|
// DominantColor is "#rrggbb": a flat placeholder clients can paint before any decode.
|
|
DominantColor string `structs:"dominant_color"`
|
|
CreatedAt time.Time `structs:"created_at"`
|
|
}
|
|
|
|
const ImageTypePrimary = "primary"
|
|
|
|
// ArtworkSourceFailed is a pseudo-source selecting absent states that exhausted the retry budget
|
|
// rather than being answered. The "!" keeps it from colliding with a stored source value.
|
|
const ArtworkSourceFailed = "!failed"
|
|
|
|
// 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 is Jellyfin's; its mappers read this field directly, so it stays off native JSON.
|
|
BlurHash string `structs:"-" json:"-"`
|
|
ThumbHash string `structs:"-" json:"thumbHash,omitempty"`
|
|
// DominantColor is the only placeholder needing no decode, so it can paint on the first frame.
|
|
DominantColor string `structs:"-" json:"dominantColor,omitempty"`
|
|
// A thumbhash's own aspect is quantised, so clients need these to shape the placeholder exactly.
|
|
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"`
|
|
// Trace is the encoded walk that produced this state; LastFailure is the walk of the attempt
|
|
// that exhausted the retry budget. Both are JSON, read back with artwork.DecodeTrace.
|
|
Trace string `structs:"trace"`
|
|
LastFailure string `structs:"last_failure"`
|
|
// 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
|
|
ThumbHash string
|
|
DominantColor 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,
|
|
ThumbHash: i.ThumbHash,
|
|
DominantColor: i.DominantColor,
|
|
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 is the earliest time the drain may take this row, not when it will run.
|
|
RetryAt time.Time `structs:"retry_at"`
|
|
EnqueuedAt time.Time `structs:"enqueued_at"`
|
|
// Trace is why the last attempt failed. Only Get reads it; the drain projects it away.
|
|
Trace string `structs:"trace"`
|
|
}
|
|
|
|
// Queue priorities: higher drains first.
|
|
const (
|
|
ArtworkPriorityRecheck = 0
|
|
// ArtworkPriorityBackfill sits between the hourly sweep and scan-driven work. Nothing enqueues
|
|
// it today; it stays named so a row still carrying it can be reported and cancelled.
|
|
ArtworkPriorityBackfill = 10
|
|
ArtworkPriorityScan = 50
|
|
ArtworkPriorityBump = 100
|
|
)
|
|
|
|
// Delete* takes the rows to remove; Purge* finds them itself and reports how many went.
|
|
type ArtworkRepository interface {
|
|
GetImage(ctx context.Context, hash string) (*Artwork, error)
|
|
PutImage(ctx context.Context, a *Artwork) error
|
|
// PurgeOrphans deletes rows referenced by no item_artwork row and older than cutoff.
|
|
PurgeOrphans(ctx context.Context, createdBefore time.Time) (int64, error)
|
|
GetItemArtwork(ctx context.Context, kind Kind, id, imageType string) (*ItemArtwork, error)
|
|
PutItemArtwork(ctx context.Context, ia *ItemArtwork) error
|
|
// PutLastFailure records the trace of the attempt that exhausted the retry budget.
|
|
PutLastFailure(ctx context.Context, kind Kind, id, imageType, trace string) error
|
|
DeleteForItems(ctx context.Context, kind Kind, ids []string) error
|
|
// GetInfoForItems hydrates a page in one batched query.
|
|
GetInfoForItems(ctx context.Context, kind Kind, ids []string) (map[string]ItemArtworkInfo, error)
|
|
// GetMimeByHash returns hash -> current mime for every stored artwork.
|
|
GetMimeByHash(ctx context.Context) (map[string]string, error)
|
|
// PurgeDanglingItems removes state rows whose entity no longer exists.
|
|
PurgeDanglingItems(ctx context.Context) (int64, error)
|
|
}
|
|
|
|
type ArtworkQueueRepository interface {
|
|
// Get returns the pending row for an item, or ErrNotFound when it is not queued.
|
|
Get(ctx context.Context, kind Kind, id, imageType string) (*ArtworkQueueItem, error)
|
|
// Enqueue upserts; an existing row keeps the higher priority and has its retry_at reset.
|
|
Enqueue(ctx context.Context, items ...ArtworkQueueItem) error
|
|
// EnqueuePreservingBackoff upserts like Enqueue but preserves an existing row's retry_at, so a
|
|
// request-triggered read-through never resets a failed resolution's backoff.
|
|
EnqueuePreservingBackoff(ctx context.Context, items ...ArtworkQueueItem) error
|
|
// EnqueueAllMissing inserts queue rows for all entities with no item_artwork row, at the given priority.
|
|
EnqueueAllMissing(ctx context.Context, kind Kind, priority int) (int64, error)
|
|
// EnqueueIfMissing inserts only for items with no item_artwork row yet.
|
|
EnqueueIfMissing(ctx context.Context, items ...ArtworkQueueItem) error
|
|
// CountBySource reports how many items of a kind currently resolve from the given sources.
|
|
// An empty sources slice means every source; "" matches absent state, and the pseudo-source
|
|
// ArtworkSourceFailed matches the absent states that gave up.
|
|
CountBySource(ctx context.Context, kind Kind, sources []string) (int64, error)
|
|
// SourcesInUse lists the distinct sources items of a kind currently resolve from, "" included.
|
|
SourcesInUse(ctx context.Context, kind Kind) ([]string, error)
|
|
// EnqueueBySource inserts queue rows for items of a kind whose current source matches.
|
|
// It does not clear existing artwork state: the current image stays until it is replaced.
|
|
EnqueueBySource(ctx context.Context, kind Kind, sources []string, priority int) (int64, 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(ctx context.Context, 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(ctx context.Context, kind, id, imageType string, seenRetryAt, retryAt time.Time, trace string) error
|
|
// DeleteIfUnchanged deletes only while retry_at still matches, sparing a concurrent re-enqueue.
|
|
DeleteIfUnchanged(ctx context.Context, kind, id, imageType string, retryAt time.Time) error
|
|
Count(ctx context.Context) (int64, error)
|
|
// CountQueued reports the pending rows matching the kinds and priorities, grouped by both;
|
|
// an empty filter means every one.
|
|
CountQueued(ctx context.Context, kinds []Kind, priorities []int) ([]ArtworkQueueStat, error)
|
|
// PurgeDangling removes queue rows whose entity no longer exists.
|
|
PurgeDangling(ctx context.Context) (int64, error)
|
|
// PurgeQueued removes pending rows matching the kinds and priorities; an empty filter means every one.
|
|
PurgeQueued(ctx context.Context, kinds []Kind, priorities []int) (int64, error)
|
|
}
|
|
|
|
type ArtworkQueueStat struct {
|
|
ItemKind string
|
|
Priority int
|
|
Count int64
|
|
}
|