Spotify Moodroom
A mood-based auto-queue I built for my own Spotify library. Six rooms, pools mined from my liked songs by an LLM, and an event-driven engine that learns from my skips and never lets Spotify autoplay decide what plays next.
Spotify autoplay always wins eventually.
Every queue I make runs dry. The last song ends, and Spotify falls back to its own recommendations — songs from people who listen like me, not the songs Iactually like. The vibe I set up forty minutes ago quietly disappears.
Spotify's recommender pushes you toward what people like you also listen to. Useful for discovery, terrible for vibe — the "people like me" cohort isn't my taste, it's an average of my taste cluster.
A playlist on shuffle has no idea you skipped "Take Time" for the third time. Tomorrow it's in the same place, ready to be skipped again.
When a fixed playlist ends, Spotify autoplay kicks in. The thing you set up to control your mood quietly hands itself back to the recommender after track ten.
can the curation be done by a system tuned to one library specifically — mine — learning from my skips and surviving the moment a playlist would otherwise end?
From my liked songs to a queue that never dies.
Six steps. Two of them human (mine), four of them the system doing exactly what the human told it to. If you fork the repo and point it at your account, your library flows through the same pipeline and the rooms you define become yours.
My Spotify library goes in.
One OAuth flow against my own Spotify account, then every track in my liked songs is pulled into a local SQLite snapshot. 246 tracks for the first build.
I define the rooms by hand.
Six rooms — Late Night, Throwback Pop, House, Rage, Throwback Rap, After Hours. Naming them is the only strategic work I do; everything below this is the system.
The library gets mined into the right rooms.
An LLM (Claude, via OpenRouter) places each of my liked tracks into the room it fits. Marvins Room → Late Night. SICKO MODE → Rage. 180+ of my 246 liked tracks landed somewhere.
A weighted sampler picks 10 tracks per click.
Each click pulls 5 CONFIRMED (mined from my library) · 4 SAFE_SIDE (genre-correct candidates I haven't liked yet) · 1 DISCOVERY (a fresh swing). No track repeats within a 15-pick rolling window.
An event-driven loop keeps the queue alive.
A background Python thread polls Spotify every six seconds. When my current track changes, one fresh track is added to the queue. Autoplay never gets a turn.
My skips teach the pool what doesn't belong.
Skip a CONFIRMED track in under 30s → it gets demoted to SAFE_SIDE on the first strike, removed on the second. Survive a full play and a DISCOVERY track auto-promotes into SAFE_SIDE. The pool learns from how I actually listen, not from a survey.
Six rooms. My library mined into all of them.
The hero · six rooms.
Six mood cards in a 3×2 grid. Aurora drifting in indigo. Each card has its own colored hover glow. Picking a room is the only choice I have to make all night.
The click · everything responds.
I click Late Night. Five cards fade. The chosen card scales up. Aurora crossfades to a deeper indigo. The now-playing card materializes at the top — and Spotify on my laptop starts the first track.
The card · CONFIRMED, with a 0:30 tick.
Track name in big serif. A bucket badge reads CONFIRMED — meaning this track came from my own liked songs, mined into Late Night. The progress bar has an amber tick at 0:30 — past it, a skip is natural and the engine ignores it.
The queue · CONF · SAFE · DISC.
Ten upcoming tracks as a numbered manifest. Each row carries a small badge — CONF (indigo, from my library), SAFE (amber, room-correct candidate I haven't liked yet), DISC (gold ✦, a fresh swing). I can see which pool every track came from before it plays.
The skip · the engine reacts in place.
I skip a CONFIRMED track at 0:12 in Spotify. A notice slides in above the now-playing: 'last skip · first skip · at 0:12 of 3:42 · CONFIRMED → DEMOTED to SAFE_SIDE'. The pool just rewrote itself based on something I did. No survey, no thumbs-down button.
The auto-queue · invisible top-up.
A track ends. A background Python loop notices the track_id changed and adds one fresh pick to the bottom of the manifest. Spotify autoplay never gets a turn. As long as my laptop is on, the room never runs dry.
Skip-demote — learns from playing, not asking.
A skip in <30s writes to a state.json overlay: CONFIRMED → SAFE_SIDE on first strike, SAFE → removed on second. No surveys, no taste questionnaires. The signal is whether I let the song play.
Discovery slot — the pool grows itself.
One in ten picks is from a fresh DISCOVERY_POOL — a track I've never liked, but which fits the room on paper. Survive a full play and the system auto-promotes it into SAFE_SIDE for next time.
Library mining — my taste, not collaborative filtering.
Each room's CONFIRMED pool was mined from my own liked tracks via an LLM scan against the room definition. 45 of 64 Late Night tracks were already in my library — I just hadn't grouped them this way.
Event-driven top-up — autoplay never wins.
A background loop polls current_playback every six seconds. When the track_id changes, that's the signal — one fresh pick joins the queue, replacing the slot Spotify autoplay would have filled.
No back-to-back repeats — rolling dedup window.
Every queued track is remembered in a 15-entry rolling deque. The sampler filters that set out before each pick, so the room never trips into the same song twice in a row.
Build 01 · the system at rest.
Numbers after the first build, the library-mining pass, five Late Night calibration rounds, and the post-build audit that cut 13 off-room tracks. The runtime is local — a Flask app on my laptop talking to the Spotify Web API. It only auto-queues while I have it open. Cloud deploy is Build 02.
On the first audit pass against Late Night's 146-track pool, 13 tracks were removed for off-vibe — including Knife Talk (aggressive Memphis trap, belongs in Rage), Cry For Me (recent Weeknd disco-pop, off-texture), and Pyramids (10-minute multi-movement epic, structural mismatch). Lollipop and Mystery Lady were flagged, then kept on closer listen. The audit was me listening, not the system — the system's job is to stop putting them in the queue once I skip them.
Build 01 scope, and what comes next.
Build 01 is the proof of concept and the version I actually use. The roadmap is the production-readiness path for me — not a scale-it-to-everyone plan. This stays a personal tool. If you want it for yourself, the source is on GitHub; the hosted version is just my laptop.
| Build | Scope | Status |
|---|---|---|
| Build 01 | Six lanes · auto-queue · skip-demote · discovery slot · audit | Shipped |
| Build 02 | Always-on cloud deploy (Fly.io) · cross-device · custom domain | Planned |
| Build 03 | Web Playback SDK · browser playback without a Premium device | Planned |
| Build 04 | Lane export · publish a calibrated pool as a Spotify playlist | Planned |
| Build 05 | Mobile-first UI · gestures, swipe-to-skip-and-demote | Backlog |
Subhankar Shukla.
Third-year Rotman Commerce student at the University of Toronto, CFA Level 2 candidate. Builds AI-powered tooling on the side. Recent production projects include AXIOM (institutional valuation platform) and the Wego Earnings Digest.
Moodroom is a personal tool. It exists because I wanted it for myself — not as a startup, not as a product to sell. The source is open: anyone with a Spotify Premium account can clone it and run the same pipeline against their own library. And the patterns inside (skip-demote learning, library-mining via LLM, event-driven top-up so autoplay never wins) are things Spotify could implement directly. If someone there sees this and ships any of it, that's a good outcome too.
Independent project. Not affiliated with Spotify.
© Subhankar Shukla, 2026.