From 4fcacd01d53916e84ee9627f2402b8f51f4ccb75 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Tue, 28 Apr 2026 11:44:11 -0700 Subject: [PATCH] Denormalize library.artwork_url onto flowsheet on bin-pick (#628) The bin-pick branch of addEntry already calls getAlbumFromDB to backfill album info, but until now did not pull artwork_url even though the column exists on library since migration 0045. Adding it to the SELECT lets the freshly-inserted flowsheet row carry artwork at INSERT time, eliminating the metadata race for bin-picks on every client surface at once: the addEntry HTTP response carries the URL to dj-site, and the playlist-proxy's synchronous SELECT-on-tubafrenzy-created reads a non-null value. Free-form entries (no album_id) still rely on the asynchronous LML enrichment UPDATE and need the second-emit fix tracked in the rest of #628. Test coverage added at the controller boundary: asserts the bin-pick addTrack call carries through both the populated and the null library.artwork_url cases. --- apps/backend/services/flowsheet.service.ts | 5 ++ .../controllers/flowsheet.controller.test.ts | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/apps/backend/services/flowsheet.service.ts b/apps/backend/services/flowsheet.service.ts index 26bf1cd9..db0d39b3 100644 --- a/apps/backend/services/flowsheet.service.ts +++ b/apps/backend/services/flowsheet.service.ts @@ -606,6 +606,11 @@ export const getAlbumFromDB = async (album_id: number) => { album_title: library.album_title, record_label: library.label, label_id: library.label_id, + // Denormalized onto the new flowsheet row at INSERT time so dj-site + // and the iOS playlist-proxy see artwork immediately, without waiting + // for the asynchronous LML enrichment UPDATE to land. Free-form + // entries (no album_id) still rely on enrichment. See #628. + artwork_url: library.artwork_url, }) .from(library) .innerJoin(artists, eq(artists.id, library.artist_id)) diff --git a/tests/unit/controllers/flowsheet.controller.test.ts b/tests/unit/controllers/flowsheet.controller.test.ts index e9ffc742..4f10f767 100644 --- a/tests/unit/controllers/flowsheet.controller.test.ts +++ b/tests/unit/controllers/flowsheet.controller.test.ts @@ -440,6 +440,84 @@ describe('flowsheet.controller', () => { }); }); + it('denormalizes artwork_url from the library row onto the new flowsheet entry on the bin-pick path (#628)', async () => { + // Bin-pick path eliminates the artwork race entirely when library has + // a cached artwork_url: the addEntry response carries the URL, so the + // DJ's screen renders artwork immediately rather than waiting up to + // 60s for the next dj-site poll to pick up the LML enrichment UPDATE. + const albumInfo = { + artist_name: 'Jessica Pratt', + album_title: 'On Your Own Love Again', + record_label: 'Drag City', + label_id: 31, + artist_id: 9, + artwork_url: 'https://example.com/jp-oyola.jpg', + }; + mockGetAlbumFromDB.mockResolvedValue(albumInfo); + mockAddTrack.mockResolvedValue({ + id: 100, + show_id: activeShow.id, + artist_name: 'Jessica Pratt', + album_title: 'On Your Own Love Again', + track_title: 'Back, Baby', + album_id: 12, + rotation_id: null, + play_order: 1, + add_time: new Date(), + }); + + const req = createMockBodyReq({ + artist_name: 'Jessica Pratt', + album_title: 'On Your Own Love Again', + track_title: 'Back, Baby', + record_label: 'Drag City', + album_id: 12, + }); + const res = createMockRes(); + + await addEntry(req as Request, res as Response, mockNext); + + expect(res.status).toHaveBeenCalledWith(201); + expect(mockAddTrack).toHaveBeenCalledWith( + expect.objectContaining({ artwork_url: 'https://example.com/jp-oyola.jpg', album_id: 12 }) + ); + }); + + it('passes through a null artwork_url when the library row has none (#628)', async () => { + const albumInfo = { + artist_name: 'Stereolab', + album_title: 'Aluminum Tunes', + record_label: 'Duophonic', + artist_id: 7, + artwork_url: null, + }; + mockGetAlbumFromDB.mockResolvedValue(albumInfo); + mockAddTrack.mockResolvedValue({ + id: 101, + show_id: activeShow.id, + artist_name: 'Stereolab', + album_title: 'Aluminum Tunes', + track_title: 'Cybele\u2019s Reverie', + album_id: 13, + rotation_id: null, + play_order: 1, + add_time: new Date(), + }); + + const req = createMockBodyReq({ + artist_name: 'Stereolab', + album_title: 'Aluminum Tunes', + track_title: 'Cybele\u2019s Reverie', + record_label: 'Duophonic', + album_id: 13, + }); + const res = createMockRes(); + + await addEntry(req as Request, res as Response, mockNext); + + expect(mockAddTrack).toHaveBeenCalledWith(expect.objectContaining({ artwork_url: null, album_id: 13 })); + }); + it('delegates metadata enrichment without artistId for free-form inserts (no album_id)', async () => { const completedEntry = { id: 2,