diff --git a/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts b/lib/research/playcounts/__tests__/mapUnmappedAlbumTracks.test.ts index 954a5851..714d597f 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,32 @@ 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;