feat: optional direct Bandcamp/Apple/YouTube links via Odesli (flag-gated) (closes task/odesli-direct-cross-platform-links)

Source: task/odesli-direct-cross-platform-links (plan/improvement-backlog) — bandcamp was only a Google search; Odesli gives direct cross-platform links for Spotify-resolved names.

- service/odesli.js: makeOdesli({fetch,baseUrl}) -> getLinks(spotifyUrl); pure parseOdesli core; bounded AbortSignal.timeout; non-200/timeout/garbage -> {} (graceful).
- resolver.js: opt-in ODESLI / ODESLI_URL flag (default OFF). When ON, enrich each Spotify-resolved result with additive bandcamp/appleMusic/youtube fields (isHttpUrl-validated), cached per Spotify url. OFF -> no call, byte-identical behavior. /health + startup log surface the flag.
- extension: a tiny additive "bc" link to a direct Bandcamp page when result.bandcamp is present (reuses isSafeHttpUrl); primary artist/album/spotify/google rendering unchanged. styles.css: bandcamp-teal secondary link.
- odesli.test.js + odesli.fixture.json: parseOdesli unit-tested against a real captured Odesli response (Blood Incantation album -> direct bandcamp; absent apple/youtube omitted; junk -> {}); getLinks 200/non-200/throw/parse-error paths.
- docs: .env.example, README, AGENTS.md note the opt-in flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
paul
2026-06-24 02:41:30 +00:00
co-authored by Claude Opus 4.8
parent 5ceea28948
commit e06c983b16
9 changed files with 544 additions and 8 deletions
+26 -1
View File
@@ -109,6 +109,24 @@
return a;
}
// Optional, additive: a tiny secondary "bc" link to the DIRECT Bandcamp page when the resolver
// enriched the result with one (the Odesli leg). Absent => null => nothing extra rendered, so a
// result with no `bandcamp` field looks exactly as before. The href is re-validated the same way
// makeLink does — an unsafe scheme yields null and no link is injected.
function makeBandcampLink(links) {
const url = links.bandcamp;
if (typeof url !== "string" || !Extract.isSafeHttpUrl(url)) return null;
const name = links.name || "";
const a = document.createElement("a");
a.className = "rsl-link rsl-bandcamp rsl-direct";
a.textContent = "bc";
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.title = `Open ${name} on Bandcamp (direct)`;
return a;
}
// `matches` is the node's allMatchesIn(text) result, computed once per scan in scan()
// and reused here — the regex battery runs once per node per scan, not twice.
function wrapNode(node, matches) {
@@ -124,9 +142,16 @@
if (m.index < last) continue; // skip overlaps
if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
const matchText = text.substr(m.index, m.length);
const link = makeLink(matchText, resolved.get(m.name));
const links = resolved.get(m.name);
const link = makeLink(matchText, links);
// makeLink returns null for an unsafe (non-http(s)) href: keep the plain text.
frag.appendChild(link || document.createTextNode(matchText));
// Additive: when the primary link rendered AND the resolver supplied a direct Bandcamp url,
// append a tiny secondary "bc" link. No bandcamp field => nothing extra (unchanged rendering).
if (link) {
const bc = makeBandcampLink(links);
if (bc) frag.appendChild(bc);
}
last = m.index + m.length;
}
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));