One new music genre every week — chosen by a Bayesian model that learns what you like.
Spotify killed its recommendation API. So this builds its own, from a frozen map of 6,291 genres.
Español · How it works · Install · Dead APIs, and what to use instead
Music discovery got worse. Not because the algorithms got dumber, but because the good ones became inaccessible:
- November 2024 — Spotify deprecated
audio-features,audio-analysis,recommendationsandrelated-artistsfor all new applications. No replacement, no waitlist. - February 2026 — a second round removed more catalog endpoints and capped search results at 10.
- December 2023 — Every Noise at Once, the best genre map ever built, stopped updating when its creator was laid off from Spotify.
So the classic "build a recommender on danceability, energy and valence" recipe is dead. This project takes the opposite route: it reconstructs a genre space from what's still reachable, and puts a small, honest model on top of it.
The result is a weekly ritual. Every Friday it picks a genre you have never heard, builds a playlist on Spotify and YouTube Music, writes you a briefing explaining why it chose that genre for you, and asks you to rate it. Your rating moves the whole model.
Each of the 6,291 genres becomes a 70-dimensional vector:
| Dimensions | Source | What it captures |
|---|---|---|
| 5 | Every Noise map: x, y, and the RGB of each label |
Spotify's original projection over timbre, energy and organic-vs-electronic |
| 1 | Font size of the label | Genre popularity — inverted, this is the rarity index |
| 64 | Truncated SVD over the genre × artist matrix, TF-IDF weighted | Which genres share an audience — item-item collaborative filtering |
The collaborative part is the interesting one. Every genre page lists ~200 representative
artists, ranked by how central they are to that genre. That gives a sparse matrix of
629,049 genre-artist links across 468,755 artists. Weight each artist by 1/log(2+rank),
apply IDF so that artists appearing in hundreds of genres count less, normalize each row, then
take the top 64 singular vectors.
You can audit the result directly:
$ sonda neighbors "bossa nova"
velha guarda · samba · brazilian jazz · violao · samba-jazz
$ sonda neighbors "vaporwave"
mallsoft · chiptune · abstract idm · deconstructed club · uk experimental electronicThat map is frozen at 2023-11-19, which does not matter here: obscure genres don't expire.
Preference is modeled as Bayesian linear regression:
$$y = \mathbf{w}^\top \phi(g) + \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, \beta^{-1}), \qquad \mathbf{w} \sim \mathcal{N}(0, \alpha^{-1} I)$$
$$\Sigma = \left(\alpha I + \beta \Phi^\top \Phi\right)^{-1}, \qquad \mathbf{m} = \beta \Sigma \Phi^\top \mathbf{y}$$
Why not a neural network? One recommendation per week is ~50 explicit labels per year. On that budget a network memorizes instead of generalizing. A linear model with an explicit posterior buys three things that matter more here than capacity:
- It quantifies its own uncertainty, so it knows when it is guessing.
- It enables Thompson Sampling (below), which explores precisely where it is ignorant.
- It stays interpretable — the weights say which dimensions of sound you respond to.
Setting $\alpha = d$ keeps the prior calibrated to the rating scale: with standardized features, $\mathbb{E}[(\mathbf{w}^\top x)^2] = \lVert x \rVert^2 / \alpha \approx 1$, which is exactly the range of $y \in [-1, 1]$. With $\alpha = 1$ the model believes a priori in ratings of ±8 and extrapolates wildly outside the region where it has data — an early version of this predicted −2.88 on a −1..1 scale.
Listening history is folded in as weighted implicit evidence: for each genre, the fraction of its representative artists you actually listen to, weighted by how central each artist is. It only produces positive signal — "I play this a lot" is not proof of dislike — so it enters with lower confidence than an explicit rating.
Instead of picking the highest posterior mean, Thompson Sampling draws one weight vector $\tilde{\mathbf{w}} \sim \mathcal{N}(\mathbf{m}, \Sigma)$ and picks the best genre under that draw. Where the model is confident, draws agree and it exploits. Where it isn't, draws vary wildly and an unknown genre wins. Exploration falls out of the uncertainty itself — no epsilon, no temperature to tune.
This one was learned the hard way. With rarity as a soft term in the objective, expected utility always won and the system recommended metal and thrash metal — rarity 0.02, the most mainstream labels on the map. A discovery tool that recommends metal discovers nothing. Rarity became a hard constraint.
Novelty had the same problem. Measuring distance to the centroid of everything you listen to is meaningless — the centroid of 1,161 genres is an average point that corresponds to no real music, which made "metal" look as novel as gamelan. Novelty is now the distance to your nearest known genre.
The final selection is:
$$\text{score}(g) = z\big(\tilde{\mathbf{w}}^\top \phi(g)\big) + \lambda, z(\text{rarity}) + \mu, z(\text{novelty})$$
subject to hard exclusions: nothing you already listen to (3+ artists in it), nothing you have rated, nothing recommended before, and nothing below the rarity threshold.
The cold start is solved by farthest point sampling: each survey question is the genre
furthest from every genre already asked, so 25 questions cover the entire map instead of 25
variants of one corner. Each plays a 30-second sample through afplay.
A real draw: ugandan hip hop · louisiana blues · carnatic · russian punk · iskelmä · deathgrind · EBM · neurofunk · bluegrass · big band. No two are alike — that is the point.
First real recommendation, on a listener whose history is Radiohead, The Weeknd, Kendrick Lamar and Rex Orange County:
| Recommended | melodic black metal — rarity 0.98 |
| Model predicted | +0.22 ± 0.60 |
| Actually rated | +0.6 ✅ within the predictive interval |
The reasoning was legible: the calibration survey put psychedelic doom at +1.0 and
symphonic black metal at +0.6 while sinking every electronic genre (EBM, techno, drill and
bass, tribal house, neurofunk — all −0.4). The model encoded that as a negative weight on
color_b, the Every Noise channel for electric and atmospheric. Not a list of banned genres —
a direction in space.
Requires macOS 14+, Python 3.12, and uv.
git clone https://github.com/Chere3/sonda.git
cd sonda
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e .
sonda init
sonda sync map # 6,291 genres
sonda sync artists # ~200 artists per genre; resumable, ~75 minConnect your accounts. Last.fm only needs an API key from last.fm/api/account/create (instant, no review):
sonda auth lastfm --user YOUR_USER --key YOUR_KEY
sonda auth spotify # OAuth PKCE, local callback server
sonda import lastfm && sonda import spotify
sonda buildThen calibrate and go:
sonda survey --n 25 # listen and rate; this is what gives the model negatives
sonda run-week # pick a genre, build both playlists, write the briefing
sonda rate "genre" 0.8 --note "why"sonda schedule install # Friday 09:00, retries Friday 19:00 and Saturday 13:00Uses launchd's StartCalendarInterval, not an interval: if the Mac is asleep at the scheduled
time, launchd runs the job on wake instead of losing the week. run-week is idempotent, so the
extra triggers are free insurance.
sonda app installSwiftUI, built with SwiftPM — no Xcode required. It tints itself with the color Every Noise assigns to the week's genre, and holds no logic of its own: it asks the CLI for JSON.
flowchart LR
EN[Every Noise at Once<br/>6,291 genres] --> SP[Genre space<br/>70 dimensions]
LF[Last.fm<br/>scrobbles] --> PR[Profile<br/>weighted coverage]
SPO[Spotify<br/>top artists] --> PR
PR --> M[Bayesian model<br/>+ Thompson Sampling]
SUR[Calibration survey] --> M
SP --> M
M --> SEL[Weekly pick<br/>utility + rarity + novelty]
SEL --> PL1[Spotify playlist]
SEL --> PL2[YouTube Music playlist]
SEL --> DOC[Briefing written by Claude]
PL1 & PL2 & DOC --> APP[Menu bar app]
APP -->|your rating| M
The weekly briefing is written by Claude through the Agent SDK. It receives only verified data — real artists with their Last.fm listener counts and tags, the model's metrics, your previous ratings — and is explicitly forbidden from inventing dates, discographies or history. A briefing with fabricated facts is worse than no briefing: it poisons trust in the whole system.
Everything below was verified against live endpoints in September 2026, not recalled from memory. If you are building anything on music APIs, this table may save you a day:
| Endpoint | Status | What this project does instead |
|---|---|---|
GET /audio-features, /audio-analysis |
Dead for new apps since Nov 2024 | Every Noise geometry + SVD over shared artists |
GET /recommendations, /related-artists |
Dead for new apps since Nov 2024 | Own Bayesian model |
GET /artists/{id}/top-tracks |
403 Forbidden | GET /search with the artist: filter, discarding results whose artist ID doesn't match |
POST /playlists/{id}/tracks |
Deprecated Feb 2026 | POST /playlists/{id}/items |
POST /users/{id}/playlists |
Moved | POST /me/playlists |
GET /artists/{id} |
Still alive, but no longer returns genres or popularity |
Genres come from Every Noise |
GET /search |
Alive, limit capped at 10 (was 50) |
Query per artist instead of in bulk |
| YouTube Music OAuth | Valid token, 400 on the internal endpoint (#676) | Browser header authentication |
Ideas that make recommendations weirder and more accurate are the most welcome. Better genre features, other data sources (MusicBrainz, ListenBrainz, the AcousticBrainz dumps), smarter exploration strategies, other platforms.
See CONTRIBUTING.md. One rule matters above all: verify external APIs against their current documentation, never from memory. Half the work in this repository was routing around endpoints that quietly died.
Built on Every Noise at Once by Glenn McDonald, which mapped 6,291 genre-shaped distinctions before Spotify let him go. This project is a small argument that the map should outlive the company that stopped paying for it.
Scraping is rate-limited and cached; please keep it that way.
MIT — see LICENSE.




0 comments
log in to comment.