feat(library): resolve rotation Discogs id via LML /lookup on tier-1/2 miss (#986)#987
Merged
Merged
Conversation
…2 miss Extends `getDiscogsReleaseIdByRotationId` with a tier-3 LML `POST /api/v1/lookup` fallback on the rotation row's `(artist_name, album_title)` when the direct `rotation.discogs_release_id` column and the `library_identity.discogs_release_id` fallback both miss. Per-`rotation_id` LRU caches positive (1 hr) and negative (10 min) results. Mirrors tubafrenzy's `RotationTracklistCache.fetchAndCache` — the same `searchDiscogsRelease(artist, title)` path the classic site picker uses. Without this, `/library/rotation/:rotation_id/tracks` (#940) returns `[]` for every existing rotation row: `rotation.discogs_release_id` is mirrored from a tubafrenzy column that's populated on 0/21,563 rows (paste-URL-prefill only; verified prod 2026-05-21), and `library_identity.discogs_release_id` is structurally NULL until #801 extends the LML bulk-resolve contract with release-level resolution. Tier 3 keeps the picker working today; tiers 1 and 2 are the substrate we hand off to once upstreams catch up. Closes #986.
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.
Closes #986.
Summary
Extends
getDiscogsReleaseIdByRotationIdwith a tier-3 LMLPOST /api/v1/lookupfallback on the rotation row's(artist_name, album_title)when both the directrotation.discogs_release_idand thelibrary_identityfallback are NULL. Per-rotation_idLRU caches positive and negative results so the LML chokepoint isn't hammered on picker dropdown opens. Mirrors tubafrenzy'sRotationTracklistCache.fetchAndCache— the same path the classic-site picker uses.Without this,
/library/rotation/:rotation_id/tracks(#940) returns[]for every existing rotation row in prod:rotation.discogs_release_idis mirrored from a tubafrenzy column populated on 0/21,563 rows (verified 2026-05-21), andlibrary_identity.discogs_release_idis structurally NULL until #801 extends the LML bulk-resolve contract with release-level resolution.Behavior
Three tiers, walked in order:
rotation.discogs_release_id(direct) — covers future paste-URL-prefill addslibrary_identity.discogs_release_idvia thealbum_idbridge (fallback) — covers post-#801 release-level identity rowsPOST /api/v1/lookupon(artist_name, album_title)(runtime) — covers the present and most post-turndown rows. Cached perrotation_id: positive 1 hr, negative 10 min. Two LRUs to match the artwork/negativeCache pattern inproxy.controller.ts(lru-cache v11'sV extends {}constraint forces splitting the null case onto a separate cache).Errors from
lookupMetadata(timeouts, 5xx) are swallowed and not cached so a transient LML blip doesn't lock the picker into degraded mode for the negative TTL window. LML'slookupMetadataalready wraps in a Sentry span carryinglml.cache.*attributes, so observability lands without per-callsite instrumentation.No DB cache-through. Tubafrenzy's MySQL column isn't written by this path either, and a mix between paste-URL-prefilled and LML-resolved values in the same column would muddy provenance for a future audit.
Files
apps/backend/services/library.service.ts— extends SELECT to includeartist_name+album_title, adds the LRU pair +__resetRotationLmlLookupCacheForTests, and theresolveRotationDiscogsReleaseViaLmlhelperapps/backend/controllers/library.controller.ts— JSDoc ongetRotationTracksupdated for the new tier-3 behavior + service-layer cache notetests/unit/services/library.service.test.ts— adds an 11-casegetDiscogsReleaseIdByRotationId — tier-3 LML fallbackdescribe blockTest plan
npm run typecheck— greennpm run lint— 0 errors (413 pre-existing warnings only)npm run format:check— cleannpm run test:unit— 2016/2016 pass (11 new assertions for tier-3 fallback, cache, NULL bypass, error-not-cached)npm run build— greennode scripts/validate-migrations.mjs— passes (no migrations touched)node scripts/check-bulk-update-analyze.mjs— passesbash scripts/check-precondition-guards.sh— passes (no migrations touched)/library/rotation/<id>/tracksreturns a non-empty array on a Heavy row that was empty beforeop:http.client name:lml.lookupspans tied to/library/rotation/.../trackstransactions — confirm the per-request cost is what we expectRelated
/library/rotation/:rotation_id/tracksendpoint that consumes thislibrary-identity-consumer(tier 2's substrate)getDiscogsReleaseIdBy*siblings (this PR keeps the function intact for the refactor to fold in)