BBC 6 Music → Spotify Sync: Learning Guide

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.