musicmedia

PlaylistFM

The behind the scenes tool NPR's system never had. Four radio streams, and every song logged properly enough to keep the station legal.

PlaylistFM interface

01

The Problem

Most public radio runs on Grove, NPR's system for running a website. It's good for a news organisation: articles, podcasts, events, donors.

Radio Milwaukee is a different animal, and so are KEXP, KCRW and The Current. The content isn't articles, it's a continuous stream of songs, and the questions the audience asks are what's playing right now, what did you play last week, and who should I listen to next. Grove was never built to answer those, sensibly enough. For years the answer was bolted on widgets, outside services and spreadsheets, each one a compromise.

Behind the scenes it was worse. Four stations play tens of thousands of songs a quarter, and every one has to end up as a correct record with the right artist, title, album, length and record label before SoundExchange, the organisation that collects music royalties, will accept the report that lets the station keep playing music.

Music directors sorted that out in spreadsheets, email and memory, and half an hour of every morning went to it before anyone made a decision.

02

The AI Design Decision

Three decisions, and the first one is that no model touches the data. A record label is legally consequential: a wrong one is a failed filing, and a language model asked for a record label will always return a plausible one, because plausible is what the objective rewards. Enrichment is a deterministic waterfall instead, Apple Music, then Discogs with a deluxe and remaster retry, then MusicBrainz, then Discogs on the artist alone, and finally the literal string Self-released for 414 Music, which is what SoundExchange accepts for a local unsigned act. That last tier is not a fallback, it is domain knowledge, and knowing which default is legally correct is the part a general purpose model cannot supply. Second, the plan was multi tenant from day one and I reversed it in writing: the multi tenancy tax is real and enumerable, Radio Milwaukee was the only confirmed customer, and it took about six of twenty three open questions off the critical path. Every table still carries an orgId as a constant, so a second station costs no migration. Third, and this is the one that made the project possible at all: I am not a software engineer, and nearly every commit was written with Claude Code. The hard parts were never the typing. What should the interface prioritise, where does the live event row go, what is the right operator workflow when a track fails to resolve, those came from twenty years of watching what music directors actually do. The AI is fast at typing. It is not the one with the taste.

03

The Architecture

  1. 01Check what each of the four stations is playing, every minute
  2. 02put every song into the same shape so the rest of the system only has to learn one
  3. 03look the song up in three music databases in a fixed order, so the answer is never a guess
  4. 04show the staff only the songs that came back wrong
  5. 05the same clean list feeds the public playlist and the quarterly legal report

04

The Responsible AI Constraint

There's no AI in this product, and that's the point. If a song's record label is wrong, the station's filing is wrong, and a language model asked for a record label will always hand you something that looks right.

So nothing here invents a value. Each source either knows the answer or passes the question along, and the last step is a fixed answer that's legally correct for a local unsigned artist. The report won't generate at all if there are songs it couldn't sort out, and there's no override button, because a report that quietly goes out incomplete is worse than one that refuses.

What the system does instead of guessing is ask a person, and it's careful about how it asks. Failures are grouped so one click fixes a whole batch. Dismissing something makes it stay dismissed, so the same nuisance doesn't come back every hour. And 414 Music's low label coverage is marked as expected rather than broken, so the real problems stay visible.

An alert that cries wolf teaches the person to stop reading alerts, and then it's worse than having none.

05

The Result

Live and in daily use. The public playlist runs on radiomilwaukee.org and the partner widget and demo are at playlistfm.app, across 88Nine, HYFIN, Rhythm Lab Radio and 414 Music. The numbers worth quoting are the staff ones. The music director's morning cleanup went from about thirty minutes to about ninety seconds. Building the quarterly legal report went from roughly half a day of assembling a spreadsheet to picking a station, picking dates, and clicking download. About 95 percent of songs now show real cover art, and the ones that don't are mostly local artists who aren't on the streaming services yet. The playlist updates a second or two after the song changes on air, with nothing to refresh. Any partner station can drop the whole thing onto their own site with one line of code, and it takes on their fonts automatically.

06

If you remember one thing

There's no AI anywhere in this product. A made-up record label is a failed legal filing, so every lookup is fixed and predictable, and the last step is an answer that's correct by law rather than a guess.

Tech Stack

  • Convex
  • Next.js 16
  • React 19
  • Preact (embed bundle)
  • Trigger.dev
  • Fly.io (ICY worker)
  • Cloudflare Pages
  • Clerk
  • Apple Music API
  • MusicBrainz
  • Discogs
  • Bun

Links