* feat(metadata): expand provider image cache queue
* fix(metadata): harden provider image cache queue
Addresses bug-review feedback from Codex/CodeRabbit on the metadata image
cache pipeline. All findings validated against the code before fixing;
false positives (rows/connection deadlock, PhotoSourcePath merge coupling)
were confirmed non-issues and left unchanged.
- Honor metadata.cache_images for the background processor. The
cache_metadata_images task was registered whenever S3 was configured,
so merely enabling object storage downloaded the entire provider-artwork
catalog even with caching disabled. Add ImageCacheProcessor.SetEnabled,
gate RunOnce/RunUntilIdle on it, and wire it (with hot reload) from
cfg.Metadata.CacheImages in main.go.
- Guard terminal job updates with lease ownership. EnqueueBatch can
repurpose a running row with a new source; MarkSucceeded/MarkFailed
keyed on id alone let a stale worker finalize the replacement job and
drop the new artwork. Thread locked_by through and add
status='running' AND locked_by=$n guards.
- Avoid uploading stale jobs onto the live artwork key. Verify the
target still references the job's source (CurrentTargetSourcePath)
before CacheImage, so a job whose source an admin/refresh already
replaced cannot overwrite the deterministic storage object.
- COALESCE nullable external IDs in EnqueueExistingProviderArtwork. A
NULL tmdb_id/tvdb_id/imdb_id on any candidate failed the scan and
aborted the whole cache run; matches the existing item_repo pattern.
- Stop re-downloading the catalog every 30 days. Discovery now skips
targets whose *_path is already a cached relative path, making the
cached row the durable dedup marker instead of the prunable job row.
- Decouple catalog sweeps from queue draining. RunOnce no longer runs
discovery per batch; RunUntilIdle sweeps only when the queue drains and
throttles full sweeps to every 15m, so idle installs stop full-scanning
every entity table each minute.
- Requeue claimed-but-unstarted jobs on cancellation. Acquire the
semaphore before spawning workers and RequeueClaimed any jobs not yet
started, instead of leaving them locked until the 15m lease expires.
- Skip the backoff sleep after the final upload attempt in
putObjectWithRetry (saves ~1.5s on permanent failures).
- Add the s3/file/local/upload/generated exclusion to the seasons and
episodes backfill in migration 20260617184537 for consistency with the
later migration (the bad backfill was inert downstream, but the
asymmetry is removed).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>