feat: detect & link album mentions, not just artists (closes task/album-detection)
Source: task/album-detection (objective/linker-improvements) — project brief asks for artist OR album linking; MVP linked artists only.
- service/lib.js: pickAlbum (the exact-name gate, named for the album path) + linkResult now carries kind:"artist"|"album" (additive; defaults to artist so older clients keep working).
- service/resolver.js: one Spotify type=artist,album search per candidate, exact-match against either; returns {url,kind}; cache key includes the kind hint. resolveOne/resolveAll thread the LLM kind hint through.
- service/llm.js: parseClassification now returns Map(name -> "artist"|"album") so album-kind candidates route to the album path (.has() still works like the old Set).
- extension/extract.js: albumMatchesIn over two precise signals (quoted strings + album cue phrases "the album X", "X LP/EP"), folded into allMatchesIn.
- extension/content.js + styles.css: album links render italicised (.rsl-album) with an album tooltip; artist behavior unchanged.
- Tests: new pure-logic coverage in lib.test.js / llm.test.js / extract.test.cjs (album shaping, Map-kind routing, album candidate extraction). scripts/check.sh green.
- README.md + AGENTS.md: document album linking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+8
-6
@@ -26,13 +26,15 @@ export function buildClassifyMessages(candidates) {
|
||||
];
|
||||
}
|
||||
|
||||
// Parse the model's JSON reply into a Set of norm(name) the model marked artist/album.
|
||||
// Defensive: only names that were actually in the candidate list count (guards against the
|
||||
// model inventing names); an unparseable reply yields an empty Set, and the caller then
|
||||
// falls back to the Spotify-exact-match gate.
|
||||
// Parse the model's JSON reply into a Map of norm(name) -> "artist" | "album" for the
|
||||
// candidates the model marked as music (the kind routes each to the artist or album Spotify
|
||||
// lookup). Defensive: only names that were actually in the candidate list count (guards
|
||||
// against the model inventing names); an unparseable reply yields an empty Map, and the
|
||||
// caller then falls back to the Spotify-exact-match gate. A Map still answers `.has()` like
|
||||
// the old Set, so a name absent from it is dropped exactly as before.
|
||||
export function parseClassification(text, candidates) {
|
||||
const allowed = new Set(candidates.map(norm));
|
||||
const confirmed = new Set();
|
||||
const confirmed = new Map();
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(text);
|
||||
@@ -45,7 +47,7 @@ export function parseClassification(text, candidates) {
|
||||
const k = norm(row.name);
|
||||
if (!allowed.has(k)) continue;
|
||||
const kind = String(row.kind ?? "").toLowerCase();
|
||||
if (kind === "artist" || kind === "album") confirmed.add(k);
|
||||
if (kind === "artist" || kind === "album") confirmed.set(k, kind);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user