2. The BBC Side: Scraping a Page With No API
The source
https://www.bbc.co.uk/programmes/articles/5JDPyPdDGs3yCLdtPhGgWM7/bbc-radio-6-music-playlist
It's an editorial article, not a data feed. A producer updates it on Mondays. It looks roughly like this:
<div class="text--prose">
<p><strong>NEW ENTRIES ARE IN BOLD ◼️ Entries marked ↑ have moved category</strong></p>
<h3>29TH JUNE-5TH JULY 2026</h3>
<p><strong>Bleachers - you and forever</strong></p>
<p>↑ Courtney Barnett - Mantis</p>
<p>James Blake - Tracks from 'Trying Times'</p>
...
</div>
There's no API, no RSS and no JSON, so we scrape it: download the HTML and pick it apart. The code is src/services/bbcScraper.ts (about 230 lines).
A nice detail: the 2022 Python script this project grew out of used the
selector div.text--prose > p. The TypeScript scraper uses the same selector
four years later. The BBC has redesigned around that block, but not the block
itself (see History).
How it works
Three steps: fetch, parse, date.
Fetch, with retries
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return (await axios.get(this.url, { timeout: 30000, headers: { 'User-Agent': 'BBC6MusicPlaylistSync/1.0' } })).data;
} catch (error) {
if (this.isClientError(error)) throw ...; // 4xx: retrying won't help
if (attempt < this.maxRetries) await this.sleep(this.retryDelayMs * attempt); // 5s, then 10s
}
}
There are three ideas worth being able to explain here:
- Linear backoff (wait 5s, then 10s). The ADR's reasoning: it's a public page with no rate limit to respect, and exponential backoff (5s, 25s, 125s) would just be slower. Spotify gets different treatment, because Spotify does rate-limit.
- Don't retry 4xx. A 404 means the URL is wrong, and asking again won't change that. 5xx and network errors are usually temporary, so those get retried.
- An honest User-Agent. It names the bot, which is polite. Some sites block unknown agents, though, and this is a likely place for a future breakage.
Parse
const $ = cheerio.load(html);
const container = $('div.text--prose');
if (container.length === 0) throw new BBCScraperError('container not found ...');
container.find('p').each(...)
Cheerio is jQuery for the server. It parses HTML into a tree you can query
with CSS selectors, and it doesn't run a browser. For each <p>:
- Skip noise.
SKIP_PATTERNScovers lines in brackets, "NEW ENTRIES", "moved category" and the ◼️ emoji. - Strip the movement arrows (↑ ↓) from the start of the line.
- Split on the separator. The regex is
/\s+[-–—]\s+/, which matches a hyphen, en dash or em dash with spaces around it. The spaces matter: "Jay-Z" must not split. Only the first separator counts, so a title with a dash in it stays intact. - Reject editorial titles.
EDITORIAL_TITLE_PATTERNScatches lines like "Tracks from 'Trying Times'" and "selected tracks" (#4). These look likeArtist - Titlebut aren't songs.
Interesting fact: the album field exists in the data model (BBCSong.album)
and the matcher gives +1000 points for an album match, but the BBC page never
lists albums. That tier never fires in practice.
Date
The scraper tries to read <time datetime="..."> to learn which week the
playlist is for. That element doesn't exist on the page
(#14, open).
The code always falls back to "the most recent Monday" and logs a warning that
nobody reads. The real date is in the <h3>, written as a range:
29TH JUNE-5TH JULY 2026.
This matters less than it sounds, because the month the songs go into no longer comes from this date. It comes from the processing state (#10). It's still a silent lie in the logs, though.
Why axios + cheerio
| Option | Verdict | Why |
|---|---|---|
| axios + cheerio | Chosen | The page is static HTML, so cheerio is fast and light (no browser) |
| Puppeteer / Playwright | Rejected | Launches headless Chrome (100 MB+ RAM, slow). Only worth it if the page needs JavaScript to render |
| JSDOM | Rejected | A full DOM. Heavier than needed |
| Regex on raw HTML | Rejected | Brittle, and famously a bad idea |
Native fetch instead of axios |
Fair challenge | Node 18+ has fetch. Axios was chosen for timeouts and error objects, but fetch + AbortSignal.timeout() would do the job today with one fewer dependency |
If a reviewer asks "why axios in 2026?", the honest answer is the last row: it was a reasonable default when the plan was written, and it isn't worth replacing now.
How scraping breaks, and what protects you
Scrapers fail in three ways. It's worth knowing all three:
- Loudly. The container is missing, so we throw, so you get an email. That's the good case.
- Quietly wrong. The page changes shape but still parses. For example, editorial lines start being read as songs (#4), or the date guess is silently wrong (#14).
- Looks like success. The BBC's outage behaviour is a
302redirect to/error.html, which returns 200 OK with a "Service Temporarily Unavailable" page. Axios follows the redirect, sees 200, and reports success. Then parsing fails outside the retry loop, so there's no retry, and the error message says the layout changed when the real cause is an outage (#15, open, high priority). The fixturetests/fixtures/bbc-service-unavailable.htmlis already waiting for that fix.
The defence is fixtures: saved copies of real pages in tests/fixtures/
(sample, empty, malformed, outage) that the parser is tested against. When the
BBC changes the page, you save the new version as a fixture, watch the test go
red, then fix the parser. That's TDD applied to someone else's HTML.
Questions you might get asked
"Isn't scraping fragile?" Yes, and the design accepts that rather than hiding it. Parsing is isolated in one class and tested against saved real pages. If the container disappears the job fails loudly, and an email goes out. The remaining risk is quiet breakage, where the page changes but still parses. The open issues #14 and #15 are both examples of that, and they're the next things I'd fix.
"Is it legal or polite to scrape the BBC?" It's one request a week (plus retries) to a public page, with a User-Agent that identifies the bot, for a personal playlist. That's about as light as scraping gets. I'd check the BBC's terms before turning this into a public service.
"Why not use a headless browser to be safe?" Because the content is in the initial HTML. A browser would add around 100 MB of memory and a slow, flaky dependency to solve a problem we don't have. If the BBC moved to client-side rendering, that would be the moment to switch.
"How do you know the parser handles real-world input?" The fixtures are real pages, and every bug that came from a real page (editorial lines, an outage page) became a fixture and a test first.