From 0a01085ed017cbf4a155dc48dff972cb4eadcaf9 Mon Sep 17 00:00:00 2001 From: kami Date: Mon, 10 Aug 2026 14:00:04 +0400 Subject: [PATCH] fix(discovery): give every track a place in the graph, not just the ones MusicBrainz knows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit track_artists_v2 is a view over claim_fusion, and only the MusicBrainz spine wrote credited_main_on claims. The scanner filled track_artists and left the spine alone, so the tag-derived backfill migration from July was the only thing that ever put local credits in the graph. Every track scanned since was therefore invisible to all but one Vibe generator — 740 of them, including 20 of the 26 acquired recommendations, all of which had perfectly good artist tags sitting unused. Obscure music is exactly what MusicBrainz does not know and exactly what a self-hosted library is full of. The scanner now writes those credits itself. Source is `tag`, the lowest trust in the spine, so a later MusicBrainz claim about the same edge still outranks it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KENqSChfyqWnor6ud2WWH6 --- workers/src/scanner.service.ts | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/workers/src/scanner.service.ts b/workers/src/scanner.service.ts index 3e26d4e..a569264 100644 --- a/workers/src/scanner.service.ts +++ b/workers/src/scanner.service.ts @@ -252,6 +252,14 @@ export class ScannerService { ); } + // 5. Put the same credit into the claim graph. Every Vibe generator but + // the fallback reaches tracks through claim_fusion, and only MusicBrainz + // was writing those edges — so a track MusicBrainz has never heard of was + // invisible to the recommender no matter how well tagged the file was. + // `tag` is the lowest-trust source in the spine, so a later MusicBrainz + // claim about the same edge still outranks this one. + await this.writeTagCredits(trackId, String(artistId), featuredIds.map(String)); + console.log(`[Scanner] Successfully processed: ${trackTitle}`); // Trigger external-API enrichment for this track + artist + album. @@ -315,6 +323,34 @@ export class ScannerService { return { id: String(inserted.rows[0].id), name: String(inserted.rows[0].name) }; } + /** + * Record the file's own artist credits as claims. Confidence is 1.0 because + * the tag says this without ambiguity; how much that is worth is decided by + * the trust attached to the `tag` source, not here. + */ + private async writeTagCredits(trackId: string, artistId: string, featuredIds: string[]): Promise { + const credits: Array<[string, string]> = [ + [artistId, 'credited_main_on'], + ...featuredIds.map((id): [string, string] => [id, 'featured_on']), + ]; + for (const [objectId, predicate] of credits) { + try { + await this.pgClient.query( + `INSERT INTO claims ( + subject_type, subject_id, predicate, object_type, object_id, source, confidence + ) VALUES ('track', $1::uuid, $2, 'artist', $3::uuid, 'tag', 1.0) + ON CONFLICT (subject_type, subject_id, predicate, object_type, object_id, source, user_id) + DO UPDATE SET last_reinforced_at = NOW()`, + [trackId, predicate, objectId] + ); + } catch (err) { + // A missing edge costs this track its place in the graph; it must not + // cost the whole scan the file. + console.error(`[Scanner] Failed to write ${predicate} claim for track ${trackId}:`, err); + } + } + } + private async enqueueEnrichment(trackId: string, artistId: string, albumId: string) { const keep = { removeOnComplete: { age: 86400, count: 5000 },