4. The Matcher: Turning a Line of Text Into One Track
On 14 June 2026 the BBC listed "M.O.T.H.E.R. – My Love". The matcher
searched Spotify, got 19 results, ranked them, and confidently added
Luciano Pavarotti singing "Una furtiva lagrima" from Donizetti's The
Elixir of Love to the June playlist. It set reviewRequired: false and sent
no email.
It's funny, and it's also the most important thing to understand about this project: search results are not evidence. Spotify's search falls back to fuzzy relevance when nothing matches, so you always get something back. A matcher that ranks without first verifying will eventually pick nonsense, and it will do it confidently.
The code is src/services/songMatcher.ts.
The pipeline
flowchart TD
A["BBC: artist, title"] --> B["Search with fallbacks<br/>(up to 4 query variants)"]
B -->|0 results| NF["not_found → review"]
B -->|results| C{"Artist verified?<br/>(any credited artist)"}
C -->|none| AM["review: artist mismatch"]
C -->|some| D{"Title verified?"}
D -->|none| TM["review: title mismatch"]
D -->|exactly 1| OK1["success"]
D -->|2+| E["Score and rank"]
E --> F{"Clear winner?"}
F -->|yes| OK2["success: top-ranked"]
F -->|no| MM["review: multiple matches"]
Five possible endings, and three of them are "ask a human". That's deliberate.
Stage 1: Search with fallbacks
The BBC and Spotify spell things differently. Rather than one perfect query, the matcher tries up to four, and stops at the first one that returns anything:
| # | Title | Artist | Exists because |
|---|---|---|---|
| 1 | original | original | |
| 2 | (feat. …) stripped |
original | #1: Spotify puts featured artists in the artist list, not the title. This caused 5 of the 7 "not found" songs in the first live run |
| 3 | original | & The … stripped |
#2: "Avalon Emerson & The Charm" is just "Avalon Emerson" on Spotify |
| 4 | stripped | stripped | both |
The helpers are tiny and worth reading:
titleCleaner.ts (one regex for feat.,
ft. and featuring inside () or []) and
artistNameUtils.ts (strip a trailing
& … or and …).
This is a fallback ladder: start strict, loosen step by step, and log each step so you can see in the logs why something matched.
Stage 2: Verify, then rank
This is the fix for Pavarotti (#13). Before any scoring, every candidate must pass two checks.
Artist check: does any artist credited on the track refer to the BBC artist?
- Names are normalised first: lowercase, apostrophes removed, any other
punctuation turned into spaces. So "M.O.T.H.E.R." becomes
m o t h e r. - Exact equality after normalising → pass.
- Phrase containment is allowed only if both names are more than one word. "Nick Cave" inside "Nick Cave & The Bad Seeds" passes. A one-word name must match exactly, so "Beck" doesn't match "Jeff Beck". Comparison is by whole words too, so "Beck" never matches "Becko" or "Clark Beckham" either. Those were the real wrong matches the code comment cites.
- An artist made entirely of punctuation (there really is an artist called "@") normalises to nothing, so it falls back to a literal comparison.
Title check: with featured artists stripped from both titles, one title's words must be a prefix of the other's.
- "Shine" vs "Shine – 2011 Remaster" → pass (a remaster or radio edit suffix is fine)
- "Love" vs "Concept of Love" → fail (the words aren't at the start)
- "Air" vs "Ayr" → fail
There's a twist. "Concept of Love" really is the track the BBC called "Love" (Ibibio Sound Machine, #12). So the check now sends a correct match to review. That's the trade-off working as intended: a correct track flagged for review costs a click, while a wrong track added silently costs your trust in the playlist.
Notice the order: filter first, then rank. Ranking can only choose among candidates. It can't tell you that none of them are right.
Stage 3: Scoring
Candidates that pass both checks are scored like this:
| Points | Rule | Why |
|---|---|---|
| +1000 | Album name matches the BBC's album | The strongest signal. (It never fires, because the BBC page doesn't list albums) |
| +500 | A single whose release name equals the track name ("self-titled single") | #3: 6 Music plays new singles. In the first run, all 5 review items were this exact case and the single was right every time |
| +100 | Release type is album |
Prefer the canonical album version over a random EP or compilation |
| +10 | Not a remix and not live | Studio version |
| +0 to 1 | popularity / 100 |
Tiebreaker. It can never outweigh a real rule |
The numbers are spaced by orders of magnitude on purpose. This is a lexicographic score: a higher tier always beats any combination of lower tiers. The weights don't add up to some blended number. They encode a priority order in a single number.
Worked example: Bleachers, "you and forever"
| Candidate | Self-titled single | Album | Studio | Popularity | Score |
|---|---|---|---|---|---|
| "you and forever" (single you and forever) | +500 | +10 | 0.70 | 510.70 | |
| "you and forever" (album dirty wedding dress) | +100 | +10 | 0.46 | 110.46 |
The single wins by 400 points, so it's auto-selected. Before #3, the album version won 110 to 10 and the song went to review.
Notice this contradicts CLAUDE.md's "album version over single/EP" rule. The real data changed the rule, and the docs weren't updated. That's drift worth fixing (see Known flaws).
Stage 4: "Clear winner?"
if (gap >= 10) return true; // a structural rule separated them
if (gap < 2) return true; // same structure: popularity decides (#7)
return false;
#7:
originally only gap >= 10 counted. When two versions were structurally
identical (say, both studio album tracks), popularity could only add a
difference of less than 1, so every such song went to review: all 10 in the
April run. The fix says: if the structure ties, trust popularity.
There's a catch, though. Look closely. Every structural tier is a multiple
of 10, and popularity adds less than 1. So the gap between two candidates is
always either at least 10 or less than 1. The return false line can
never run, which means the "multiple matches" review outcome is
unreachable. Since #13, verification sits in front of ranking, so this is
much less dangerous than it was. Still, the code reads as if it has a safety
net that doesn't exist. See Known flaws.
What "review" means in practice
Review items are listed in an email with the alternatives. You open Spotify, add the right track by hand, and next week the live playlist read sees it (guide 5), so the song is never flagged again.
What didn't make it (yet): an LLM matcher
#22 (open, low priority) proposes asking an LLM to choose among the candidates, since "pick the version a radio station would be playing right now" is easy for a model and hard for rules. Each new heuristic (#1, #2, #3) fixes one pattern, and the long tail keeps growing.
That's a sensible idea behind the verification gate. Let the model choose among verified candidates, and never among raw search results. Otherwise you have only swapped Pavarotti for a different confident mistake.
Questions you might get asked
"How does the matching work?" It searches Spotify with up to four query variants, from strict to loose. It throws away any result whose artist or title doesn't verify against the BBC listing. It ranks the survivors with a tiered score (album match, then self-titled single, then album release, then studio version, with popularity as the tiebreaker) and auto-selects the winner. Anything it can't verify goes to a human by email.
"Why rules instead of fuzzy string similarity, like Levenshtein distance?" Similarity scores need a threshold, and every threshold is wrong for some songs: "Love" and "Concept of Love" are quite similar. The rules here are explicit about what kind of difference is allowed (a suffix like "– 2011 Remaster", a featured artist, a backing band), so each pass or fail can be explained in one sentence. When I get one wrong, I add a test.
"Tell me about a bug that taught you something." Pavarotti. The ranking was fine; the mistake was assuming search results contained the answer. When the real song wasn't on Spotify, we still picked the "best" of 19 unrelated tracks. The fix was to verify before ranking and to prefer "I don't know" over a confident wrong answer. That applies far beyond music.
"What's your false positive vs false negative trade-off?" A false positive (wrong track added) is worse than a false negative (song flagged for review). A wrong song sits in your playlist unnoticed. A review email costs me 30 seconds. So the verification checks are strict, and ambiguity goes to a human.
"How do you test this without hitting Spotify?"
The 69 matcher unit tests mock SpotifyService and feed it real-shaped
results, including the Pavarotti case and the "Beck"/"Clark Beckham" case. scripts/test-matcher.ts runs the same code against live
Spotify when I want to check by hand.