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
+14 -1
View File
@@ -17,7 +17,9 @@ is tracked in the **towl** work-item store under project **`reddit-spotify-linke
`node:test`. The only music API is Spotify (MusicBrainz removed). The resolver confirms **both artists
and albums**: one `type=artist,album` Spotify search per candidate, exact
case-insensitive name match against either; each result carries
`kind: "artist" | "album"`.
`kind: "artist" | "album"`. `odesli.js` = the optional Odesli (song.link) enrichment client
(injected `fetch`, pure `parseOdesli` core, HTTP-only — no new deps); `odesli.test.js` +
`odesli.fixture.json` (a captured real response) cover it.
- `extension/` — Firefox **MV2** content script (`content.js` + `extract.js` +
`manifest.json` + `styles.css`). No build step. `extract.js` is the pure candidate
extractor (Title-Case runs + cue phrases for artists; quoted strings + album cue
@@ -94,6 +96,17 @@ fixes small things directly.
to classify which candidates are real artists/albums. It stays **zero-NPM-dep**:
HTTP only, no in-process model runtime. The heuristic + Spotify exact-match is the
zero-dep default when no LLM is configured.
- **Optional Odesli (song.link) enrichment** is opt-in, default OFF (`ODESLI=1`, or `ODESLI_URL`
to override the endpoint — mirrors the ollama opt-in). When ON, an **album** result that already
carries a DIRECT Spotify url is enriched with the album's direct Bandcamp (+ Apple/YouTube) links
as ADDITIVE fields (`bandcamp`/`appleMusic`/`youtube`) — the existing `name`/`kind`/`spotify`/
`google`/`primary` shape is unchanged, so off the behavior is byte-identical and an older
extension keeps rendering. The gate is `shouldEnrich` (pure, unit-tested): **album-kind results
only** — Odesli rejects ARTIST urls (405 UNSUPPORTED_URL), so artist results are skipped entirely
(no wasted round-trip on the common case). Only the Spotify-creds path produces a url to enrich;
the no-creds Google fallback is untouched. Bounded (`AbortSignal.timeout`) + cached per Spotify
url + graceful (Odesli down/garbage keeps the current result). The input is our own validated
Spotify album url against the fixed host (no SSRF).
- Detection precision lever (zero-dep default): a candidate links **only** when
Spotify returns an **artist or album** whose name matches it case-insensitively. A
confirmed name links to its Spotify artist/album page if available, else a