* 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>
117 lines
3.2 KiB
Go
117 lines
3.2 KiB
Go
package catalog
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
"github.com/Silo-Server/silo-server/internal/titleutil"
|
|
)
|
|
|
|
// MetadataUpdate contains the fields that can be updated on a media item,
|
|
// season, or episode. Nil pointer fields are skipped (not updated).
|
|
type MetadataUpdate struct {
|
|
Title *string
|
|
SortTitle *string
|
|
OriginalTitle *string
|
|
Overview *string
|
|
Tagline *string
|
|
ContentRating *string
|
|
Year *int
|
|
Runtime *int
|
|
Genres *[]string
|
|
Studios *[]string
|
|
Networks *[]string
|
|
Countries *[]string
|
|
ReleaseDate *string
|
|
FirstAirDate *string
|
|
LastAirDate *string
|
|
AirTime *string
|
|
AirTimezone *string
|
|
AirDate *string
|
|
Status *string
|
|
ShowStatus *string
|
|
RatingIMDB *float64
|
|
RatingTMDB *float64
|
|
RatingRTCritic *int
|
|
RatingRTAudience *int
|
|
ImdbID *string
|
|
TmdbID *string
|
|
TvdbID *string
|
|
SeasonNumber *int
|
|
EpisodeNumber *int
|
|
LockedFields *[]int
|
|
PosterPath *string
|
|
PosterSourcePath *string
|
|
PosterThumbhash *string
|
|
BackdropPath *string
|
|
BackdropSourcePath *string
|
|
BackdropThumbhash *string
|
|
LogoPath *string
|
|
LogoSourcePath *string
|
|
StillPath *string
|
|
StillSourcePath *string
|
|
StillThumbhash *string
|
|
}
|
|
|
|
// UpdateMediaItemMetadata updates specific metadata fields on a media_items row.
|
|
func (s *DetailService) UpdateMediaItemMetadata(ctx context.Context, contentID string, upd *MetadataUpdate) error {
|
|
if err := applyDefaultSortTitleOnAdminUpdate(ctx, s.itemRepo, contentID, upd); err != nil {
|
|
return err
|
|
}
|
|
return s.itemRepo.UpdateMetadata(ctx, contentID, upd)
|
|
}
|
|
|
|
// UpdateSeasonMetadata updates specific metadata fields on a seasons row.
|
|
func (s *DetailService) UpdateSeasonMetadata(ctx context.Context, contentID string, upd *MetadataUpdate) error {
|
|
return s.seasonRepo.UpdateMetadata(ctx, contentID, upd)
|
|
}
|
|
|
|
// UpdateEpisodeMetadata updates specific metadata fields on an episodes row.
|
|
func (s *DetailService) UpdateEpisodeMetadata(ctx context.Context, contentID string, upd *MetadataUpdate) error {
|
|
return s.episodeRepo.UpdateMetadata(ctx, contentID, upd)
|
|
}
|
|
|
|
func applyDefaultSortTitleOnAdminUpdate(
|
|
ctx context.Context,
|
|
itemRepo *ItemRepository,
|
|
contentID string,
|
|
upd *MetadataUpdate,
|
|
) error {
|
|
if upd.Title == nil || upd.SortTitle != nil {
|
|
return nil
|
|
}
|
|
|
|
item, err := itemRepo.GetByID(ctx, contentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if item == nil {
|
|
return nil
|
|
}
|
|
if titleLockedForAdminUpdate(item, upd) {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(item.SortTitle) != "" {
|
|
return nil
|
|
}
|
|
|
|
derived := titleutil.DeriveDefaultSortTitle(*upd.Title)
|
|
if derived == "" {
|
|
return nil
|
|
}
|
|
upd.SortTitle = &derived
|
|
return nil
|
|
}
|
|
|
|
// fieldNameLocked matches metadata.FieldName and EditMetadataDialog FIELD_NAME.
|
|
const fieldNameLocked = 0
|
|
|
|
func titleLockedForAdminUpdate(item *models.MediaItem, upd *MetadataUpdate) bool {
|
|
if upd.LockedFields != nil {
|
|
return slices.Contains(*upd.LockedFields, fieldNameLocked)
|
|
}
|
|
return slices.Contains(item.LockedFields, fieldNameLocked)
|
|
}
|