From 54f03148d1419663a0a2a59a228d99064c3a695b Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 18 Jun 2026 18:21:49 -0500 Subject: [PATCH 1/2] fix: enrich captured songs with artists + notes (root cause) The valuation capture path created songs rows from the Spotify track lookup but discarded track.artists and never ran the manual flow's enrichment, so captured songs had no song_artists and no notes -> the chat catalog view's isCompleteSong filter (artist + notes required, on by default) hid every valuation track (count shown, list empty). mapUnmappedAlbumTracks now carries track.artists through and runs the same enrichment as processSongsInput: linkSongsToArtists (auto-creates the artist account) + queueRedisSongs (queues note generation). TDD: new test asserts artists are linked + queued; lib/research/playcounts + lib/songs 109 tests pass. Root-cause follow-up on recoupable/chat#1801. --- .../__tests__/mapUnmappedAlbumTracks.test.ts | 32 +++++++++++++++++ .../playcounts/mapUnmappedAlbumTracks.ts | 34 ++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts b/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts index 954a5851..51ceff3c 100644 --- a/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts +++ b/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts @@ -5,6 +5,8 @@ import generateAccessToken from "@/lib/spotify/generateAccessToken"; import getTracks from "@/lib/spotify/getTracks"; import { upsertSongs } from "@/lib/supabase/songs/upsertSongs"; import { upsertSongIdentifiers } from "@/lib/supabase/song_identifiers/upsertSongIdentifiers"; +import { linkSongsToArtists } from "@/lib/songs/linkSongsToArtists"; +import { queueRedisSongs } from "@/lib/songs/queueRedisSongs"; import { SpotifyRateLimitError } from "@/lib/spotify/SpotifyRateLimitError"; vi.mock("@/lib/spotify/generateAccessToken", () => ({ default: vi.fn() })); @@ -13,6 +15,8 @@ vi.mock("@/lib/supabase/songs/upsertSongs", () => ({ upsertSongs: vi.fn() })); vi.mock("@/lib/supabase/song_identifiers/upsertSongIdentifiers", () => ({ upsertSongIdentifiers: vi.fn(), })); +vi.mock("@/lib/songs/linkSongsToArtists", () => ({ linkSongsToArtists: vi.fn() })); +vi.mock("@/lib/songs/queueRedisSongs", () => ({ queueRedisSongs: vi.fn() })); const ALBUMS = [ { @@ -55,6 +59,34 @@ describe("mapUnmappedAlbumTracks", () => { expect([...mapped.entries()]).toEqual([["t_new", "ISRC_NIKES"]]); }); + it("links captured songs to their Spotify artists and queues them for note enrichment", async () => { + vi.mocked(getTracks).mockResolvedValue({ + tracks: [ + { + id: "t_new", + name: "Nikes on My Feet", + external_ids: { isrc: "ISRC_NIKES" }, + artists: [{ id: "a1", name: "Mac Miller" }], + }, + ], + error: null, + } as never); + + await mapUnmappedAlbumTracks(ALBUMS, new Set(["t_mapped", "t_noisrc"])); + + // Root-cause fix: captured songs get the same enrichment as the manual flow — + // artists linked + queued for notes — so they aren't "missing info" in the catalog. + expect(linkSongsToArtists).toHaveBeenCalledWith([ + expect.objectContaining({ + isrc: "ISRC_NIKES", + spotifyArtists: [{ id: "a1", name: "Mac Miller" }], + }), + ]); + expect(queueRedisSongs).toHaveBeenCalledWith([ + expect.objectContaining({ isrc: "ISRC_NIKES" }), + ]); + }); + it("returns an empty map without Spotify calls when everything is mapped", async () => { const mapped = await mapUnmappedAlbumTracks(ALBUMS, new Set(["t_mapped", "t_new", "t_noisrc"])); diff --git a/lib/research/playcounts/mapUnmappedAlbumTracks.ts b/lib/research/playcounts/mapUnmappedAlbumTracks.ts index e987b037..e9fdb8fc 100644 --- a/lib/research/playcounts/mapUnmappedAlbumTracks.ts +++ b/lib/research/playcounts/mapUnmappedAlbumTracks.ts @@ -4,6 +4,10 @@ import { upsertSongs } from "@/lib/supabase/songs/upsertSongs"; import { upsertSongIdentifiers } from "@/lib/supabase/song_identifiers/upsertSongIdentifiers"; import { SpotifyAlbumPlayCounts } from "@/lib/apify/spotify/fetchSpotifyAlbumPlayCounts"; import { SpotifyRateLimitError } from "@/lib/spotify/SpotifyRateLimitError"; +import { getSpotifyArtists } from "@/lib/songs/getSpotifyArtists"; +import { linkSongsToArtists } from "@/lib/songs/linkSongsToArtists"; +import { queueRedisSongs } from "@/lib/songs/queueRedisSongs"; +import { SongWithSpotify } from "@/lib/songs/getSongsByIsrc"; /** * Self-mapping bootstrap (chat#1794): resolve ISRCs for actor tracks that have @@ -49,20 +53,35 @@ export async function mapUnmappedAlbumTracks( name: track.name, albumId: context.albumId, albumName: context.albumName, + spotifyArtists: getSpotifyArtists(track.artists ?? []), }, ]; }); if (resolved.length === 0) return new Map(); // Dedupe by ISRC: reissues put the same recording on several albums in one - // batch, and upsertSongs' DO UPDATE cannot affect the same row twice. - const songsByIsrc = new Map( + // batch, and upsertSongs' DO UPDATE cannot affect the same row twice. Carry + // the Spotify artists through for enrichment. + const songsByIsrc = new Map( resolved.map(r => [ r.isrc, - { isrc: r.isrc, name: r.name ?? null, album: r.albumName ?? null }, + { + isrc: r.isrc, + name: r.name ?? null, + album: r.albumName ?? null, + spotifyArtists: r.spotifyArtists, + }, ]), ); - await upsertSongs([...songsByIsrc.values()]); + const songs = [...songsByIsrc.values()]; + + await upsertSongs( + songs.map(song => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { spotifyArtists, ...record } = song; + return record; + }), + ); await upsertSongIdentifiers( resolved.flatMap(r => [ { song: r.isrc, platform: "spotify", identifier_type: "track_id", value: r.trackId }, @@ -72,6 +91,13 @@ export async function mapUnmappedAlbumTracks( ]), ); + // Root cause (chat#1801): give captured songs the same enrichment as the + // manual/CSV flow — link artists (auto-creating the artist account) and + // queue note generation — so valuation tracks aren't "missing info" and + // render in the catalog view rather than being filtered out. + await linkSongsToArtists(songs); + await queueRedisSongs(songs); + return new Map(resolved.map(r => [r.trackId, r.isrc])); } catch (error) { if (error instanceof SpotifyRateLimitError) throw error; From dbaf5c82c26972aa870e8f76c0e41c911c2c42c0 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 18 Jun 2026 18:40:05 -0500 Subject: [PATCH 2/2] style: prettier-format the capture-enrichment test Co-Authored-By: Claude Opus 4.8 (1M context) --- .../playcounts/__tests__/mapUnmappedAlbumTracks.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts b/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts index 51ceff3c..714d597f 100644 --- a/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts +++ b/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts @@ -82,9 +82,7 @@ describe("mapUnmappedAlbumTracks", () => { spotifyArtists: [{ id: "a1", name: "Mac Miller" }], }), ]); - expect(queueRedisSongs).toHaveBeenCalledWith([ - expect.objectContaining({ isrc: "ISRC_NIKES" }), - ]); + expect(queueRedisSongs).toHaveBeenCalledWith([expect.objectContaining({ isrc: "ISRC_NIKES" })]); }); it("returns an empty map without Spotify calls when everything is mapped", async () => {