A shared playlist for a road trip: the owner sends a link, friends add tracks from Deezer and iTunes with previews, the service orders them fairly and exports to M3U / CSV / TXT. No sign-ups, no API keys. Python / FastAPI · Redis · React + Vite + Tailwind · Vercel — product, design, front end and back end, solo.
A group of friends is heading out on a road trip and everyone wants "their" music in the car. Usually one person makes the playlist and the rest put up with it. I needed a way to build a shared playlist with no sign-ups, in a couple of minutes, so everyone adds their own — and the result can be taken to any music service.
The first version did something else: it merged participants' existing playlists by link and looked for overlap. Measurements on real data showed the idea didn't work:
"Flipping the scenario removed all three problems at once: when a person picks a track from the catalogue, the track already has exact metadata, cover art and a preview. Nothing to identify, nothing to wait for."
That's the project's main lesson — sometimes the right move isn't to optimise the algorithm but to change where the data enters.
Search runs in parallel against Deezer and iTunes Search — both free, neither needs a key. Results are merged by the "artist + title" pair: duplicates collapse, the Deezer record gets its genre from iTunes. The result order is deliberately not re-sorted: an attempt to boost popular tracks put "The Weeknd — Starboy" first for the query "daft punk".
Nothing gets discarded here — every track was added by a real person, and dropping someone's pick means offending them. The algorithm only decides the sequence:
# Track the debt: how many of this participant's tracks should have played by now.
share = {who: len(items) / total for who, items in remaining.items()}
while any(remaining.values()):
position = len(ordered)
who = max(
(k for k in remaining if remaining[k]),
key=lambda k: (share[k] * (position + 1) - played[k], -first_seen[k]),
)
# Same artist back-to-back is the worst option — push it to the back of the queue.
best = min(
remaining[who],
key=lambda t: (_artist(t) == last_artist, _tempo_distance(t, previous)),
)
With a key present, a model (Gemma via the Google API) joins in: it suggests tracks in the spirit of the list and arranges the order for continuous listening. Two principles:
if sorted(indexes) != list(range(len(tracks))):
warnings.append(
"The model lost or duplicated tracks — the regular algorithm was used."
)
return None, warnings
return [tracks[i] for i in indexes], warnings
Without a key the service works in full.
RPUSH, the quota via INCR, so friends' parallel requests don't overwrite each other. A link lives for a week./api. Had to work out that public doesn't make it into the function bundle — the SPA route /p/:id is solved with a platform-level rewrite.A dark neutral background, thin borders, one accent gradient. A few decisions I'm not ashamed of:
<audio> per row — otherwise previews would play simultaneously.unicode-range: the Russian UI loads only the subset it needs, four files — about 140 KB. Unbounded for the heading, Golos Text for the UI, Geist Mono for numbers and durations.prefers-reduced-motion: content never depends on whether an animation ran.Python / FastAPI · Redis · React + Vite + Tailwind · Vercel (static + Python function)
A working service you can use with no sign-up and not a single API key. Three things I took from the project: measure a hypothesis on real data before polishing it; design so that AI errors are caught structurally, not by eye; and don't be afraid to throw away version one when the numbers say it doesn't work.