Fix enrichment batch starvation, image collisions, best-image selection (#343) - #355
Merged
Merged
Conversation
…e selection, duplicate registration Implements #343 (part of epic #322). - Batch starvation: EnrichArtists/EnrichAlbums/EnrichTracks queries now order by last_enriched_at (NULLs first, ID tie-break) via a dialect-portable boolean sort key, so Limit(100/200) batches rotate through the whole library instead of re-selecting the same first rows whose Or-predicates (e.g. LidarrIDIsNil) still match after enrichment. - Image filename collisions: downloadArtistImage/downloadAlbumImage filenames now include a per-image URL-hash discriminator ({id}-{type}-{urlhash8}{ext}), so N same-type images store N distinct files instead of collapsing onto one via the os.Stat exists-branch. Governing: ADR-0027. - REQ-ENRICH-022: artist/album image serving now ranks candidates by IsPrimary -> Likes -> Width*Height (ID as deterministic tie-break), skipping records without a downloaded local_path (issue #127 behavior preserved). - REQ-ENRICH-050: Registry.Register (and MetadataService.Register) now return an error on duplicate enricher type registration instead of silently overwriting; cmd/server/main.go registration exits on registration failure. Governing: ADR-0015. All new behaviors are covered by regression tests that fail against the previous implementation (starvation rotation, distinct files for same-type images, REQ-ENRICH-022 ranking, duplicate-registration error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
Independent review — verdict: LGTMAdversarial review against issue #343, ADR-0015, ADR-0027, and SPEC metadata-enrichment-pipeline (REQ-ENRICH-022/040/050). I re-ran Verified
Findings (all non-blocking)
Nice work on the honest test evidence (verifying the regression tests fail pre-fix) and on the deferred-doc-updates list — items 1 above overlaps with the ADR-0027 update already flagged there. 🤖 Posted on behalf of |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / Why
Implements story #343 (part of epic #322). Governing artifacts: ADR-0015, ADR-0027, SPEC metadata-enrichment-pipeline REQ-ENRICH-022 / REQ-ENRICH-050.
Batch starvation (
internal/services/metadata.go)The artist/album/track enrichment queries used unordered
Limit(100)(tracks: 200) with Or-predicates (LidarrIDIsNil,LastAiEnrichedAt*) that still match just-enriched rows, so the same first-limit rows re-filled every batch and rows beyond the limit were never enriched. The queries nowOrderbylast_enriched_atascending with NULLs first and ID as tie-break (byLastEnrichedAtNullsFirst). Since every processed row gets itslast_enriched_atbumped, successive ticks rotate through the entire library. The NULL grouping is expressed as a portable boolean sort key (col IS NOT NULLASC) because Postgres sorts NULLs last on ASC and MySQL lacksNULLS FIRST.Image filename collisions (
internal/services/metadata.go)downloadArtistImage/downloadAlbumImagenamed files{id}-{imageType}{ext}while rows dedupe by URL, so N same-type images collapsed onto one file via theos.Statexists-branch (all DB rows pointed at image #1's bytes). Filenames now include a per-image discriminator — an 8-hex-char SHA-256 hash of the URL:{id}-{imageType}-{urlhash}{ext}. Same-URL re-downloads still hit the exists-branch (REQ-ENRICH-033 skip behavior preserved). Note on existing data: rows already collapsed under the old naming are NOT automatically re-fetched on persistent-volume deployments —DownloadImagesonly selects rows with an emptylocal_path, andrepairStaleImagePathsonly fires when the file is missing from disk. Previously-collapsed rows are re-downloaded (with distinct filenames) only if the data directory is lost or the file otherwise goes missing. A one-shot backfill that clearslocal_pathfor rows sharing a file is possible follow-up work.REQ-ENRICH-022 best-image selection (
internal/handlers/images.go)ArtistImage/AlbumImageserving previously ignored the storedLikesand dimensions. Selection now ranks per spec:IsPrimary→ highestLikes→ largestWidth × Height, with ID as a deterministic final tie-break, skipping records with no downloadedlocal_path(issue #127 behavior preserved). Album images carry noLikescolumn, so their ranking is IsPrimary → dimensions.REQ-ENRICH-050 duplicate registration (
internal/enrichers/enrichers.go)Registry.Registersilently overwrote an existing factory. It now returns an error on duplicate type registration and keeps the original factory;MetadataService.Registerpropagates it.cmd/server/main.goregistration was minimally adapted (mustRegisterEnricherclosure that logs and exits) — note a sibling story (#324) also owns that file; the edit is confined to the metadata enricher registration block.Test evidence
make test— all packages pass;gofmt -lclean;golangci-lint(v1.64.8, repo config) clean on the four changed packages.origin/main'smetadata.go):TestEnrichArtists_NoBatchStarvation— 250 never-matchable artists (fixture per acceptance criteria) all enriched across 3 ticks atLimit(100); plusTestEnrichArtists_BatchesRotate,TestEnrichAlbums_NoBatchStarvation,TestEnrichTracks_NoBatchStarvation(Limit 200).TestDownloadArtistImage_SameTypeImagesGetDistinctFiles— an artist with 5 fanart images stores 5 distinct files with distinct bytes; album counterpart included;TestDownloadArtistImage_ExistingFileSkipsRedownloadguards the REQ-ENRICH-033 skip path.internal/handlers/images_selection_test.go— 10 cases covering IsPrimary > Likes > dimensions ordering, nil Likes/dimensions, empty-local_path skipping, determinism.internal/enrichers/registry_test.go— duplicate registration errors and preserves the original factory; distinct types coexist.Deferred Design Doc Updates
docs/adrs/ADR-0027-image-storage-filesystem.md— examplelocal_pathformat (data/images/artists/1-thumbnail.png) should be updated to the new{id}-{type}-{urlhash}.{ext}pattern.docs/adrs/ADR-0015-pluggable-enricher-registry-pattern.md— the "Bad, because … only one factory per type is allowed" consequence and the Confirmation section should note that duplicate registration now errors per REQ-ENRICH-050.docs/openspec/specs/metadata-enrichment-pipeline/spec.md— REQ-ENRICH-050 describesRegister(enricher Enricher), while the implemented signature isRegister(t Type, factory Factory)(pre-existing drift, now with error return).Closes #343
🤖 Posted on behalf of
@joestumpby Claude.