* 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>
114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
package scanner
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/catalog"
|
|
)
|
|
|
|
const audiobookCoverTestThumbhash = "thumb"
|
|
|
|
type fakeAudiobookCoverCacher struct {
|
|
calls int
|
|
data []byte
|
|
contentID string
|
|
err error
|
|
}
|
|
|
|
func (f *fakeAudiobookCoverCacher) CacheAudiobookCover(_ context.Context, data []byte, contentID string) (string, string, error) {
|
|
f.calls++
|
|
f.data = append([]byte(nil), data...)
|
|
f.contentID = contentID
|
|
if f.err != nil {
|
|
return "", "", f.err
|
|
}
|
|
return "local/audiobooks/" + contentID + "/poster/original.test-revision.webp", audiobookCoverTestThumbhash, nil
|
|
}
|
|
|
|
type fakeAudiobookCoverStore struct {
|
|
posterPath string
|
|
getErr error
|
|
contentID string
|
|
update *catalog.MetadataUpdate
|
|
err error
|
|
}
|
|
|
|
func (f *fakeAudiobookCoverStore) GetPosterPath(_ context.Context, _ string) (string, error) {
|
|
return f.posterPath, f.getErr
|
|
}
|
|
|
|
func (f *fakeAudiobookCoverStore) UpdateMetadata(_ context.Context, contentID string, update *catalog.MetadataUpdate) error {
|
|
f.contentID = contentID
|
|
f.update = update
|
|
return f.err
|
|
}
|
|
|
|
func TestApplyAudiobookSidecarCoverCachesFolderCover(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "folder.png"), []byte("audiobook-sidecar-cover"), 0o644); err != nil {
|
|
t.Fatalf("write cover: %v", err)
|
|
}
|
|
cacher := &fakeAudiobookCoverCacher{}
|
|
store := &fakeAudiobookCoverStore{}
|
|
|
|
err := applyAudiobookSidecarCover(context.Background(), store, cacher, "content-1", dir)
|
|
if err != nil {
|
|
t.Fatalf("applyAudiobookSidecarCover: %v", err)
|
|
}
|
|
|
|
if cacher.calls != 1 || string(cacher.data) != "audiobook-sidecar-cover" {
|
|
t.Fatalf("cache call = calls %d data %q", cacher.calls, string(cacher.data))
|
|
}
|
|
if store.update == nil || store.update.PosterPath == nil || *store.update.PosterPath != "local/audiobooks/content-1/poster/original.test-revision.webp" {
|
|
t.Fatalf("poster update = %#v", store.update)
|
|
}
|
|
if store.update.PosterThumbhash == nil || *store.update.PosterThumbhash != "thumb" {
|
|
t.Fatalf("poster thumbhash = %#v", store.update.PosterThumbhash)
|
|
}
|
|
}
|
|
|
|
func TestApplyAudiobookSidecarCoverPreservesExistingPoster(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), []byte("audiobook-sidecar-cover"), 0o644); err != nil {
|
|
t.Fatalf("write cover: %v", err)
|
|
}
|
|
cacher := &fakeAudiobookCoverCacher{}
|
|
store := &fakeAudiobookCoverStore{
|
|
posterPath: "provider/poster.webp",
|
|
}
|
|
|
|
err := applyAudiobookSidecarCover(context.Background(), store, cacher, "content-1", dir)
|
|
if err != nil {
|
|
t.Fatalf("applyAudiobookSidecarCover: %v", err)
|
|
}
|
|
|
|
if cacher.calls != 0 {
|
|
t.Fatalf("cache calls = %d, want 0", cacher.calls)
|
|
}
|
|
if store.update != nil {
|
|
t.Fatalf("unexpected update = %#v", store.update)
|
|
}
|
|
}
|
|
|
|
func TestFindSidecarAudiobookCoverSkipsSymlinks(t *testing.T) {
|
|
dir := t.TempDir()
|
|
target := filepath.Join(dir, "secret.jpg")
|
|
if err := os.WriteFile(target, []byte("not-a-cover"), 0o644); err != nil {
|
|
t.Fatalf("write target: %v", err)
|
|
}
|
|
if err := os.Symlink(target, filepath.Join(dir, "cover.jpg")); err != nil {
|
|
t.Fatalf("symlink cover: %v", err)
|
|
}
|
|
|
|
data, path, err := findSidecarAudiobookCover(dir)
|
|
if err != nil {
|
|
t.Fatalf("findSidecarAudiobookCover: %v", err)
|
|
}
|
|
if data != nil || path != "" {
|
|
t.Fatalf("findSidecarAudiobookCover returned data %q path %q, want no cover", string(data), path)
|
|
}
|
|
}
|