Spotify Moodroom · Build 01 · 2026
Personal tool · 6 rooms · 670+ tracks · runs on my laptop
Build 01

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.

Built for myself, over a few sessions, to fix the moment when a Spotify playlist runs dry and autoplay takes over. The source is open — anyone with a Premium account can fork it and re-mine it against their own liked songs. The patterns inside (skip-demote, library mining, event-driven top-up) are also things Spotify could implement directly. This is what they look like working.
Built by Subhankar Shukla. Independent project — not affiliated with Spotify. Runs locally on my Mac against my own Spotify account; no hosted service, no users.
Why I built it

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.

Three failure modes I kept hitting — and decided to fix
01 · Centroid bias

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.

02 · No skip feedback

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.

03 · Queue collapse

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.

So the question becomes

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?

How it works

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.

The pipeline
01
Input

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.

02
Human

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.

03
AI

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.

04
System

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.

05
System

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.

06
System

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.

Step 02 in detail · the rooms I picked

Six rooms. My library mined into all of them.

01
late night
OVO atmosphere
64 saved · 34 pool · 35 fresh
02
throwback pop
2010 radio
9 saved · 62 pool · 29 fresh
03
house
afro & Keinemusik
7 saved · 30 pool · 15 fresh
04
rage
high-energy trap
42 saved · 26 pool · 18 fresh
05
throwback rap
2010s anthems
52 saved · 40 pool · 16 fresh
06
after hours
cinematic electronic
31 saved · 29 pool · 15 fresh
Walkthrough · 01 of 6
01

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.

Walkthrough · 02 of 6
02

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.

Walkthrough · 03 of 6
03

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.

Walkthrough · 04 of 6
04

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.

Walkthrough · 05 of 6
05

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.

Walkthrough · 06 of 6
06

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.

Design choice · 01 of 5
─── why this isn't just a playlist ───
01

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.

Design choice · 02 of 5
02

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.

Design choice · 03 of 5
03

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.

Design choice · 04 of 5
04

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.

Design choice · 05 of 5
05

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.

What it produced

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.

6
Rooms calibrated
670+
Tracks across all pools
246
My liked songs scanned
180+
Library tracks mined into rooms
20
Validated picks (Late Night)
5
Calibration rounds (Late Night)
13
Audit pass — misfits removed
Local
Where it runs
One audit example

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.

Coverage

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.

BuildScopeStatus
Build 01Six lanes · auto-queue · skip-demote · discovery slot · auditShipped
Build 02Always-on cloud deploy (Fly.io) · cross-device · custom domainPlanned
Build 03Web Playback SDK · browser playback without a Premium devicePlanned
Build 04Lane export · publish a calibrated pool as a Spotify playlistPlanned
Build 05Mobile-first UI · gestures, swipe-to-skip-and-demoteBacklog
About

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.