2026-07-21 22:35:34 -04:00
|
|
|
package model
|
|
|
|
|
|
|
|
|
|
import "time"
|
|
|
|
|
|
|
|
|
|
// Artwork is one unique image, identified by the XXH3-64 hash of its bytes.
|
|
|
|
|
type Artwork struct {
|
2026-07-22 15:52:54 -04:00
|
|
|
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"`
|
2026-07-21 22:35:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ImageTypePrimary = "primary"
|
|
|
|
|
|
2026-07-22 20:06:13 -04:00
|
|
|
// ItemImage is per-entity artwork state hydrated at query time; never persisted
|
2026-07-23 19:54:32 -04:00
|
|
|
// (structs:"-" keeps it out of upserts).
|
2026-07-22 20:06:13 -04:00
|
|
|
type ItemImage struct {
|
2026-07-23 19:54:32 -04:00
|
|
|
ImageHash string `structs:"-" json:"imageHash,omitempty"`
|
|
|
|
|
ImageAbsent bool `structs:"-" json:"imageAbsent,omitempty"`
|
|
|
|
|
BlurHash string `structs:"-" json:"blurHash,omitempty"`
|
2026-07-22 20:06:13 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-21 22:35:34 -04:00
|
|
|
// ItemArtwork is an entity's resolved artwork state. Hash=="" means known absent.
|
|
|
|
|
type ItemArtwork struct {
|
2026-07-21 23:24:42 -04:00
|
|
|
ItemKind string `structs:"item_kind"`
|
|
|
|
|
ItemID string `structs:"item_id"`
|
|
|
|
|
ImageType string `structs:"image_type"`
|
|
|
|
|
Hash string `structs:"hash"`
|
|
|
|
|
Source string `structs:"source"`
|
2026-07-22 15:52:54 -04:00
|
|
|
// SourcePath is the backing file (folder/upload: the image; embedded: the audio file); "" otherwise.
|
|
|
|
|
SourcePath string `structs:"source_path"`
|
2026-07-23 09:59:27 -04:00
|
|
|
// RefMtime is SourcePath's mtime (unix-nanoseconds) at resolution; 0 when there is no SourcePath.
|
2026-07-22 15:52:54 -04:00
|
|
|
RefMtime int64 `structs:"ref_mtime"`
|
2026-07-21 23:24:42 -04:00
|
|
|
// attempted_at/updated_at are nullable in the schema but always set by PutItemArtwork;
|
|
|
|
|
// raw inserts must set them too, since these non-pointer time.Time fields fail to scan NULL.
|
2026-07-21 22:35:34 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 23:37:05 -04:00
|
|
|
// Absent reports a known-absent artwork state (resolved, no image).
|
|
|
|
|
func (i ItemArtworkInfo) Absent() bool { return i.Hash == "" }
|
|
|
|
|
|
2026-07-21 22:35:34 -04:00
|
|
|
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 {
|
2026-07-21 23:14:01 -04:00
|
|
|
// Image identity (artwork table)
|
|
|
|
|
GetImage(hash string) (*Artwork, error)
|
|
|
|
|
PutImage(a *Artwork) error
|
|
|
|
|
GetImages(hashes []string) (map[string]Artwork, error)
|
2026-07-21 23:24:42 -04:00
|
|
|
// GetOrphanHashes returns hashes referenced by no item_artwork row and older than cutoff.
|
2026-07-21 22:35:34 -04:00
|
|
|
GetOrphanHashes(createdBefore time.Time) ([]string, error)
|
2026-07-22 00:11:15 -04:00
|
|
|
// DeleteOrphans deletes the given hashes only if still unreferenced and older than cutoff (atomic re-check).
|
|
|
|
|
DeleteOrphans(createdBefore time.Time, hashes []string) error
|
2026-07-21 23:14:01 -04:00
|
|
|
// Per-item state (item_artwork table)
|
refactor(artwork): thread model.Kind through the artwork API
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.
2026-07-24 16:58:23 -04:00
|
|
|
GetItemArtwork(kind Kind, id, imageType string) (*ItemArtwork, error)
|
2026-07-21 23:14:01 -04:00
|
|
|
PutItemArtwork(ia *ItemArtwork) error
|
refactor(artwork): thread model.Kind through the artwork API
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.
2026-07-24 16:58:23 -04:00
|
|
|
DeleteForItem(kind Kind, id string) error
|
2026-07-22 20:40:26 -04:00
|
|
|
// DeleteForItems removes state rows for the given ids of one kind, in chunks.
|
refactor(artwork): thread model.Kind through the artwork API
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.
2026-07-24 16:58:23 -04:00
|
|
|
DeleteForItems(kind Kind, ids []string) error
|
2026-07-21 23:24:42 -04:00
|
|
|
// GetInfoForItems hydrates a page: one batched query, item_artwork joined to artwork.
|
refactor(artwork): thread model.Kind through the artwork API
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.
2026-07-24 16:58:23 -04:00
|
|
|
GetInfoForItems(kind Kind, ids []string) (map[string]ItemArtworkInfo, error)
|
2026-07-22 00:28:51 -04:00
|
|
|
// GetAllMimes returns hash -> current mime for every stored artwork, for sweep retention checks.
|
|
|
|
|
GetAllMimes() (map[string]string, error)
|
2026-07-22 10:21:48 -04:00
|
|
|
// PurgeDanglingItemArtwork removes state rows whose entity no longer exists.
|
|
|
|
|
PurgeDanglingItemArtwork() (int64, error)
|
2026-07-21 22:35:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ArtworkQueueRepository interface {
|
2026-07-23 00:26:46 -04:00
|
|
|
// Enqueue upserts; an existing row keeps the higher of the two priorities and has its
|
|
|
|
|
// retry_at reset (a detected change wants immediate re-resolution).
|
2026-07-21 22:35:34 -04:00
|
|
|
Enqueue(items ...ArtworkQueueItem) error
|
2026-07-23 00:26:46 -04:00
|
|
|
// 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
|
2026-07-21 22:35:34 -04:00
|
|
|
// DequeueBatch returns up to n items with retry_at <= now, priority desc, enqueued_at asc.
|
2026-07-25 13:32:07 -04:00
|
|
|
// Restricted to the given item kinds when any are passed, so a drain pool sees only its own
|
|
|
|
|
// work and cannot be held up behind another kind's backlog.
|
|
|
|
|
DequeueBatch(n int, kinds ...string) ([]ArtworkQueueItem, error)
|
2026-07-21 22:35:34 -04:00
|
|
|
// MarkFailed increments attempts and pushes retry_at into the future.
|
|
|
|
|
MarkFailed(kind, id, imageType string, retryAt time.Time) error
|
2026-07-22 14:02:52 -04:00
|
|
|
// MarkFailedIfUnchanged applies the failure backoff only while retry_at still matches
|
|
|
|
|
// seenRetryAt; a concurrent re-enqueue (which resets retry_at) keeps its fresh eligibility.
|
|
|
|
|
MarkFailedIfUnchanged(kind, id, imageType string, seenRetryAt, retryAt time.Time) error
|
2026-07-21 22:35:34 -04:00
|
|
|
Delete(kind, id, imageType string) error
|
2026-07-22 11:57:40 -04:00
|
|
|
// DeleteIfUnchanged deletes the row only if its retry_at still matches retryAt, so a
|
|
|
|
|
// concurrent re-enqueue (which resets retry_at) survives instead of being erased.
|
|
|
|
|
DeleteIfUnchanged(kind, id, imageType string, retryAt time.Time) error
|
2026-07-21 22:35:34 -04:00
|
|
|
Count() (int64, error)
|
2026-07-21 23:37:05 -04:00
|
|
|
// EnqueueStaleAbsent inserts queue rows (priority Recheck) for absent states older than cutoff.
|
refactor(artwork): thread model.Kind through the artwork API
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.
2026-07-24 16:58:23 -04:00
|
|
|
EnqueueStaleAbsent(kind Kind, attemptedBefore time.Time) (int64, error)
|
2026-07-24 20:42:14 -04:00
|
|
|
// EnqueueMissing inserts queue rows (priority Recheck) for entities of the kind that have no
|
|
|
|
|
// item_artwork row at all, so a never-processed entity is eventually resolved even without a scan.
|
|
|
|
|
EnqueueMissing(kind Kind) (int64, error)
|
2026-07-22 11:57:40 -04:00
|
|
|
// PurgeDangling removes queue rows whose entity no longer exists.
|
|
|
|
|
PurgeDangling() (int64, error)
|
2026-07-21 22:35:34 -04:00
|
|
|
}
|