Source: task/handle-shreddit-shadow-dom (plan/steward-linker-design) — the TreeWalker only walked light DOM, so new-reddit comment bodies inside shadow roots were missed. - collectTextNodes now walks the comment-tree root's light DOM AND recursively descends into the open shadow roots of elements within that subtree, so comment text is reached whether reddit slots it (light DOM) or encapsulates it (shadow DOM). - Scoped to getRoot()'s comment tree (never the whole document's shadow roots); bounded by a depth cap; a no-op on old reddit (no shadow roots). - Slotted light-DOM nodes are not double-collected (a shadow walker sees the shadow tree's own nodes, not a <slot>'s assigned light nodes); a Set de-dupes by node identity belt-and-braces. - All existing guards preserved (inSkippable / rsl-link skip). README + AGENTS document the support. - Limitation: CLOSED shadow roots are unreachable from a content script and stay unlinked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
8.7 KiB
Markdown
196 lines
8.7 KiB
Markdown
# Reddit → Spotify / Bandcamp linker
|
|
|
|
A Firefox extension that, while you browse a Reddit thread, finds **artist / band
|
|
names and album titles** 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 and albums instead of names you have to copy-paste into
|
|
search.
|
|
|
|
The hard part is *"which words are artists/albums and which aren't?"*. Rather than
|
|
guess with client-side rules, the extension hands candidate names to a tiny local
|
|
**resolver service** that confirms each one and only links the names that are
|
|
really artists or albums. The resolver confirms names two ways (you enable either
|
|
or both): **Spotify** (exact-name match across artists **and** albums → direct
|
|
Spotify pages) and an optional **local LLM** via ollama (classifies which
|
|
candidates are artists/albums). A confirmed name links to its Spotify
|
|
artist/album page when available, otherwise to a Google search for
|
|
"<name> bandcamp". Album links are italicised so you can tell an album from an
|
|
artist at a glance.
|
|
|
|
```
|
|
reddit page content script resolver service
|
|
─────────── ────────────── ────────────────
|
|
comment text ──extract──> candidate names ──POST /resolve──> Spotify API (exact-match + direct links)
|
|
and/or
|
|
<a> links <──rewrite── confirmed links <───────────────── local LLM (ollama, optional)
|
|
```
|
|
|
|
## 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 (spotify: off, llm: off)
|
|
```
|
|
|
|
For anything to link you need **at least one** of two gates configured —
|
|
**Spotify credentials** or the **local LLM** (or both). With neither, the resolver
|
|
warns at startup and nothing links.
|
|
|
|
### Spotify credentials (one of the two gates)
|
|
|
|
Spotify does two jobs: it **gates** a name (a candidate links only when Spotify has
|
|
that **artist or album** by exact name match) and supplies the **direct** Spotify
|
|
page link. A single search call covers both types, so albums confirm and link with
|
|
no extra setup. To enable it:
|
|
|
|
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. Startup reports `(spotify: on, llm: off)`.
|
|
|
|
With Spotify alone, names that aren't on Spotify don't link — precise, but it
|
|
misses real artists Spotify doesn't carry. The local LLM gate (below) catches
|
|
those.
|
|
|
|
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}
|
|
```
|
|
|
|
A confirmed name links to its Spotify artist page when available, otherwise to a
|
|
Google search for "<name> bandcamp" (which surfaces the artist's Bandcamp page).
|
|
|
|
## Better matching with a local LLM (optional)
|
|
|
|
The optional LLM gate is the **matching-quality lever**: it classifies which
|
|
candidate strings are real artists/albums, so confirmed names that aren't on
|
|
Spotify still link (via the "<name> bandcamp" Google fallback). The resolver calls
|
|
ollama over **plain HTTP**, so it stays **zero npm dependencies**.
|
|
|
|
On a 16GB MacBook (M2):
|
|
|
|
1. Install ollama — <https://ollama.com/download> or `brew install ollama`.
|
|
2. Pull a small model: `ollama pull qwen2.5:3b` (~2GB; runs comfortably on a 16GB
|
|
M2; the first resolve call warms the model so it's a little slow once, then
|
|
fast).
|
|
3. In `service/.env`, point the resolver at ollama (and optionally override the
|
|
model):
|
|
|
|
```
|
|
OLLAMA_URL=http://localhost:11434
|
|
OLLAMA_MODEL=qwen2.5:3b
|
|
```
|
|
4. Restart the resolver. Startup reports `(spotify: off, llm: qwen2.5:3b)` (or
|
|
`(spotify: on, llm: qwen2.5:3b)` if you also set Spotify creds).
|
|
5. Confirm: `curl localhost:8787/health` should show `"llm":"qwen2.5:3b"`.
|
|
|
|
With both gates, the LLM decides which names are artists and Spotify supplies the
|
|
direct links; with the LLM alone, confirmed names get a Spotify direct link if one
|
|
exists, else the Google fallback.
|
|
|
|
## 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 and album titles become a single link — to **Spotify** if it's on
|
|
Spotify, otherwise to **Bandcamp** — colour-coded (green = Spotify, blue =
|
|
Google/Bandcamp); album links are italicised so they read as albums. 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,"spotify":<bool>,"llm":<model|false>,"cached":<n>}`. If both
|
|
`spotify` and `llm` are off, nothing can link — configure at least one gate
|
|
above.
|
|
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/background.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)
|
|
|
|
- **Artist 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*.
|
|
- **Album detection leans on two precise signals** — quoted titles (`"Reek of Putrefaction"`)
|
|
and album cue phrases ("the album X", "X LP/EP"). Album free-text is noisier than artist
|
|
names, so it deliberately over-generates only from these cues and lets the Spotify
|
|
exact-album match (and/or the LLM) be the precision filter. A bare album title written with
|
|
no quotes or cue may be missed.
|
|
- **Links are direct to Spotify when Spotify has the artist/album**; otherwise a confirmed
|
|
name links to a Google search for "<name> bandcamp" (which surfaces the Bandcamp page).
|
|
- **Without Spotify creds or the LLM, nothing links** — at least one gate must be
|
|
configured.
|
|
- **New reddit (shreddit) shadow DOM** — the comment-text scan pierces the **open**
|
|
shadow roots of the `shreddit-*` web components within the comment tree, so it links
|
|
comment bodies whether reddit slots them into light DOM or encapsulates them in a
|
|
shadow root. **Closed** shadow roots are unreachable from a content script and stay
|
|
unlinked (a browser limitation, not a setting).
|
|
- The optional LLM adds a little latency on the **first** call (ollama warms the
|
|
model), then is fast.
|
|
- The resolver caches results in memory; it resets when you restart it.
|
|
- Built and tested as a Firefox MV2 extension.
|
|
|
|
## License
|
|
|
|
Personal project — do as you like.
|