Open-source playlist generator for hobbyist radio stations
Project description
Station Studio
An open-source radio station playlist generator. Describe how your station should sound (jingle every N tracks, no artist repeats inside a 10-track window, mix block of curated genres, weekend continuity from a separate source pool) — and Station Studio generates iTunes XML playlists you can drop straight into Music or iTunes.
Built for radio enthusiasts who run their own stations. Two real stations ship as examples; you can fork either or build your own from scratch.
Why this exists
If you've ever:
- Curated a radio station for friends / a Twitch stream / your own shower
- Wanted "shuffle, but smarter" — never the same artist twice in a row, weekend feels different from weekday, jingles at the right intervals, mix blocks of specific genres
- Tried to build that in iTunes smart playlists and hit the wall
…Station Studio is that wall, removed. The engine handles the constraint logic; you describe what you want.
What it does
Given:
- Your music, in iTunes XML format (export from Music / iTunes)
- A station template — a small JSON file describing what plays when (or built visually via the web UI, coming soon)
It produces:
- iTunes XML playlists — drop them back into Music / iTunes
- Per-pick traceability — every track placement is explained ("this track was picked because cursor for NIGHT_1 is at position 847 and this is the 12th slot of that block")
- Reproducibility — same template + same source files + same week number = same playlist, byte-for-byte
You can also visualise a station's template in the web UI — a chip-per-pick canvas grouped into sections, color-coded by source (or strategy), with click-through detail for every slot and macro. Click "🗺 Visualise template" on any station page. (Read-only today; in-canvas authoring is on the roadmap — see DESIGN.md §16.2.3+.)
Install
Three install channels, all first-class. Pick whichever matches what you've got installed:
| Channel | Prerequisites | Best for |
|---|---|---|
| Docker | docker | Trying it out, leaving it running on a NAS |
| Pip wheel | Python 3.11+ | Local-only / BYO-music, no Node setup |
| Dev clone | Python 3.11+, Node 22+, make |
Hacking on Station Studio itself |
All three serve the same UI + API and the same auto-seeded example stations. Pick one:
1. Docker (no Python, no Node)
git clone https://github.com/raby/station-studio.git
cd station-studio
docker compose up --build # first run builds; later runs reuse
Open http://localhost:8001. App-home (your DB + imported pools +
exports) lives in a docker-managed volume named
station-studio-data, so state survives docker compose down.
Inspect with docker volume inspect station-studio-data; back up
with:
docker run --rm -v station-studio-data:/data -v $PWD:/backup \
alpine tar czf /backup/data.tar.gz /data
The image is multi-stage (node-build → wheel-build → slim runtime, non-root, healthchecked), ~312MB. Same SvelteKit static SPA as the pip wheel — no divergent build path.
2. Pip wheel (Python only, BYO-music)
If you have Python 3.11+ and want a single-process app on
localhost, build a wheel and pip-install it:
git clone https://github.com/raby/station-studio.git
cd station-studio
make wheel # builds web/build + dist/*.whl
pip install dist/station_studio-*.whl
station-studio serve --host 127.0.0.1 --port 8001
make wheel runs the SvelteKit static build internally — you don't
need Node installed separately at runtime, only at build time. On
first run the CLI creates ~/.station-studio/ (override with
$STATION_STUDIO_HOME), copies the example stations, and runs
Alembic.
Once the first tagged release ships, this becomes:
pip install station-studio # straight from PyPI, no clone
station-studio serve
The PyPI publish workflow is wired (.github/workflows/publish.yml,
trusted-publishing via OIDC, test.pypi rehearsal before real PyPI);
the first release lands when project trust is configured on PyPI.
See CONTRIBUTING.md for the
cut-a-release procedure.
3. Dev clone (hack on it)
If you want to modify Station Studio or contribute upstream:
git clone https://github.com/raby/station-studio.git
cd station-studio
make install # creates .venv, installs Python + Node deps
make dev # FastAPI :8001 + SvelteKit :5173 with HMR
Open http://localhost:5173. Hot-reload on both backend and frontend; see CONTRIBUTING.md for the slice pattern + test discipline.
Single-worker for now. The async-generation job tracker (§16.11) holds job state in-memory per uvicorn worker. All three channels above use a single worker; if you reconfigure to
--workers N(N>1), the frontend's polling request can land on a worker that doesn't know about the job and you'll get spurious 404s. Stick to single-worker until DB-backed job state lands (§16.21+).
Loading your music
All four example stations (Starter + Stateful Starter + Rebirth
Classics + Rebirth Radio) are seeded automatically on first boot
— pick one in the sidebar. The two starter stations' three pools
(STARTER_CONTENT, STARTER_JINGLES, STATEFUL_CONTENT) also
auto-import from the bundled synthetic XMLs, so both starters work
end-to-end without any setup.
To import your own iTunes XMLs as source pools, open the Sources page (or click "Source pools →" from the landing). Drag-and-drop or pick an XML file, name the pool, choose a role (content / reference), and hit Import. Re-import to refresh, Delete to remove (§16.25).
Want to learn the manual import flow first? Go to the Sources
page and delete STARTER_CONTENT, then re-import it by uploading
stations/starter-content.itunes.xml. The starter pools work as a
real-shaped iTunes XML you can practice on before importing your own
library. (The starter station will silently re-import on next boot
if you leave both pools deleted, so it can't get stuck broken.)
In Music (macOS), export a playlist as XML via: File → Library → Export Playlist… → choose "XML" format. (In iTunes on Windows, same path.)
Advanced: many pools from a manifest (CLI). The batch path still
ships for power users — drop a JSON manifest at the shape of
tests/fixtures/sample_pool_manifest.json
and run (from the dev clone):
make import-weekly-pools manifest=path/to/your-manifest.json
Useful when you have a fixed pool list to import in CI / on a fresh box, or when re-bootstrapping a station from scratch. The UI path above is the recommended hobbyist flow.
Example stations included
Starter Station — stations/starter.json
- 168 slots / week (one per hour, Mon → Sun)
- Stateless cursors — no cross-week state
- Two strategies:
weighted(content) +jingle(every 8 slots) - Companion synthetic pools (
starter-content.itunes.xml,starter-jingles.itunes.xml) — 50 synthetic tracks across 10 artists + 5 jingle tracks. Auto-imported on first boot so the station works out of the box with no external data. - Read this first. Smallest, most approachable shape; perfect for "how does the slot+strategy model work?" before you fork anything bigger.
Stateful Starter — stations/stateful-starter.json
- 336 slots / week (one every 30 min, Mon → Sun)
- One persistent cursor advancing 24 positions/week, cycling every 14 weeks → exact pool coverage per cycle
- Two strategies:
sequential(most slots, cursor-driven) +prev_ref(sprinkled every 12 slots, reads last week's playlist at the same position) - Companion pool (
stateful-starter-content.itunes.xml) — 336 synthetic tracks across 14 artists, also auto-imported on first boot. - Read this second — your second station, with state.
Demonstrates cursor advance / cycle math + cross-week
prev_refsemantics on a focused teaching surface, with no row-template addressing to learn yet.
Rebirth Classics — stations/rebirth-classics.json
- 3,010 slots / week, 7 rows × 2 strategy blocks
- Single stateless cursor advancing 60 positions/week, cycling at 16
- Strategy:
windowonly (the simplest production case) - Read this third — first
row_templatestation, single-cursor mechanics in the per-day shape (vs the starters'flat_slotsflat list).
Rebirth Radio — stations/rebirth-radio.json
- 2,196 slots / week, all five strategies (
jingle,weighted,mix,prev_ref,sequential), nine stateful cursors with cross-week advance - Imported from
weekly/(a bespoke 1-station generator that Station Studio replaces) - Read this fourth (or as your reference for "can the engine do X?"). Forking from here is forking a complete production station — be ready for it.
Roll your own
The visual block editor (§16.2) will let you build a template by
dragging chips onto a timeline. Until that ships, the cleanest
starting point is stations/starter.json — copy it under a new
filename, adjust the slot count, swap in your own source names, and
drop it in ~/.station-studio/stations/. Restart serve and your
station appears in the sidebar. Read DESIGN.md §14.32 / §14.34 for
the full template grammar reference.
How it works
your iTunes XMLs ──► import into source pools
station template ──► describes what plays when
│
└──► engine.generator(template, pools, week, seed)
│
├──► picks: ordered list of (slot, track)
├──► trace: per-slot explanation
└──► persistence: Playlist + Trace rows
│
└──► export → iTunes XML
Seven strategies for placing a track at a slot:
window— take N consecutive tracks from a source at a cursor positionpick— one track from a source at a cursor positionjingle— random pick from genre-tagged subset (e.g. "Daytime jingle")weighted— pick from a source with artist-dedup + recent-tracks shufflemix— pick from a genre-tagged subset, excluding tracks used in last N weeksprev_ref— read this position's track from last week's playlistsequential— read N consecutive tracks at a named per-station cursor
Each strategy is a small pure-function module under engine/strategies/.
Project status
| Engine | Stable. Byte-equality verified across 30-week chains for two structurally distinct stations. |
| Backend API | Stable. FastAPI; full CRUD on stations + sources + rules. |
| Install | Wheel + docker-compose both ship and smoke-pass (§16.13). PyPI publish queued (§16.13.t1). |
| CLI | station-studio serve / import / seed / generate / export / backup ships with the wheel — full hobbyist subcommand set, no repo checkout required. Makefile targets remain for in-repo dev work (refresh, byte-equality fixtures, etc.). |
| Web UI | Wizard + trace viewer + Sources page + read-only template visualiser ship; in-canvas template editor in design (§16.2 — flagship feature). |
| Docs | Engineering docs in DESIGN.md; user-facing docs are growing. |
| License | MIT — fork freely. |
Contributing
Yes please. See CONTRIBUTING.md for slice pattern, test discipline, and how to propose new station shapes.
Architecture deep-dive
Engineering details — engine internals, persistence model, slice discipline, byte-equality test methodology — live in DESIGN.md. It's a long document; the table of contents at the top lists every §14.x slice that shipped.
If you're contributing engine work, also see
engine/divergence.py (slot-level diff
helpers) and engine/prior_seed.py (the
prior-seed validator + the import-prior contract) for how the
port acceptance tests catch regressions. tests/byte_equality.py
is a thin re-export shim for backward compatibility.
Acknowledgements
Station Studio is the next-generation replacement for weekly/
(Rebirth Radio's bespoke playlist generator, ~2017-present). The
strategies, template grammar, and most of the engine semantics are
ports of weekly/'s design — re-shaped to be expressive enough
for any station, not just Rebirth's specific shape.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file station_studio-1.5.0-py3-none-any.whl.
File metadata
- Download URL: station_studio-1.5.0-py3-none-any.whl
- Upload date:
- Size: 480.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d903cf5ec517224b017ffc4c0e8a439de48d5485940020227d89261ec7551c5
|
|
| MD5 |
675f130a565227446b7b4c474dd6897c
|
|
| BLAKE2b-256 |
4eaa78aaa9758c51475ccae1d7e10bab56533bb7b0f4084b84e20bc7150041c1
|
Provenance
The following attestation bundles were made for station_studio-1.5.0-py3-none-any.whl:
Publisher:
publish.yml on raby/station-studio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
station_studio-1.5.0-py3-none-any.whl -
Subject digest:
8d903cf5ec517224b017ffc4c0e8a439de48d5485940020227d89261ec7551c5 - Sigstore transparency entry: 2012116198
- Sigstore integration time:
-
Permalink:
raby/station-studio@8ed717b33f3883eba347465d36ad9576321af109 -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/raby
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8ed717b33f3883eba347465d36ad9576321af109 -
Trigger Event:
push
-
Statement type: