T/T
Back to work

Personal project · 2026

Dusty Grooves

If it ain't vinyl, it ain't real.

The Dusty Grooves storefront in daylight, with a tap-to-enter prompt at the bottom

What it is

Dusty Grooves is a browser music player disguised as a record shop that never made it out of 1983. You walk up to the storefront, step inside, and the owner, Big Tony, helps you find a record before dropping the needle on it.

Built in React, the app's personality comes from a small state machine. Every state maps to one of Tony's poses and lines, so searching, loading, playing and even failed searches feel like part of a conversation. No accounts, no playlists, no sign-up. Just search, pick a record and listen.

Big Tony waving hello inside the shop, beneath the pink GROOVES neon
Big Tony, greeting a first-timer like a regular.

The problem

Building the shop was the easy part. Getting sound out of it was the whole project.

My original plan was simple: find a free audio source and point an audio element at it. By the time I built the app, every service I intended to use had either disappeared or returned unusable links. The few that still worked generated URLs that browsers couldn't play directly because they only worked inside YouTube's own player.

I had a finished shopfront, a finished character and a working search, but no reliable way to play a track.

The decisions

Finding the song, not the stream

The solution was to stop chasing audio files altogether. Instead, each track is resolved to a YouTube video and played through YouTube's own player.

A small Cloudflare Worker queries community Invidious instances on the server, avoiding browser cross-origin restrictions and returning clean JSON. It keeps a short list of instances and automatically tries the next one if a host goes offline. Because that list lives in the Worker, I can swap hosts without redeploying the site.

Picking the right one

A search rarely returns a single obvious match. Results are often live performances, remixes, karaoke versions or hour-long loops, so every candidate is scored before playback.

Signal Weight
Track name in the title +10
Artist in the title or channel +10
"Official" in the title +5
Verified channel +3
View count, by tier +1 to +3
Remix, cover, karaoke, sped up −8
Live, concert, or acoustic −4
Under 30 seconds −10
Over ten minutes −6

Track and artist matches carry the most weight, while official uploads, verified channels and high view counts help identify the best candidate. If the highest-scoring result doesn't reach five points, the app refuses to play anything rather than risk choosing the wrong song.

Playing it

Playback happens through a hidden 1×1 YouTube IFrame player.

This is the biggest compromise in the project. YouTube doesn't offer an audio-only mode, so the app streams a tiny video while using only its sound. Forcing playback to 240p keeps bandwidth low, while the interface polls the player four times per second because the IFrame API doesn't provide a time-update event.

Cover art

Album artwork comes from Last.fm where available, falling back to the video's YouTube thumbnail and finally a default sleeve. Every track has artwork, even when music databases come up empty.

Big Tony listening on headphones while a track plays, the now playing bar open below him
A track playing, with the now playing bar and search dock open.

What I'd do differently

The weakest part of the project is playback recovery, and I know exactly why. When YouTube blocks a video, only a small number of error codes trigger another attempt. The most common failure, disabled embedding, isn't one of them. Worse, retries repeat the same search and usually land on the same blocked video.

The proper fix would be to keep the ranked results and move to the next candidate instead of starting over. I'd also treat blocked embeds as recoverable errors.

I'd cache successful matches too, since searching for the same track currently repeats the whole lookup process.

The larger issue, though, is the foundation itself. Playback depends on public Invidious instances that I don't control, and that list keeps shrinking. At the time of writing, only one reliable host remains. If I rebuilt the project today, I'd either pay for a supported audio API or narrow the scope to a fixed music library that I could host myself and guarantee would always work.

More work on the home page, or the full history on the CV.