Catch artist names written in lowercase right after a cue phrase ("check out devourment",
"by cattle decapitation", "fan of ...") — the Title-Case/ALL-CAPS pass missed those.
- extension/extract.js (new): pure extraction (matchesIn + new cueMatchesIn + allMatchesIn),
UMD so it loads as a content script AND require()s in node for tests. The cue pass uses the
regex `d` flag to map matches back to positions for in-place wrapping; a small cue-stoplist
drops "the/this/new/..." right after the cue.
- extension/content.js: extraction now comes from globalThis.RSLExtract.allMatchesIn
(Title-Case + cue union), keeping the existing position-based wrapping.
- extension/manifest.json: load extract.js before content.js.
- extension/extract.test.cjs (new) + scripts/check.sh: 6 node:test cases for the extraction
logic; check.sh now runs the extension tests too (16 total, green).
- README: updated the detection limitation note.
Deferred (design item 2): merging names split across inline formatting (cross-node) — more
involved; left as a follow-up.
Verified: scripts/check.sh green (16 tests). In-browser behavior is logic-verified only (no
headless Firefox); additive to the pending background-fix re-test — the [rsl] logs distinguish
"candidates found" from "resolve failed".
Source: task/smarter-candidate-extraction (objective/linker-improvements).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
5.8 KiB
Markdown
142 lines
5.8 KiB
Markdown
# Reddit → Spotify / Bandcamp linker
|
|
|
|
A Firefox extension that, while you browse a Reddit thread, finds **artist / band
|
|
names** in the comments (and the post body) and turns them into inline links to
|
|
**Spotify** and **Bandcamp** — so a recommendation thread like
|
|
[this one](https://www.reddit.com/r/SlamDeathMetal/comments/1txh2r1/) becomes a
|
|
list of clickable artists instead of names you have to copy-paste into search.
|
|
|
|
The hard part is *"which words are artists and which aren't?"*. Rather than guess
|
|
with client-side rules, the extension hands candidate names to a tiny local
|
|
**resolver service** that checks each one against a real music database and only
|
|
links the names that actually match an artist.
|
|
|
|
```
|
|
reddit page content script resolver service
|
|
─────────── ────────────── ────────────────
|
|
comment text ──extract──> candidate names ──POST /resolve──> Spotify API (direct links)
|
|
or
|
|
<a> links <──rewrite── confirmed links <───────────────── MusicBrainz (zero-setup)
|
|
```
|
|
|
|
## Layout
|
|
|
|
```
|
|
service/ tiny Node.js resolver (one file, zero npm deps)
|
|
resolver.js
|
|
package.json
|
|
.env.example
|
|
extension/ Firefox MV2 extension
|
|
manifest.json
|
|
background.js (does the resolver fetch; talks to the content script)
|
|
content.js
|
|
styles.css
|
|
icon.svg
|
|
```
|
|
|
|
## 1. Run the resolver service
|
|
|
|
Requirements: **Node 18+** (uses the built-in `fetch`). No `npm install` needed —
|
|
there are no dependencies.
|
|
|
|
```sh
|
|
cd service
|
|
node resolver.js
|
|
# resolver listening on http://localhost:8787 (provider: musicbrainz)
|
|
```
|
|
|
|
That's it. With no configuration the resolver validates names against
|
|
**MusicBrainz** (no account required). When MusicBrainz knows an artist's official
|
|
links (its `url-relations`), you get **direct** Spotify and Bandcamp artist pages;
|
|
otherwise it falls back to Spotify/Bandcamp **search** URLs.
|
|
|
|
### Optional: direct Spotify links
|
|
|
|
For **direct artist links** (instead of Spotify search), give the service Spotify
|
|
API credentials:
|
|
|
|
1. Create a free app at <https://developer.spotify.com/dashboard> and copy its
|
|
**Client ID** and **Client Secret**.
|
|
2. `cp .env.example .env` and fill them in (the `.env` file is gitignored):
|
|
|
|
```
|
|
SPOTIFY_CLIENT_ID=your_id
|
|
SPOTIFY_CLIENT_SECRET=your_secret
|
|
```
|
|
3. Restart the service. It will report `provider: spotify` on startup.
|
|
|
|
(Bandcamp has no public API; direct Bandcamp links come from MusicBrainz
|
|
`url-relations` when present, else a Bandcamp search URL.)
|
|
|
|
Quick check that it works:
|
|
|
|
```sh
|
|
curl -s localhost:8787/resolve \
|
|
-H 'content-type: application/json' \
|
|
-d '{"candidates":["Cerebral Bore","Devourment","Notarealband Xyzzy"]}'
|
|
# {"Cerebral Bore":{...},"Devourment":{...},"Notarealband Xyzzy":null}
|
|
```
|
|
|
|
## 2. Install the extension in Firefox
|
|
|
|
The extension is unpacked / unsigned, so load it as a **temporary add-on**:
|
|
|
|
1. Open `about:debugging#/runtime/this-firefox`.
|
|
2. **Load Temporary Add-on…**
|
|
3. Select `extension/manifest.json`.
|
|
|
|
It's now active until you restart Firefox. (To keep it permanently you'd sign it
|
|
via [AMO](https://addons.mozilla.org/developers/) or use Firefox Developer/ESR
|
|
with `xpinstall.signatures.required = false`.)
|
|
|
|
## 3. Use it
|
|
|
|
1. Make sure the resolver service is running.
|
|
2. Open any Reddit comments thread (a URL with `/comments/` in it).
|
|
3. Artist names become a single link — to **Spotify** if the artist is on Spotify,
|
|
otherwise to **Bandcamp** — colour-coded (green = Spotify, teal = Bandcamp). New
|
|
comments loaded as you scroll are linked automatically.
|
|
4. The toolbar icon shows the extension is active; its badge shows how many links were
|
|
added on the current page.
|
|
|
|
## Troubleshooting
|
|
|
|
If nothing gets linked:
|
|
|
|
1. Confirm the resolver is running — `curl localhost:8787/health` should return
|
|
`{"ok":true,...}`.
|
|
2. On the reddit thread, open the browser console (F12) and look for `[rsl]` logs: they
|
|
report the root element, candidate count, and resolver responses on each scan. A
|
|
`resolve failed` line means the background script can't reach the resolver (is it on
|
|
`:8787`?); `candidates found: 0` means the comment text wasn't matched (please report
|
|
the thread).
|
|
3. The content script reaches the resolver **through the extension's background script**
|
|
— a page-context fetch to `http://localhost` is blocked by reddit's CSP — so make sure
|
|
the add-on loaded without errors in `about:debugging`.
|
|
|
|
## Configuration
|
|
|
|
The resolver URL is the constant `RESOLVER_URL` at the top of
|
|
`extension/content.js` (default `http://localhost:8787`). If you change the host
|
|
or port, update **both** that constant **and** the matching entry in
|
|
`extension/manifest.json` → `permissions` (Firefox needs the host listed there to
|
|
let the content script reach it).
|
|
|
|
## Limitations (MVP)
|
|
|
|
- **Detection is mostly capitalization-based**, with a cue-phrase pass that also catches
|
|
lowercase names right after "check out / by / fan of / recommend …". Names split across
|
|
inline formatting (e.g. **bold** mid-name) can still be missed. The resolver's
|
|
exact-name match keeps false *positives* low; the trade-off is some false *negatives*.
|
|
- **Links are direct when MusicBrainz has the artist's `url-relations`** (official
|
|
Bandcamp / Spotify pages); otherwise they fall back to a search URL. (Each
|
|
confirmed artist costs one extra throttled MusicBrainz lookup, cached after.)
|
|
- The resolver caches results in memory; it resets when you restart it.
|
|
- Without Spotify credentials the MusicBrainz path is rate-limited to ~1 lookup/
|
|
second, so the first scan of a large thread fills in gradually (cached after).
|
|
- Built and tested as a Firefox MV2 extension.
|
|
|
|
## License
|
|
|
|
Personal project — do as you like.
|