Skip to content

gnujoow/spotify-mixmaster

Repository files navigation

spotify-mixmaster

Building a playlist takes real effort. For a DJ, the work is curation — picking the right tracks from a list, in the right order. spotify-mixmaster does it automatically from your Spotify Liked Songs, by genre and mood.

A beam search assembles the tracklist from harmonic mixing (Camelot wheel), BPM flow, and an energy curve.

Use it as a CLI, or hook it up to hermes-agent and ask in natural language.

🎧 Example: chill jazz house — 28 tracks · 1 hr 33 min, an actual set built with resequence

 6. [ 5A 124.0 E0.92 2022] Work It Out     Trimtone     disco house  5A→5A same(1.00) Δbpm 0.0%
 7. [ 5A 126.0 E0.83 2023] Sambossa        Bellaire     disco house  5A→5A same(1.00) Δbpm 1.6%
★8. [ 6A 129.8 E0.95 2007] Girls Like Us   B15 Project  bassline     5A→6A up1(0.90) Δbpm 3.0%

Features

  • Harmonic mixing — directional Camelot wheel scoring (same / up1 / relative / boost …)
  • BPM flow — ±6% transitions with half/double-time matching (87 ≈ 174)
  • Energy curves — classic (late peak) / linear / wave / flat, or define your own in YAML
  • Genre buckets + DJ slots — 40-ish Beatport-style buckets, opener-to-closer slot classification
  • Draft workflow — drop a few favorite tracks into a playlist and the sequencer places them as landmarks, filling the gaps around them
  • Quality floor — refuses to force bad transitions; ends the set short rather than padding it
  • Reproducible — every run records its seed; same seed = same set
  • All rules (weights, constraints, curves, buckets) live in config/*.yaml — tune without touching code

APIs used

API Purpose Free tier
Spotify Web API Liked Songs sync, playlist creation (OAuth PKCE) ~600 req/day in Dev Mode (measured)
FreqBlog BPM · Camelot key · energy · mood (primary enrichment) 1,000 req/month, ~50 on-demand analyses/day
Last.fm Artist tags → genre classification generous
MusicBrainz Artist country (--country filter) 1 req/s, no key
Musicae (Apify actor) — optional Per-track genres, 9 DJ scores ~1,000 tracks/month on Apify credits

Why third-party APIs? Spotify blocked Audio Features / Audio Analysis / Recommendations for new apps on 2024-11-27, with no replacement. The Feb 2026 changes also removed batch artist lookups and effectively killed the genres field — so BPM/key/energy come from FreqBlog and genres from Last.fm tags. Details in docs/api-responses.md.

Prerequisites

  • uv + Python 3.12+
  • A Spotify account and an app Client ID from the developer dashboard (setup below)
  • A FreqBlog API key — BPM/key/energy enrichment
  • A Last.fm API key — artist tags
  • (Optional) An Apify account — Musicae per-track genre / DJ-score batch analysis

Install

git clone https://github.com/gnujoow/spotify-mixmaster.git
cd spotify-mixmaster
uv sync

Setup

1. Register a Spotify apphttps://developer.spotify.com/dashboard → Create app:

  • Redirect URI: http://127.0.0.1:8888/callback (localhost is rejected for new apps — it must be 127.0.0.1)
  • If asked which APIs you need, check Web API
  • Copy the Client ID (PKCE flow — no client secret needed)

2. Config file

cp config.example.toml config.toml

Fill in your Client ID and API keys:

[spotify]
client_id = "..."

[enrichment]
freqblog_api_key = "..."   # BPM/key/energy enrichment
lastfm_api_key = "..."     # artist tags (https://www.last.fm/api/account/create)
# musicae_api_key = "..."  # optional

Run

uv run mixmaster auth              # 1. Spotify login (browser, once)
uv run mixmaster sync              # 2. pull Liked Songs + artist tags
uv run mixmaster enrich --limit 30 # 3. BPM/key/energy enrichment (daily drip)
uv run mixmaster classify          # 4. genre buckets + DJ slots
uv run mixmaster stats             # check progress
  • Every command is resumable — interrupt it, run it again, it picks up where it left off (idempotent upserts, graceful stop on 429)
  • enrich --limit 30 matches the FreqBlog free tier (1,000/month). Running without --limit can burn a month of quota in a day
  • sync is incremental; use sync --full to also reflect un-liked tracks

Build a set

# 60 minutes of deep house, warmup→peak→cooldown curve, compare 3 candidates
uv run mixmaster sequence --minutes 60 --curve classic --candidates 3 \
  --bucket deep_house --bucket organic_house_downtempo

# Recreate the candidate you liked by seed, publish as a private playlist
uv run mixmaster sequence --minutes 60 --curve classic \
  --bucket deep_house --seed 549427 --create "Deep House 60"

# Draft workflow: tracks in this playlist are guaranteed in the set
uv run mixmaster sequence --minutes 60 --from-playlist <URL>

# Reorder an existing playlist by the sequencing rules (dry-run by default)
uv run mixmaster resequence <URL> [--apply | --create "name"]
Option What it does
--minutes N / --tracks N Set length (minutes or track count)
--bucket X Genre bucket filter (repeat = OR, k+rnb = intersection)
--slot X DJ slot filter (opener/warmup/peak/afterhours/closer)
--curve X Energy curve (classic/linear/wave/flat)
--country CC Artist country (e.g. KR)
--candidates N Generate and compare N candidates
--seed N Reproduce (same conditions + seed = same set)
--seed-track ID Start from this track
--require-track ID / --from-playlist REF Draft tracks, always included (marked ★)
--create "name" Publish as a private playlist
--json / --json-out PATH Structured output (for agent integrations)

hermes-agent integration

If you run hermes-agent, register the skill and trigger set generation in natural language:

Headless operation

PKCE means there is no client secret. Run mixmaster auth on a machine with a browser, copy data/tokens.json to the server, and token refresh works headless from then on.

  • ⚠️ Never refresh the same token from two machines — refresh token rotation will invalidate one side. Operate from one place only.
  • Suggested cron: daily syncenrich --limit 30stats (right after 00:00 UTC, when the FreqBlog daily quota resets)
  • The DB uses WAL mode — before copying it to another machine, run sqlite3 data/mixmaster.db "PRAGMA wal_checkpoint(TRUNCATE)"

Layout

src/mixmaster/
├── cli.py            # auth/sync/enrich/classify/import-musicae/sequence/resequence/stats
├── sync.py           # collection: Liked Songs + Last.fm artist tags
├── enrich.py         # enrichment state machine (drip backfill, quota handling)
├── enrichment/       # adapters: freqblog.py + musicae.py
├── classify.py       # YAML rules → genre buckets / DJ slots
├── sequence.py       # beam-search sequencing + report + publish + reorder
├── musicbrainz.py    # artist country
├── lastfm.py         # artist.getTopTags
├── db.py             # SQLite (user_version migrations, WAL)
└── spotify/          # OAuth PKCE + API client (retry/quota handling)

config/sequence.yaml  # sequencing rules — meant to be edited
config/classify.yaml  # classification rules — meant to be edited
docs/phase4-design.md # sequencing design, research, decision log

Tests

uv run pytest   # 145 tests

License

MIT

About

DJ-style playlist generator for your Spotify Liked Songs — harmonic mixing (Camelot wheel), BPM flow, and energy curves via beam search

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors