Skip to content

feat(library): resolve rotation Discogs id via LML /lookup on tier-1/2 miss (#986)#987

Merged
jakebromberg merged 1 commit into
mainfrom
rotation-tracks-lml-lookup
May 21, 2026
Merged

feat(library): resolve rotation Discogs id via LML /lookup on tier-1/2 miss (#986)#987
jakebromberg merged 1 commit into
mainfrom
rotation-tracks-lml-lookup

Conversation

@jakebromberg
Copy link
Copy Markdown
Member

Closes #986.

Summary

Extends getDiscogsReleaseIdByRotationId with a tier-3 LML POST /api/v1/lookup fallback on the rotation row's (artist_name, album_title) when both the direct rotation.discogs_release_id and the library_identity fallback are NULL. Per-rotation_id LRU caches positive and negative results so the LML chokepoint isn't hammered on picker dropdown opens. Mirrors tubafrenzy's RotationTracklistCache.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_id is mirrored from a tubafrenzy column populated on 0/21,563 rows (verified 2026-05-21), and library_identity.discogs_release_id is structurally NULL until #801 extends the LML bulk-resolve contract with release-level resolution.

Behavior

Three tiers, walked in order:

  1. rotation.discogs_release_id (direct) — covers future paste-URL-prefill adds
  2. library_identity.discogs_release_id via the album_id bridge (fallback) — covers post-#801 release-level identity rows
  3. LML POST /api/v1/lookup on (artist_name, album_title) (runtime) — covers the present and most post-turndown rows. Cached per rotation_id: positive 1 hr, negative 10 min. Two LRUs to match the artwork/negativeCache pattern in proxy.controller.ts (lru-cache v11's V 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's lookupMetadata already wraps in a Sentry span carrying lml.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 include artist_name + album_title, adds the LRU pair + __resetRotationLmlLookupCacheForTests, and the resolveRotationDiscogsReleaseViaLml helper
  • apps/backend/controllers/library.controller.ts — JSDoc on getRotationTracks updated for the new tier-3 behavior + service-layer cache note
  • tests/unit/services/library.service.test.ts — adds an 11-case getDiscogsReleaseIdByRotationId — tier-3 LML fallback describe block

Test plan

  • npm run typecheck — green
  • npm run lint — 0 errors (413 pre-existing warnings only)
  • npm run format:check — clean
  • npm run test:unit — 2016/2016 pass (11 new assertions for tier-3 fallback, cache, NULL bypass, error-not-cached)
  • npm run build — green
  • node scripts/validate-migrations.mjs — passes (no migrations touched)
  • node scripts/check-bulk-update-analyze.mjs — passes
  • bash scripts/check-precondition-guards.sh — passes (no migrations touched)
  • CI green
  • Manual Build & Deploy after merge (no migrations; deploy cadence rule doesn't apply)
  • Post-deploy: verify /library/rotation/<id>/tracks returns a non-empty array on a Heavy row that was empty before
  • Post-deploy: verify dj-site rotation entry-mode picker shows tracks for a Heavy release in prod
  • Post-deploy: spot-check Sentry trace explorer for op:http.client name:lml.lookup spans tied to /library/rotation/.../tracks transactions — confirm the per-request cost is what we expect

Related

  • #984 / #983 — added tier 1 (the column + mirror)
  • #940/library/rotation/:rotation_id/tracks endpoint that consumes this
  • #802library-identity-consumer (tier 2's substrate)
  • #801 — release-level resolution in LML's bulk-resolve contract (orthogonal; would populate tier 2 over time, reducing tier-3 traffic)
  • #981 — consolidate getDiscogsReleaseIdBy* siblings (this PR keeps the function intact for the refactor to fold in)

…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.
@jakebromberg jakebromberg merged commit aa33678 into main May 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolve rotation Discogs release id via LML /lookup on tier-1/tier-2 miss (tubafrenzy parity for #940 picker)

1 participant