* fix(metadata): publish artwork revisions atomically * fix(metadata): harden artwork revision cleanup * fix(metadata): address artwork revision review findings - restore image applies for all media_items types and reject unsupported target/image combinations with 400 before uploading; episodes coerce to stills and the web dialog no longer offers image tabs episodes can't use - add WHEN clauses to displacement triggers and hoist to_jsonb so bulk catalog upserts that assign unchanged artwork columns skip the trigger - make artworkkey the single variant-ladder owner: imagecache derives its widths from it and triggers store image_type instead of hardcoded variant arrays, expanded by the collector at deletion time - sweep dormant registry rows periodically so references lost through untriggered surfaces degrade to slow cleanup instead of leaking - park just-published revisions dormant, keep dormant rows dormant on re-cache, and batch the GC reference pre-check per run - heal rows re-referencing a just-deleted revision via reconciler-style resets after the deletion commits - share a per-URL image-loaded hook across DetailHero, ItemCard, SectionItemCard, GlobalSearch, and CollectionPosterCard - deduplicate Cache/CacheBytes finalization and drop unused VariantPaths plumbing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(catalog): cast reused timestamp parameter in revision upsert Postgres cannot deduce one type for $3 used both as a plain value and inside a CASE arm; the dev deploy surfaced it as SQLSTATE 42P08 on every publication. Cast both uses and cover the arm/park/track upserts with database-backed tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(metadata): address artwork revision review comments - keep a durable heal path: deletion marks deleted_at instead of removing the registry row, so a failed post-delete heal retries with backoff and broken references never park; trackers clear the marker on re-upload - never treat bare existence as an immutable-content match; backends without content verification rewrite the object - exercise revisioned cover keys in scanner/enrichment fakes, compare the tracked manifest exactly, and honor cancellation in the blocking test deleter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
// Package artworkkey owns the object-key naming contract for cached artwork.
|
|
// Legacy names such as original.webp and revisioned names such as
|
|
// original.<revision>.webp are both supported.
|
|
package artworkkey
|
|
|
|
import (
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const OriginalVariant = "original"
|
|
|
|
// Build returns an object key for a variant under basePath.
|
|
func Build(basePath, variant, revision, ext string) string {
|
|
basePath = strings.TrimRight(strings.TrimSpace(basePath), "/")
|
|
variant = strings.TrimSpace(variant)
|
|
revision = strings.TrimSpace(revision)
|
|
if basePath == "" || variant == "" {
|
|
return ""
|
|
}
|
|
if ext == "" {
|
|
ext = ".webp"
|
|
} else if !strings.HasPrefix(ext, ".") {
|
|
ext = "." + ext
|
|
}
|
|
if revision == "" {
|
|
return basePath + "/" + variant + ext
|
|
}
|
|
return basePath + "/" + variant + "." + revision + ext
|
|
}
|
|
|
|
// Original returns the original-variant key under basePath.
|
|
func Original(basePath, revision, ext string) string {
|
|
return Build(basePath, OriginalVariant, revision, ext)
|
|
}
|
|
|
|
// Variant rewrites an original key to another variant while retaining any
|
|
// revision and extension. Unrecognized paths pass through unchanged.
|
|
func Variant(originalPath, variant string) string {
|
|
if originalPath == "" || variant == "" || variant == OriginalVariant {
|
|
return originalPath
|
|
}
|
|
dir := path.Dir(originalPath)
|
|
base := path.Base(originalPath)
|
|
if dir == "." || !strings.HasPrefix(base, OriginalVariant+".") {
|
|
return originalPath
|
|
}
|
|
return strings.TrimRight(dir, "/") + "/" + variant + strings.TrimPrefix(base, OriginalVariant)
|
|
}
|
|
|
|
// Directory returns the image-type prefix containing every revision and
|
|
// variant for an artwork key, including a trailing slash.
|
|
func Directory(objectPath string) string {
|
|
objectPath = strings.TrimSpace(objectPath)
|
|
if objectPath == "" || strings.Contains(objectPath, "://") {
|
|
return ""
|
|
}
|
|
dir := path.Dir(objectPath)
|
|
if dir == "." || dir == "/" {
|
|
return ""
|
|
}
|
|
return strings.TrimRight(dir, "/") + "/"
|
|
}
|
|
|
|
// Revision extracts the content revision from a revisioned key. Legacy keys
|
|
// return an empty string.
|
|
func Revision(objectPath string) string {
|
|
name := path.Base(strings.TrimSpace(objectPath))
|
|
ext := path.Ext(name)
|
|
stem := strings.TrimSuffix(name, ext)
|
|
firstDot := strings.IndexByte(stem, '.')
|
|
if firstDot < 0 || firstDot == len(stem)-1 {
|
|
return ""
|
|
}
|
|
return stem[firstDot+1:]
|
|
}
|
|
|
|
// VariantWidths returns the resize widths generated for an artwork type. This
|
|
// is the single source of truth for the variant ladder: image generation,
|
|
// object-key expansion, and garbage collection all derive from it.
|
|
func VariantWidths(imageType string) []int {
|
|
switch strings.ToLower(strings.TrimSpace(imageType)) {
|
|
case "backdrop":
|
|
return []int{1920, 1280, 300}
|
|
case "logo":
|
|
return []int{500}
|
|
default: // poster, still, profile
|
|
return []int{500, 300}
|
|
}
|
|
}
|
|
|
|
// VariantNames returns the cached variants generated for an artwork type.
|
|
func VariantNames(imageType string) []string {
|
|
widths := VariantWidths(imageType)
|
|
names := make([]string, 0, len(widths)+1)
|
|
names = append(names, OriginalVariant)
|
|
for _, width := range widths {
|
|
names = append(names, "w"+strconv.Itoa(width))
|
|
}
|
|
return names
|
|
}
|
|
|
|
// ObjectKeys expands an original key to every expected key for its image type.
|
|
func ObjectKeys(originalPath, imageType string) []string {
|
|
if originalPath == "" || strings.Contains(originalPath, "://") {
|
|
return nil
|
|
}
|
|
names := VariantNames(imageType)
|
|
keys := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
keys = append(keys, Variant(originalPath, name))
|
|
}
|
|
return keys
|
|
}
|