<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Freqblog</title>
    <description>The latest articles on DEV Community by Freqblog (@birrings).</description>
    <link>https://dev.to/birrings</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873922%2F4ee0520d-f25c-4231-b050-9aaf049e8ae7.png</url>
      <title>DEV Community: Freqblog</title>
      <link>https://dev.to/birrings</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/birrings"/>
    <language>en</language>
    <item>
      <title>Auto-Tag Your Music Library — With Confidence Scores</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:49:13 +0000</pubDate>
      <link>https://dev.to/birrings/auto-tag-your-music-library-with-confidence-scores-17d</link>
      <guid>https://dev.to/birrings/auto-tag-your-music-library-with-confidence-scores-17d</guid>
      <description>&lt;p&gt;"Tag my whole catalogue by mood, energy and genre" is one of those jobs that sounds like a weekend script and turns into a procurement cycle. The enterprise tagging tools — Cyanite is the one people name — do the job well, but the shape of the deal is the friction: you upload your audio, you sign up for a seat, and what comes back is a confident flat label (&lt;em&gt;energetic&lt;/em&gt;, &lt;em&gt;happy&lt;/em&gt;, &lt;em&gt;techno&lt;/em&gt;) with no indication of &lt;em&gt;how&lt;/em&gt; it was decided or how much to trust any single tag. For a side project, an internal catalogue tool, or a feature you're still prototyping, that's a lot of ceremony for some adjectives.&lt;/p&gt;

&lt;p&gt;This post is a practical walkthrough of the self-serve alternative: tag tracks by name (no audio upload) in about 30 lines of Python against the FreqBlog Music API's &lt;code&gt;GET /tag&lt;/code&gt; — and, crucially, get a &lt;strong&gt;confidence&lt;/strong&gt; and a &lt;strong&gt;provenance&lt;/strong&gt; on every tag, so you know which ones are a measurement and which ones are a hint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What &lt;code&gt;/tag&lt;/code&gt; actually is.&lt;/strong&gt; It's a tag-shaped &lt;em&gt;projection&lt;/em&gt; of the same open-data analysis &lt;code&gt;/lookup&lt;/code&gt; already returns — no audio upload, no new compute on your request. If you want the full numeric feature set (raw bpm, key, the [0,1] floats), use &lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;&lt;code&gt;/lookup&lt;/code&gt;&lt;/a&gt;; if you want the nearest-sounding tracks to a seed, use &lt;code&gt;/similar&lt;/code&gt;. &lt;code&gt;/tag&lt;/code&gt; is the "just give me the labels, honestly" surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why honesty is the feature
&lt;/h2&gt;

&lt;p&gt;Audio tagging is not one problem — it's a stack of problems with very different reliability. A track's energy or danceability is a &lt;em&gt;measurement&lt;/em&gt; off the waveform: reproducible, defensible, and the same every time. A one-word mood is a &lt;em&gt;derivation&lt;/em&gt; from those measurements via a rule. A fine-grained mood like "aggressive: 0.71" is a &lt;em&gt;model estimate&lt;/em&gt; from a research-grade classifier that was honest enough about its own limits that its authors deprecated it. Genre is a broad catalogue tag, not a taxonomy you should bet a recommender on.&lt;/p&gt;

&lt;p&gt;Most taggers flatten all four into the same confident-looking string. FreqBlog's &lt;code&gt;/tag&lt;/code&gt; refuses to: every tag in the response carries a &lt;code&gt;confidence&lt;/code&gt; and a &lt;code&gt;provenance&lt;/code&gt;, so a measured &lt;code&gt;high-energy&lt;/code&gt; and a model-estimated &lt;code&gt;aggressive&lt;/code&gt; never look equally authoritative. That's the differentiator — not "more tags," but tags you can &lt;em&gt;reason about&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The call — one track, in
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET /tag&lt;/code&gt; resolves a track by exactly one of: &lt;code&gt;track&lt;/code&gt; (plus optional &lt;code&gt;artist&lt;/code&gt;), &lt;code&gt;isrc&lt;/code&gt;, &lt;code&gt;mbid&lt;/code&gt;, &lt;code&gt;spotify_id&lt;/code&gt;, or &lt;code&gt;track_id&lt;/code&gt; (a catalog &lt;code&gt;itunes_track_id&lt;/code&gt;) — the same identifiers &lt;code&gt;/lookup&lt;/code&gt; accepts. No Spotify account, no audio file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.freqblog.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_your_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blinding Lights&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The Weeknd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Blinding Lights that prints a handful of tags across the categories — a measured &lt;code&gt;high-energy&lt;/code&gt; (1.0), &lt;code&gt;very-danceable&lt;/code&gt; and &lt;code&gt;acoustic&lt;/code&gt;; a derived mood of &lt;code&gt;tense&lt;/code&gt;; and a catalog genre of &lt;code&gt;synthwave&lt;/code&gt;. The response envelope is &lt;code&gt;{ track, count, tags:[…], disclaimer }&lt;/code&gt;: &lt;code&gt;track&lt;/code&gt; is the resolved stub (name, artist, ids), &lt;code&gt;count&lt;/code&gt; is the number of tags, and &lt;code&gt;disclaimer&lt;/code&gt; is a plain-English note about provenance you can surface in a tooltip.&lt;/p&gt;

&lt;h3&gt;
  
  
  The four confidence levels — what each one means
&lt;/h3&gt;

&lt;p&gt;Every tag's &lt;code&gt;confidence&lt;/code&gt; + &lt;code&gt;provenance&lt;/code&gt; tells you which bucket it came from. There are exactly four, and they're ordered most-to-least authoritative:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;confidence&lt;/th&gt;
&lt;th&gt;provenance&lt;/th&gt;
&lt;th&gt;what it is&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;measured&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;essentia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our own Essentia analysis of the recording — energy, danceability, valence, acousticness, instrumentalness — bucketed to a human label.&lt;/td&gt;
&lt;td&gt;the [0,1] score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;derived&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;valence+energy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single MIREX-style mood category &lt;em&gt;computed&lt;/em&gt; from the measured valence + energy (e.g. &lt;code&gt;tense&lt;/code&gt;, &lt;code&gt;calm&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;null&lt;/code&gt; (label only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model-estimated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acousticbrainz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AcousticBrainz mood SVM probabilities (happy / sad / aggressive / relaxed / party). Research-grade — treat as a hint.&lt;/td&gt;
&lt;td&gt;the raw probability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog-genre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;catalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The broad catalogue genre tag (iTunes / Last.fm chain). Coarse, not a fine taxonomy.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;null&lt;/code&gt; (label only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the rule for a consumer is simple and the API does the heavy lifting of being honest: trust the &lt;code&gt;measured&lt;/code&gt; tags as fact, treat &lt;code&gt;derived&lt;/code&gt; as a reasonable summary, and gate any product decision on a &lt;code&gt;model-estimated&lt;/code&gt; mood by its &lt;code&gt;value&lt;/code&gt; — it's only emitted when the model's probability clears 0.5, and the raw number is right there for you to threshold higher.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two things we deliberately do NOT serve — and why it makes the output better.&lt;/strong&gt; (1) AcousticBrainz also ships a &lt;em&gt;genre&lt;/em&gt; column, but it's roughly 80% mislabelled "electronic" — so we serve the broad &lt;em&gt;catalogue&lt;/em&gt; genre instead and never expose the AB genre column. (2) AB ships a single argmax mood &lt;em&gt;label&lt;/em&gt; ("this track is happy"); we don't serve that verdict either — we expose the per-axis SVM &lt;em&gt;probabilities&lt;/em&gt; with the raw numbers, so you can see it's &lt;code&gt;happy: 0.58 / relaxed: 0.55&lt;/code&gt; rather than a confident coin-flip dressed as a fact. Leaving bad data out is part of the honesty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tagging a whole library
&lt;/h2&gt;

&lt;p&gt;The endpoint is one track per call, so "tag my library" is a loop — resolve each (title, artist) once, keep the tags you care about, and you're done. A small, readable version that pulls just the high-confidence labels for a playlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;library&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blinding Lights&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The Weeknd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bohemian Rhapsody&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Queen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One More Time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Daft Punk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trusted_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Measured + derived tags only — the ones safe to file on.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;measured&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;derived&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalog-genre&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trusted_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;energy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;danceability&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mood=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mood&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, genre=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;genre&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole pattern: a function per track, a filter on &lt;code&gt;confidence&lt;/code&gt;, and a dict you write to your own store. If you want the model-estimated mood axes too, drop the filter and read &lt;code&gt;value&lt;/code&gt; — the data's there, you just choose how much to trust it. Each successful call is one quota unit (it's a &lt;code&gt;/lookup&lt;/code&gt; projection — same data, same cost), and you're only charged on a served &lt;code&gt;200&lt;/code&gt;; a miss (&lt;code&gt;404&lt;/code&gt;) or a request with no/invalid identifier (&lt;code&gt;422&lt;/code&gt;) costs nothing. The free tier's 1,000 requests/month is enough to tag a real playlist before you ever need a paid plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can and can't tag — the honest coverage
&lt;/h2&gt;

&lt;p&gt;This is the part most "tag any track" pitches get wrong, so here it is plainly. There are two coverage stories and they're different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The measured tags — the broad, reliable coverage.&lt;/strong&gt; Energy, danceability, valence, acousticness, instrumentalness and the derived mood come from our own Essentia analysis over the pre-analysed catalogue (hundreds of thousands of tracks), and any track not yet in it gets pulled in on demand the first time you look it up. &lt;strong&gt;You can tag these by name.&lt;/strong&gt; Lead with this — it's where &lt;code&gt;/tag&lt;/code&gt; is dependable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model-estimated mood axes — reachable when you supply the identifier.&lt;/strong&gt; The AcousticBrainz SVM probabilities sit over 7.5M+ recordings, but they're keyed by MusicBrainz ID. So you can reach that slice by passing an &lt;code&gt;mbid&lt;/code&gt; or an &lt;code&gt;isrc&lt;/code&gt; — &lt;em&gt;when you supply the identifier&lt;/em&gt; — not by track name alone. Only a small fraction of AB rows are resolvable by name today.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The one-line version:&lt;/strong&gt; tag any track by &lt;strong&gt;name&lt;/strong&gt; (the full analysed catalogue + on-demand backfill) for the measured features, or by &lt;strong&gt;MBID / ISRC&lt;/strong&gt; against 7.5M+ AcousticBrainz recordings &lt;em&gt;when you supply the identifier&lt;/em&gt; for the model-estimated mood. It is &lt;em&gt;not&lt;/em&gt; "7.5M tracks by name" — and we'd rather tell you that up front than have you discover it in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it compares to enterprise auto-tagging — honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No upload, self-serve.&lt;/strong&gt; You tag by name / ISRC / MBID against open-data analysis — there's no audio to upload, no seat to provision, and you can be tagging a playlist 60 seconds after you get a free key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence + provenance on every tag.&lt;/strong&gt; This is the headline difference. A closed tagger gives you a label; &lt;code&gt;/tag&lt;/code&gt; gives you a label &lt;em&gt;plus&lt;/em&gt; where it came from and how much to trust it — so you can file the measured tags and gate the model-estimated ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open data, not a black box.&lt;/strong&gt; The measured tags are our Essentia pipeline; the mood axes are AcousticBrainz; the genre is the broad catalogue tag. Nothing here is a proprietary score you can't reason about — and we leave the known-bad fields (the ~80%-"electronic" AB genre column, the argmax mood verdict) out on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage, not magic.&lt;/strong&gt; The measured tags need the track in the catalogue (or pulled in on demand); the model-estimated mood needs an MBID/ISRC. A track we can't resolve simply comes back without those tags rather than with a confident guess — better an honest gap than a wrong label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try FreqBlog — free tier, no card:&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;https://freqblog.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;Spotify Audio Features Is Dead. Here's What to Use Instead in 2026.&lt;/a&gt; — the full numeric feature set behind these tags&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-recommendations-replacement/" rel="noopener noreferrer"&gt;The Spotify /recommendations Replacement&lt;/a&gt; — from tags to nearest tracks and a derived artist graph&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-audio-features-migration-thresholds/" rel="noopener noreferrer"&gt;Migrating from Spotify Audio Features: a Field-by-Field Threshold Guide&lt;/a&gt; — how the bucket boundaries behind the tags are calibrated&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/acousticbrainz-alternative/" rel="noopener noreferrer"&gt;AcousticBrainz Alternative in 2026: The Honest Insider's Guide&lt;/a&gt; — where the model-estimated mood axes come from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/music-metadata-matching/" rel="noopener noreferrer"&gt;Why Music Metadata Matching Is Harder Than It Looks&lt;/a&gt; — what makes a track resolve or miss&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://freqblog.com/blog/auto-tag-music-library/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>api</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>Mixed In Key vs Rekordbox vs Serato: Why DJ Platforms Disagree on Key 60% of the Time</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:48:32 +0000</pubDate>
      <link>https://dev.to/birrings/mixed-in-key-vs-rekordbox-vs-serato-why-dj-platforms-disagree-on-key-60-of-the-time-22fj</link>
      <guid>https://dev.to/birrings/mixed-in-key-vs-rekordbox-vs-serato-why-dj-platforms-disagree-on-key-60-of-the-time-22fj</guid>
      <description>&lt;p&gt;Take a track. Run it through Mixed In Key, then Pioneer Rekordbox, then Serato. Compare the keys.&lt;/p&gt;

&lt;p&gt;If they all agree, congratulations — you've picked a track that's harmonically unambiguous. If you spent an afternoon picking 20 tracks and ran the experiment, you'd find that all three platforms agree on the key for only about &lt;strong&gt;39% of them&lt;/strong&gt;. Mixed In Key disagrees with Serato on 45%. Mixed In Key disagrees with Rekordbox on 38%.&lt;/p&gt;

&lt;p&gt;That isn't a typo. Three professional, paid DJ platforms produce three different answers for nearly two in three tracks. This article unpicks why — and what it means if you're building software that depends on key detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;The 39 / 45 / 38 percentages come from independent comparisons run by harmonic-mixing communities and reproduced multiple times since 2019. The methodology is straightforward: take a corpus of commercial music, run each platform's analyser on the same files, and tabulate exact-match agreement.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Agreement&lt;/th&gt;
&lt;th&gt;Disagreement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All three platforms agree&lt;/td&gt;
&lt;td&gt;~39%&lt;/td&gt;
&lt;td&gt;61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed In Key vs Serato&lt;/td&gt;
&lt;td&gt;~55%&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed In Key vs Rekordbox&lt;/td&gt;
&lt;td&gt;~62%&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Disagreement" here means the platforms returned different keys (e.g. &lt;code&gt;F# minor&lt;/code&gt; vs &lt;code&gt;A major&lt;/code&gt;) — not minor numerical differences. The disagreement is categorical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why three pro tools produce three different answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Different algorithms
&lt;/h3&gt;

&lt;p&gt;Each platform implements key detection differently. Mixed In Key uses a proprietary algorithm trained on commercial music, often with manual curation in the training set. Rekordbox uses Pioneer's in-house engine that prioritises speed (analysis runs in the player). Serato uses a third approach optimised for live performance.&lt;/p&gt;

&lt;p&gt;Most modern key detectors are &lt;strong&gt;chromagram-based&lt;/strong&gt;: split the audio into short frames, compute a 12-bin pitch-class histogram, then correlate against a reference template (Krumhansl-Schmuckler is the classic). The differences come from &lt;em&gt;how&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frame size and hop&lt;/strong&gt; — longer frames smooth out percussion, shorter frames pick up rapid modulations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harmonic peak detection vs. raw spectral energy&lt;/strong&gt; — harmonic peak detection is more accurate but slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-pass filtering before chromagram&lt;/strong&gt; — cuts cymbal hash, but can over-suppress melody.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference template choice&lt;/strong&gt; — Krumhansl, Temperley, Bellman, or a neural-network alternative each give different priors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modal handling&lt;/strong&gt; — relative major and minor share the same notes; algorithms decide differently between e.g. &lt;code&gt;A minor&lt;/code&gt; and &lt;code&gt;C major&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The relative-key flip
&lt;/h3&gt;

&lt;p&gt;The single biggest source of disagreement: relative major and minor have &lt;strong&gt;identical pitch content&lt;/strong&gt;. &lt;code&gt;A minor&lt;/code&gt; uses the same seven notes as &lt;code&gt;C major&lt;/code&gt;. Algorithms decide between them via priors that look at melody contour, downbeat emphasis, and chord voicing — signals that are easy to get wrong.&lt;/p&gt;

&lt;p&gt;On the Camelot wheel, this flip means the same track gets tagged 8A or 8B by different platforms. Either is musically defensible; only one matches the producer's intent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Practical effect:&lt;/strong&gt; when two platforms disagree on a track, ~70% of the time it's a relative-key flip (same Camelot number, different letter). The other 30% is genuine pitch-class disagreement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Different definitions of "in key"
&lt;/h3&gt;

&lt;p&gt;Many tracks change key. A pop song might verse in &lt;code&gt;A minor&lt;/code&gt; and chorus in &lt;code&gt;C major&lt;/code&gt;. Some platforms report the dominant key by duration; others report the chorus key; others bias toward the more "harmonically rich" section. Each is a defensible choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Tuning frequency drift
&lt;/h3&gt;

&lt;p&gt;Pre-1980 recordings, jazz, and a lot of indie / experimental music aren't tuned to A4 = 440 Hz. They might be at 432, 435, 442, or anywhere in between. A chromagram tuned to 440 will smear pitch-class energy across two adjacent bins for a track at 432 — and the algorithm picks whichever bin happens to win.&lt;/p&gt;

&lt;p&gt;This is the dirty secret of key detection: &lt;strong&gt;the further you get from "modern recording, tuned to 440, clear melody"&lt;/strong&gt;, the more the algorithms diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If you're a DJ
&lt;/h3&gt;

&lt;p&gt;Pick one tool and trust its analysis end-to-end. Mixing across platforms produces inconsistent results because of the disagreement above. The Camelot wheel was designed to be tolerant — adjacent moves (±1 number, same letter) work even when the underlying key tag is slightly wrong — so consistency matters more than which platform you chose.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you're building an app
&lt;/h3&gt;

&lt;p&gt;You need a single source of truth that's API-accessible. The DJ tools above don't expose their analysis as an HTTP endpoint, so you're either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building your own detector with Essentia / Librosa / Madmom (weeks of tuning, then you have your own opinion to defend)&lt;/li&gt;
&lt;li&gt;Paying Spotify (deprecated &lt;code&gt;audio_features&lt;/code&gt; as of November 2024) or AcousticBrainz (frozen July 2022)&lt;/li&gt;
&lt;li&gt;Using a managed API that handles the tuning, modal disambiguation and Camelot conversion for you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try FreqBlog — free tier, no card:&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;https://freqblog.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Confidence scores beat opinions
&lt;/h2&gt;

&lt;p&gt;Every detector should expose a confidence score alongside the key. Mixed In Key colour-codes its output (green = confident, red = best-guess). Rekordbox does too internally but doesn't expose it. Serato hides it from users entirely.&lt;/p&gt;

&lt;p&gt;This matters because for ~60% of tracks the algorithm &lt;strong&gt;knows&lt;/strong&gt; it's not certain. Surfacing that lets your application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat low-confidence detections as advisory rather than authoritative&lt;/li&gt;
&lt;li&gt;Fall back to user override or alternative algorithms&lt;/li&gt;
&lt;li&gt;Flag tracks that are likely to clash on a harmonic playlist regardless of platform agreement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our own API returns a &lt;code&gt;key_confidence&lt;/code&gt; field on every &lt;code&gt;/lookup&lt;/code&gt; response. Below 0.4 means the track is likely atonal, modal, or harmonically ambiguous — treat that as "play it solo, don't try to mix it".&lt;/p&gt;

&lt;h2&gt;
  
  
  Camelot is robust to all of this
&lt;/h2&gt;

&lt;p&gt;Here's the bright side. The Camelot wheel was specifically designed for the case where two algorithms disagree on the relative-key flip. &lt;code&gt;8A&lt;/code&gt; and &lt;code&gt;8B&lt;/code&gt; mix harmonically with each other — you can play the relative even when the algorithm got the modal wrong.&lt;/p&gt;

&lt;p&gt;If you build playlists using Camelot adjacency rules (same number, ±1 number, same-or-opposite letter), you get a tolerant matcher that works with any of the three platforms above. The key detection accuracy stops mattering as much when the downstream consumer is robust to off-by-one errors.&lt;/p&gt;

&lt;p&gt;The takeaway: stop chasing perfect key detection. Build robustness into your harmonic-mixing logic and accept that any single algorithm is a probability distribution, not an oracle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/camelot-wheel-developers-guide/" rel="noopener noreferrer"&gt;Camelot Wheel for Developers: Harmonic Mixing Without the Music Theory PhD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/half-time-double-time-bpm-detection/" rel="noopener noreferrer"&gt;Half-Time vs Double-Time BPM Detection: How We Fixed Spotify's Known Accuracy Gap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;FreqBlog Music API — the Spotify audio_features replacement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://freqblog.com/blog/mixed-in-key-vs-rekordbox-serato-key-detection/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>dj</category>
      <category>api</category>
    </item>
    <item>
      <title>Half-Time vs Double-Time BPM Detection: How We Fixed Spotify's Known Accuracy Gap</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:47:51 +0000</pubDate>
      <link>https://dev.to/birrings/half-time-vs-double-time-bpm-detection-how-we-fixed-spotifys-known-accuracy-gap-3fi6</link>
      <guid>https://dev.to/birrings/half-time-vs-double-time-bpm-detection-how-we-fixed-spotifys-known-accuracy-gap-3fi6</guid>
      <description>&lt;p&gt;Run &lt;em&gt;Blinding Lights&lt;/em&gt; by The Weeknd through Essentia's &lt;code&gt;RhythmExtractor2013&lt;/code&gt;. It returns 85.4 BPM. The track is famously 171 BPM — one of the most-played tracks of the last decade, peer-reviewed by literally every DJ in the world.&lt;/p&gt;

&lt;p&gt;This isn't a bug. It's a known property of beat-tracking algorithms: they pick the wrong tactus level for tracks where the perceived beat doesn't match the dominant onset rate. Spotify's deprecated &lt;code&gt;audio_features&lt;/code&gt; had the same problem — entire genres came back at half their actual tempo.&lt;/p&gt;

&lt;p&gt;This article unpicks why that happens and shows the 30-line heuristic we use to detect and correct it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "BPM" actually means
&lt;/h2&gt;

&lt;p&gt;Most people think BPM is a single number per track. It isn't. A piece of music has a &lt;strong&gt;metric hierarchy&lt;/strong&gt; — nested levels of regular pulse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tatum&lt;/strong&gt; — the smallest regular subdivision. For a typical 4/4 dance track, this is the 16th note.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tactus&lt;/strong&gt; — the level you tap your foot to. This is what humans usually call "the BPM."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt; — one bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hyper-measure&lt;/strong&gt; — phrase-level groupings (4-bar, 8-bar, 16-bar).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most tracks the tactus is unambiguous. &lt;em&gt;Bohemian Rhapsody&lt;/em&gt; is 72 BPM. &lt;em&gt;Levels&lt;/em&gt; by Avicii is 126 BPM. Easy.&lt;/p&gt;

&lt;p&gt;But for some tracks, especially modern pop with &lt;strong&gt;halftime drums&lt;/strong&gt; or trap-influenced production, the tactus level is genuinely ambiguous. &lt;em&gt;Blinding Lights&lt;/em&gt; has a synth-arp running at 16th notes (171 BPM × 4 = 684 BPM at the tatum), kicks every quarter note (171 BPM), but a snare on beats 2 and 4 only (85 BPM perceived as the snare-driven beat).&lt;/p&gt;

&lt;p&gt;Whether you call it 85 or 171 depends on where you'd start a metronome. Most listeners and DJs say 171. Beat trackers often say 85.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why algorithms get it wrong
&lt;/h2&gt;

&lt;p&gt;Beat trackers like Essentia &lt;code&gt;RhythmExtractor2013&lt;/code&gt;, &lt;code&gt;BeatTrackerDegara&lt;/code&gt;, and Librosa's &lt;code&gt;beat.beat_track&lt;/code&gt; all work the same way at a high level:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute an &lt;strong&gt;onset envelope&lt;/strong&gt; — signal where each peak corresponds to a percussive event (kick, snare, transient).&lt;/li&gt;
&lt;li&gt;Compute the &lt;strong&gt;tempogram&lt;/strong&gt; — autocorrelation of the onset envelope across a range of candidate periods.&lt;/li&gt;
&lt;li&gt;Pick the period with the highest score, weighted by a tempo prior (most algorithms prefer 90-180 BPM).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The failure mode for half-time tracks: the tempogram has a peak at both the perceived BPM (171) and at half (85.5). Both are mathematically valid — the snare on beats 2 and 4 forms a regular pulse at 85.5 BPM. The algorithm's tempo prior gently nudges toward higher BPMs (octave-aware priors typically peak at 120 BPM and fall off slowly), but for tracks where the kick-snare pattern strongly emphasises the half-time grid, the lower peak wins.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why pop especially?&lt;/strong&gt; Modern pop production deliberately crafts kicks and snares to feel "patient" against fast hi-hats. The snare landing on 2 and 4 (rather than every quarter note) creates a hypnotic 85-BPM feel even when the underlying tempo is 170. Producers do this on purpose. The algorithm just reports what it hears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Detecting the wrong level
&lt;/h2&gt;

&lt;p&gt;Once you know what's happening, detecting it is straightforward. A track that's "really" at 170 but reported as 85 has these tell-tale signatures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BPM in 70-95 range&lt;/strong&gt; (the suspect zone — where half-time pop typically lands)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High onset rate&lt;/strong&gt; (&amp;gt;3 events per second — lots of percussion happening between the slow snares)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High energy&lt;/strong&gt; (&amp;gt;0.4 RMS — not a slow ballad)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High danceability&lt;/strong&gt; (&amp;gt;0.55 — not a smooth jazz track)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all four are true, the track is almost certainly perceived at 2× the reported BPM. Conversely, a track reported at &amp;gt;170 BPM with &lt;em&gt;low&lt;/em&gt; onset rate (sparse percussion) is likely double-time'd — the algorithm picked up a hi-hat pattern when the tactus is the slower kick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The correction heuristic
&lt;/h2&gt;

&lt;p&gt;Here's a faithful simplification of the heuristic we ship (the production version uses curated fast/slow genre sets). Returns a corrected BPM alternative when warranted, &lt;code&gt;None&lt;/code&gt; otherwise. &lt;strong&gt;It never overwrites the raw BPM&lt;/strong&gt; — we expose the corrected value as a sibling field &lt;code&gt;bpm_alt&lt;/code&gt; so callers can choose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bpm_alt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;danceability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Half-time / double-time BPM correction. Conservative — false positives push
    customers off-grid, so we require multiple signals to agree before suggesting.
    onset_rate comes from AcousticBrainz and is null for most preview-analysed
    tracks, so genre carries the decision whenever onset_rate is missing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FAST_GENRES&lt;/span&gt;   &lt;span class="c1"&gt;# house, techno, drum &amp;amp; bass, synthwave, ...
&lt;/span&gt;    &lt;span class="n"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SLOW_GENRES&lt;/span&gt;   &lt;span class="c1"&gt;# hip-hop, r&amp;amp;b/soul, ballad, downtempo, ...
&lt;/span&gt;
    &lt;span class="c1"&gt;# Half-time → suggest 2x (a track that *feels* like twice the reported tempo)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mf"&gt;70.0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;95.0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt; \
           &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;energy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;danceability&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# onset_rate missing → lean on a fast-genre tag, else demand strong evidence
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;energy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;danceability&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;energy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;danceability&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Double-time → suggest 1/2x
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;165.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;onset_rate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tested on the live catalog:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Raw BPM&lt;/th&gt;
&lt;th&gt;bpm_alt&lt;/th&gt;
&lt;th&gt;Real BPM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blinding Lights / The Weeknd&lt;/td&gt;
&lt;td&gt;85.4&lt;/td&gt;
&lt;td&gt;170.8&lt;/td&gt;
&lt;td&gt;171&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Levitating / Dua Lipa&lt;/td&gt;
&lt;td&gt;102.8&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bohemian Rhapsody / Queen&lt;/td&gt;
&lt;td&gt;72.0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don't Stop the Music / Rihanna&lt;/td&gt;
&lt;td&gt;123.6&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The heuristic fires on Blinding Lights (correctly — it's tagged synthwave, a fast genre) and stays silent on the other three (correctly). The conservative thresholds — genre, onset rate, energy and danceability all have to agree — mean it rarely false-positives on tracks at 80-95 BPM that genuinely belong there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just multiply by 2 for everything in 70-95?
&lt;/h2&gt;

&lt;p&gt;You'd break every R&amp;amp;B ballad, every downtempo track, and every hip-hop song that's actually at 90 BPM. Real 80-95 BPM music exists in volume. The whole point of the heuristic is to &lt;strong&gt;distinguish&lt;/strong&gt; tracks that &lt;em&gt;feel&lt;/em&gt; like 170 from tracks that &lt;em&gt;are&lt;/em&gt; 85. The signal is the rest of the audio descriptors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real 85-BPM track has lower onset rate (sparse hi-hats, less internal pulse)&lt;/li&gt;
&lt;li&gt;A real 85-BPM track has lower danceability (because beat strength is lower at the perceived tactus)&lt;/li&gt;
&lt;li&gt;A 170-perceived-as-85 track will pin all four signals high&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production deployment notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't overwrite cached results
&lt;/h3&gt;

&lt;p&gt;If your existing API stores BPM in a database, applying correction in-place is a breaking change — customers' existing playlists, mixes, and analyses depend on the previous value. Always expose the corrected BPM as a &lt;strong&gt;sibling field&lt;/strong&gt; (&lt;code&gt;bpm_alt&lt;/code&gt;, &lt;code&gt;bpm_corrected&lt;/code&gt;, &lt;code&gt;bpm_perceived&lt;/code&gt;) and let callers opt in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make it derivable, not stored
&lt;/h3&gt;

&lt;p&gt;The correction only depends on existing fields: bpm, onset_rate, energy, danceability and genre. Compute it at response build time, not at ingest. That way you can tune the heuristic without re-analysing your catalog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surface confidence
&lt;/h3&gt;

&lt;p&gt;If you have a beat-detection confidence score (Essentia returns one), use it as a fifth gate. A low-confidence detection is more likely to be an octave error and benefits from the alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about machine-learned alternatives?
&lt;/h2&gt;

&lt;p&gt;Several recent papers train neural networks to predict the tactus level directly — treating tactus selection as a classification problem rather than a peak-picking problem. &lt;a href="https://github.com/CPJKU/madmom" rel="noopener noreferrer"&gt;Madmom's &lt;code&gt;DBNDownBeatTracker&lt;/code&gt;&lt;/a&gt; does something similar by jointly tracking the beat and the downbeat with a Bayesian network.&lt;/p&gt;

&lt;p&gt;These work well but they're slower, larger (60+ MB models), and require more inference compute. For a high-throughput API the heuristic above gets you 90% of the gain at &lt;em&gt;literally zero&lt;/em&gt; extra cost — it's just four float comparisons on existing fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try FreqBlog — free tier, no card:&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;https://freqblog.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/mixed-in-key-vs-rekordbox-serato-key-detection/" rel="noopener noreferrer"&gt;Why DJ Platforms Disagree on Key 60% of the Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/camelot-wheel-developers-guide/" rel="noopener noreferrer"&gt;Camelot Wheel for Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;FreqBlog Music API documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://freqblog.com/blog/half-time-double-time-bpm-detection/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>api</category>
      <category>python</category>
      <category>audio</category>
    </item>
    <item>
      <title>Camelot Wheel for Developers: Harmonic Mixing Without the Music Theory PhD</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:47:10 +0000</pubDate>
      <link>https://dev.to/birrings/camelot-wheel-for-developers-harmonic-mixing-without-the-music-theory-phd-21i8</link>
      <guid>https://dev.to/birrings/camelot-wheel-for-developers-harmonic-mixing-without-the-music-theory-phd-21i8</guid>
      <description>&lt;p&gt;Here's the bargain music theory hands developers: take the circle of fifths — a 700-year-old chord-relationship diagram — rotate it slightly, replace the keys with numbers, and you get a tool that lets a 16-year-old DJ build a harmonic set without knowing what "fifth" means.&lt;/p&gt;

&lt;p&gt;That tool is the Camelot wheel. It's the foundation Mixed In Key, Rekordbox, Serato, and Engine DJ all build on. And implementing the rules in your own software takes about 50 lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the wheel does
&lt;/h2&gt;

&lt;p&gt;Every musical key has a relationship to every other key. Some pairs sound great together (relative major/minor). Some clash horribly (a minor third apart). Music theory expresses these relationships in terms like "subdominant" and "tritone substitution" — useful if you're writing a fugue, useless if you're trying to mix two house tracks at 2am.&lt;/p&gt;

&lt;p&gt;The Camelot wheel collapses all this into a clock face: &lt;strong&gt;12 numbered positions, each with an A (minor) and B (major) variant&lt;/strong&gt;. Two tracks mix harmonically if they share a position, are adjacent on the wheel, or are on the same number with different letters.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Camelot&lt;/th&gt;
&lt;th&gt;Key name&lt;/th&gt;
&lt;th&gt;Camelot&lt;/th&gt;
&lt;th&gt;Key name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1A&lt;/td&gt;
&lt;td&gt;A♭ minor&lt;/td&gt;
&lt;td&gt;1B&lt;/td&gt;
&lt;td&gt;B major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2A&lt;/td&gt;
&lt;td&gt;E♭ minor&lt;/td&gt;
&lt;td&gt;2B&lt;/td&gt;
&lt;td&gt;F# major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3A&lt;/td&gt;
&lt;td&gt;B♭ minor&lt;/td&gt;
&lt;td&gt;3B&lt;/td&gt;
&lt;td&gt;D♭ major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4A&lt;/td&gt;
&lt;td&gt;F minor&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;td&gt;A♭ major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5A&lt;/td&gt;
&lt;td&gt;C minor&lt;/td&gt;
&lt;td&gt;5B&lt;/td&gt;
&lt;td&gt;E♭ major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6A&lt;/td&gt;
&lt;td&gt;G minor&lt;/td&gt;
&lt;td&gt;6B&lt;/td&gt;
&lt;td&gt;B♭ major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7A&lt;/td&gt;
&lt;td&gt;D minor&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;F major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8A&lt;/td&gt;
&lt;td&gt;A minor&lt;/td&gt;
&lt;td&gt;8B&lt;/td&gt;
&lt;td&gt;C major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9A&lt;/td&gt;
&lt;td&gt;E minor&lt;/td&gt;
&lt;td&gt;9B&lt;/td&gt;
&lt;td&gt;G major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10A&lt;/td&gt;
&lt;td&gt;B minor&lt;/td&gt;
&lt;td&gt;10B&lt;/td&gt;
&lt;td&gt;D major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11A&lt;/td&gt;
&lt;td&gt;F# minor&lt;/td&gt;
&lt;td&gt;11B&lt;/td&gt;
&lt;td&gt;A major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12A&lt;/td&gt;
&lt;td&gt;C# minor&lt;/td&gt;
&lt;td&gt;12B&lt;/td&gt;
&lt;td&gt;E major&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The four rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rule 1: Same key (perfect mix)
&lt;/h3&gt;

&lt;p&gt;Track at &lt;code&gt;8A&lt;/code&gt; mixes with another track at &lt;code&gt;8A&lt;/code&gt;. Boring but harmonically perfect. Useful for back-to-back tracks where the mix focuses on rhythm rather than chord progression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: Relative key (mood swap)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;8A&lt;/code&gt; mixes with &lt;code&gt;8B&lt;/code&gt;. Same number, opposite letter. Same notes, different mood — a minor track and its relative major share all seven scale degrees, so they're harmonically identical from a pitch-class perspective. Mixing 8A → 8B is the classic "go from melancholic to hopeful" move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Adjacent (energy lift / drop)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;8A&lt;/code&gt; mixes with &lt;code&gt;7A&lt;/code&gt; or &lt;code&gt;9A&lt;/code&gt;. Same letter, ±1 number. The fifth-relationship gives you a smooth modulation that feels like the energy is climbing or relaxing without ever clashing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wraparound matters&lt;/strong&gt;: the wheel is a circle. &lt;code&gt;12A → 1A&lt;/code&gt; is one step, not eleven. &lt;code&gt;1A → 12A&lt;/code&gt; likewise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4 (advanced): Energy boost / drop
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;8A → 3A&lt;/code&gt; is a +7 jump — a bigger key change that landing-DJs use to lift energy mid-set. &lt;code&gt;8A → 1A&lt;/code&gt; is -7. These aren't strictly harmonic but every commercial DJ tool exposes them as "energy boost / drop" because they're a staple of festival mixing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 50-line implementation
&lt;/h2&gt;

&lt;p&gt;Here's a complete Python implementation. Drop it into your project, no dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;camelot_compatible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;camelot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return [(camelot, relation), ...] for every key that mixes with `camelot`.
    `extended=True` adds the +7/-7 energy-boost variants.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(1[0-2]|[1-9])([AB])&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camelot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid Camelot value: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;camelot&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;other_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;other_letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adjacent_up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adjacent_down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;energy_boost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;energy_drop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;


&lt;span class="c1"&gt;# Example
&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;camelot_compatible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8B&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;relative&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adjacent_up&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adjacent_down&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;camelot_compatible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extended&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12B&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;relative&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adjacent_up&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adjacent_down&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;energy_boost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;energy_drop&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather skip writing this yourself, our API exposes the same logic as &lt;code&gt;GET /key/&amp;lt;camelot&amp;gt;/compatible?extended=true&lt;/code&gt; — &lt;a href="https://freqblog.com/#docs" rel="noopener noreferrer"&gt;no auth, no quota&lt;/a&gt;, pure-logic helper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distance: how do you score a transition?
&lt;/h2&gt;

&lt;p&gt;Once you have compatibility, you need a &lt;strong&gt;distance metric&lt;/strong&gt; for ranking transitions. Here's the rubric most DJ tools converge on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;0 hops&lt;/strong&gt; — same Camelot exactly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 hop&lt;/strong&gt; — relative (same number, opposite letter), or adjacent (±1, same letter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2-3 hops&lt;/strong&gt; — "stretchy" but workable; many crowd-friendly mixes live here&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4+ hops&lt;/strong&gt; — clash territory, only attempt with a long mix or filter
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;camelot_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Distance in &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wheel hops&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; between two Camelot values.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
    &lt;span class="n"&gt;ma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(1[0-2]|[1-9])([AB])&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;mb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(1[0-2]|[1-9])([AB])&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ma&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;    &lt;span class="c1"&gt;# treat as far
&lt;/span&gt;    &lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;ma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;nd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;bn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;bn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;# circular distance
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                                 &lt;span class="c1"&gt;# relative
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nd&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nd&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                                &lt;span class="c1"&gt;# both differ
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; the wraparound. &lt;code&gt;1A&lt;/code&gt; and &lt;code&gt;12A&lt;/code&gt; are &lt;em&gt;one&lt;/em&gt; hop apart, not eleven. The &lt;code&gt;min(diff, 12 - diff)&lt;/code&gt; trick handles it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Building a harmonic playlist
&lt;/h2&gt;

&lt;p&gt;Now you have compatibility and distance. The simplest playlist algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a seed track&lt;/li&gt;
&lt;li&gt;For each subsequent slot: filter the candidate pool to tracks within hop-distance ≤ K of the previous track's key&lt;/li&gt;
&lt;li&gt;Among those, pick by a secondary criterion (BPM continuity, similarity, popularity)&lt;/li&gt;
&lt;li&gt;Mark used, repeat
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;harmonic_walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Track&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="n"&gt;max_hops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Track&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;playlist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;current_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seed_key&lt;/span&gt;
    &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playlist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;nexts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;
             &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;
             &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;camelot_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;camelot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_hops&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;camelot_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;camelot&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;popularity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;nexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;pick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nexts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;playlist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;current_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;camelot&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;playlist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also exposed as a single API call — &lt;a href="https://freqblog.com/#dj" rel="noopener noreferrer"&gt;&lt;code&gt;GET /radio?seed_track_id=...&amp;amp;n=20&amp;amp;max_key_distance=2&lt;/code&gt;&lt;/a&gt; — if you'd rather not run the candidate pool yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  BPM continuity matters too
&lt;/h2&gt;

&lt;p&gt;Harmonic compatibility on its own isn't enough. Two tracks in &lt;code&gt;8A&lt;/code&gt; at 95 BPM and 175 BPM can't be mixed together without a cue-point trick or a halftime drop. A real harmonic-mixing matcher needs &lt;strong&gt;both&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Camelot distance ≤ K (harmonic constraint)&lt;/li&gt;
&lt;li&gt;BPM difference ≤ ±N BPM (rhythmic constraint)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For house and techno, ±5 BPM is forgiving (you can pitch ±3% on a player). For trance and DnB, you can be looser (±10 BPM). For hip-hop and R&amp;amp;B you usually want exact match because the swing pattern doesn't sound right pitched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Key vs Camelot
&lt;/h2&gt;

&lt;p&gt;Open Key is an alternative notation that some platforms (Mixed In Key, Serato) support alongside Camelot. The mapping is straightforward:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Camelot&lt;/th&gt;
&lt;th&gt;Open Key&lt;/th&gt;
&lt;th&gt;Camelot&lt;/th&gt;
&lt;th&gt;Open Key&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8A&lt;/td&gt;
&lt;td&gt;1m&lt;/td&gt;
&lt;td&gt;8B&lt;/td&gt;
&lt;td&gt;1d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9A&lt;/td&gt;
&lt;td&gt;2m&lt;/td&gt;
&lt;td&gt;9B&lt;/td&gt;
&lt;td&gt;2d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10A&lt;/td&gt;
&lt;td&gt;3m&lt;/td&gt;
&lt;td&gt;10B&lt;/td&gt;
&lt;td&gt;3d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;m&lt;/code&gt; = minor, &lt;code&gt;d&lt;/code&gt; = major (dominant). Same circle, just rotated by 7 positions and re-labeled. The compatibility rules are identical — if your code uses Camelot, support both notations as input via a small lookup table and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atonal / modal tracks&lt;/strong&gt; — ambient, drone, and a lot of jazz don't have a clear key centre. Detection algorithms return low confidence; treat as "skip from harmonic constraints" in your matcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key changes mid-track&lt;/strong&gt; — some tracks modulate. Most detectors return the dominant key by duration; if your input source supports it, prefer the chorus/drop key for a DJ mix because that's what people will be hearing during a transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Half-time / double-time confusion&lt;/strong&gt; — not a key issue but related: a track perceived at 170 BPM might be detected at 85. &lt;a href="https://freqblog.com/blog/half-time-double-time-bpm-detection/" rel="noopener noreferrer"&gt;More on that here.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try FreqBlog — free tier, no card:&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;https://freqblog.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/mixed-in-key-vs-rekordbox-serato-key-detection/" rel="noopener noreferrer"&gt;Mixed In Key vs Rekordbox vs Serato: Why DJ Platforms Disagree on Key 60% of the Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/half-time-double-time-bpm-detection/" rel="noopener noreferrer"&gt;Half-Time vs Double-Time BPM Detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/#dj" rel="noopener noreferrer"&gt;DJ tooling on the FreqBlog API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://freqblog.com/blog/camelot-wheel-developers-guide/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>dj</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build a Harmonic DJ Set Planner in 50 Lines</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:46:29 +0000</pubDate>
      <link>https://dev.to/birrings/build-a-harmonic-dj-set-planner-in-50-lines-4oj5</link>
      <guid>https://dev.to/birrings/build-a-harmonic-dj-set-planner-in-50-lines-4oj5</guid>
      <description>&lt;p&gt;When Spotify deprecated &lt;code&gt;audio_features&lt;/code&gt; and &lt;code&gt;audio_analysis&lt;/code&gt;, the obvious loss was the per-track numbers — tempo, key, energy. The quieter loss was everything you used to &lt;em&gt;build&lt;/em&gt; on top of them: the "what should I play next" logic, the harmonic-compatibility checks, the auto-ordered playlist. Plenty of people have written a host-swap guide to get the raw fields back. This post is about the layer above that — turning a crate of track names into an actual, beat-matched, key-compatible set.&lt;/p&gt;

&lt;p&gt;We'll do it in about 50 lines of Python against the FreqBlog Music API, using four endpoints: &lt;code&gt;/next-track&lt;/code&gt; (what mixes well after this?), &lt;code&gt;/transition&lt;/code&gt; (how well do these two mix, exactly?), &lt;code&gt;/setlist&lt;/code&gt; (order this whole crate into an energy arc), and &lt;code&gt;/export/rekordbox&lt;/code&gt; (drop the result straight into Rekordbox or Serato). No audio files, no Spotify ID — you pass track names or catalog IDs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New here?&lt;/strong&gt; If you haven't migrated off Spotify's removed endpoints yet, start with &lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;Spotify Audio Features Is Dead. Here's What to Use Instead in 2026&lt;/a&gt; for the landscape and the host swap, then &lt;a href="https://freqblog.com/blog/spotify-audio-features-migration-thresholds/" rel="noopener noreferrer"&gt;the field-by-field threshold guide&lt;/a&gt; to re-tune your numbers. This post assumes you can already get a track's key and BPM and want to build set logic on top.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The idea: score the transition, not just the track
&lt;/h2&gt;

&lt;p&gt;Every competing API that survived the Spotify shutdown gives you raw key and BPM and stops there. The interesting part of DJing isn't the value of one track — it's the relationship between two adjacent tracks: are the keys harmonically compatible on the &lt;a href="https://freqblog.com/blog/camelot-wheel-developers-guide/" rel="noopener noreferrer"&gt;Camelot wheel&lt;/a&gt;, are the tempos close (or a clean half/double-time match), and is the energy moving the way you want the room to move?&lt;/p&gt;

&lt;p&gt;FreqBlog exposes that relationship directly. &lt;code&gt;GET /transition&lt;/code&gt; takes two catalog tracks and returns a 0–100 score with the breakdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /transition?from_track_id=apple_ad1829eeccb70f9a&amp;amp;to_track_id=1443810719
X-Api-Key: sk_live_...

{
  "from_track": { "track_name": "Can't Stop Lovin' You",    "camelot": "11B", "bpm": 117.8, ... },
  "to_track":   { "track_name": "You Beat Me to the Punch", "camelot": "11B", "bpm": 117.5, ... },
  "score": 99,
  "components": { "harmonic": 100, "tempo": 98, "energy": 100 },
  "detail": {
    "key_relation": "same",
    "from_camelot": "11B", "to_camelot": "11B",
    "from_bpm": 117.77, "to_bpm": 117.48,
    "bpm_delta": -0.29, "bpm_octave_matched": false,
    "from_energy": 0.69, "to_energy": 0.81, "energy_delta": 0.12
  },
  "reason": "11B-&amp;gt;11B same key, 118-&amp;gt;117 BPM (-0.29), energy +0.12"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;score&lt;/code&gt; blends three components: harmonic compatibility (Camelot-wheel relationship — same key, relative major/minor, adjacent ±1, energy boost/drop), tempo proximity (octave-aware, so a 70→140 BPM half-time blend counts as a match), and energy smoothness. The &lt;code&gt;reason&lt;/code&gt; string is human copy you can drop straight into a UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — resolve your crate to catalog IDs
&lt;/h2&gt;

&lt;p&gt;The set-builder endpoints work on catalog &lt;code&gt;itunes_track_id&lt;/code&gt; values (e.g. &lt;code&gt;apple_ad1829eeccb70f9a&lt;/code&gt;). You get those from any &lt;code&gt;/lookup&lt;/code&gt; or &lt;code&gt;/search&lt;/code&gt; response — so a crate of plain track names becomes a list of IDs with one lookup each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.freqblog.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_your_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Track name + artist -&amp;gt; catalog itunes_track_id (or None on a miss).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                      &lt;span class="c1"&gt;# 202 = queued for ingest, try again shortly
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itunes_track_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;crate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t Stop Lovin&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tom Browne&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Got To Be Real&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cheryl Lynn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t Nobody&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chaka Khan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Square Biz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Teena Marie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forget Me Nots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Patrice Rushen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;crate&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A &lt;code&gt;/lookup&lt;/code&gt; on a track that isn't analysed yet returns &lt;code&gt;202&lt;/code&gt; and queues an on-demand ingest — it'll be ready in 30 s–2 min and resolve on a retry. For a planner, resolve your crate once and cache the IDs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2 — "what plays next?" with /next-track
&lt;/h2&gt;

&lt;p&gt;Given any seed track, &lt;code&gt;GET /next-track&lt;/code&gt; returns the seed's closest sonic neighbours re-ranked by transition score — each with the same &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;components&lt;/code&gt;, and &lt;code&gt;reason&lt;/code&gt; as &lt;code&gt;/transition&lt;/code&gt;. This is your live "suggest the next tune" box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;suggest_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/next-track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed_track_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;seed_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exclude_same_artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suggestions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;suggest_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;track_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;artist_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our seed — "Can't Stop Lovin'" at 11B / 118 BPM — the top picks come back same-key (11B) and within a beat or two of 118 BPM, scoring 98–99 with reasons like &lt;code&gt;"11B-&amp;gt;11B same key, 118-&amp;gt;117 BPM (-0.29), energy +0.12"&lt;/code&gt;. The &lt;code&gt;bpm_drift&lt;/code&gt; and &lt;code&gt;max_key_distance&lt;/code&gt; query params let you widen or tighten the net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — order the whole crate with /setlist
&lt;/h2&gt;

&lt;p&gt;The payoff endpoint. &lt;code&gt;POST /setlist&lt;/code&gt; takes your list of IDs and returns them &lt;em&gt;ordered&lt;/em&gt; into an energy arc, keeping every consecutive transition harmonically and tempo-smooth. Pick the arc: &lt;code&gt;peak_time&lt;/code&gt; (build to a peak, then ease), &lt;code&gt;warmup&lt;/code&gt;, &lt;code&gt;cooldown&lt;/code&gt;, or &lt;code&gt;flat&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_track_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start_id&lt;/span&gt;          &lt;span class="c1"&gt;# optional fixed opener
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/setlist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Flow score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flow_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100   (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tracks, arc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;track_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;artist_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transitions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;     &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;from_index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;to_index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ordered_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itunes_track_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back the tracks in play order, a per-step &lt;code&gt;transitions&lt;/code&gt; list (each with its own score and reason), an overall &lt;code&gt;flow_score&lt;/code&gt; (the mean of the step scores — how smooth the whole set is), and an &lt;code&gt;omitted&lt;/code&gt; list of any IDs not in the catalog. That's a complete, defensible running order — not a shuffle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — export to Rekordbox in one call
&lt;/h2&gt;

&lt;p&gt;Take the ordered IDs from &lt;code&gt;/setlist&lt;/code&gt; and hand them to &lt;code&gt;GET /export/rekordbox&lt;/code&gt; to get a Pioneer-format XML you can import into Rekordbox or Serato:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_rekordbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_set.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/export/rekordbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered_ids&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - import via Rekordbox &amp;gt; File &amp;gt; Import Collection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;export_rekordbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole planner: resolve → suggest → order → export. Counting the import, the helpers, and the crate, it lands right around 50 lines — and the part that used to be impossible without Spotify's data (or a pile of your own DSP) is now four HTTP calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each call costs
&lt;/h2&gt;

&lt;p&gt;The set-builder endpoints run through the same monthly-quota model as &lt;code&gt;/lookup&lt;/code&gt;, and you're only charged on a served &lt;code&gt;200&lt;/code&gt; — a &lt;code&gt;404&lt;/code&gt; (track not in catalog) or &lt;code&gt;422&lt;/code&gt; (bad/missing params) costs nothing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Quota&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /transition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 request&lt;/td&gt;
&lt;td&gt;A single A→B comparison, same as a &lt;code&gt;/lookup&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /next-track&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 requests&lt;/td&gt;
&lt;td&gt;Scores up to ~1,000 candidate neighbours to rank the top N.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /setlist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5 requests&lt;/td&gt;
&lt;td&gt;An N² scoring problem — the premium "order my whole crate" call.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a typical session — resolve a 12-track crate (12), order it once (&lt;code&gt;/setlist&lt;/code&gt;, 5), then export (free) — is well under 20 quota requests. The free tier's 1,000/month covers a lot of set-building before you need a paid plan.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One honest note on scope.&lt;/strong&gt; &lt;code&gt;/setlist&lt;/code&gt; uses a greedy ordering with an energy-arc objective — it's built for clean, playable running orders, not a globally-optimal tour through your crate. For long sets, run it per-section (warmup / peak / cooldown crates) and stitch, or use &lt;code&gt;/next-track&lt;/code&gt; interactively while you play. The &lt;code&gt;flow_score&lt;/code&gt; tells you how smooth the result is so you can spot a weak link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why this works without Spotify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No audio upload.&lt;/strong&gt; Everything is keyed by track name, ISRC, or catalog ID against a pre-analysed catalog — you never ship a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Spotify ID.&lt;/strong&gt; The crate is plain names; &lt;code&gt;/lookup&lt;/code&gt; resolves them and queues an on-demand analysis for anything not yet in the catalog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-class harmonic data.&lt;/strong&gt; Camelot and Open Key notation are returned directly, so the transition scoring has real harmonic relationships to work with — not just two raw key integers you have to compare yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The set logic is the product.&lt;/strong&gt; Raw key/BPM is table stakes; the differentiator is scoring the &lt;em&gt;transition&lt;/em&gt; and ordering the &lt;em&gt;set&lt;/em&gt;. That's what these three endpoints sell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try FreqBlog — free tier, no card:&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;https://freqblog.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;Spotify Audio Features Is Dead. Here's What to Use Instead in 2026.&lt;/a&gt; — the landscape and the host-swap walkthrough&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-audio-features-migration-thresholds/" rel="noopener noreferrer"&gt;Migrating from Spotify Audio Features: a Field-by-Field Threshold Guide&lt;/a&gt; — re-tune your numbers once the calls flow&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/camelot-wheel-developers-guide/" rel="noopener noreferrer"&gt;Camelot Wheel for Developers: Harmonic Mixing Without the Music Theory PhD&lt;/a&gt; — the harmonic rules behind the transition score&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/half-time-double-time-bpm-detection/" rel="noopener noreferrer"&gt;Half-Time vs Double-Time BPM Detection&lt;/a&gt; — why the tempo score is octave-aware&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freqblog.com/blog/mixed-in-key-vs-rekordbox-serato-key-detection/" rel="noopener noreferrer"&gt;Why DJ Platforms Disagree on Key 60% of the Time&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://freqblog.com/blog/build-harmonic-dj-set-planner/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>api</category>
      <category>python</category>
      <category>dj</category>
    </item>
    <item>
      <title>The Spotify /recommendations Replacement</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:45:48 +0000</pubDate>
      <link>https://dev.to/birrings/the-spotify-recommendations-replacement-30cn</link>
      <guid>https://dev.to/birrings/the-spotify-recommendations-replacement-30cn</guid>
      <description>&lt;p&gt;When people talk about the Spotify Web API deprecations, they usually mean &lt;code&gt;audio_features&lt;/code&gt; and &lt;code&gt;audio_analysis&lt;/code&gt; — the per-track numbers. But the same November 2024 cull also took &lt;code&gt;GET /v1/recommendations&lt;/code&gt; and &lt;code&gt;GET /v1/artists/{id}/related-artists&lt;/code&gt;, and those two have arguably left the bigger hole. Eighteen months on, "Spotify recommendations endpoint deprecated" is still one of the most-asked questions on Stack Overflow and the developer forum, with no official replacement — for the seed-based recommender that powered a huge number of "discover" features and "fans also like" pages.&lt;/p&gt;

&lt;p&gt;This post is a practical walkthrough of building both back, in about 40 lines of Python, against the FreqBlog Music API: &lt;code&gt;GET /recommendations&lt;/code&gt; (seed tracks → similar tracks) and &lt;code&gt;GET /related-artists&lt;/code&gt; (an artist → nearby artists). No Spotify account, no deprecated endpoint, no audio files — you pass catalog track ids and artist names.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What was removed, exactly.&lt;/strong&gt; Spotify deprecated &lt;code&gt;/v1/recommendations&lt;/code&gt; and &lt;code&gt;/v1/artists/{id}/related-artists&lt;/code&gt; for apps created after 27 November 2024, and the broader audio-intelligence set (&lt;code&gt;audio_features&lt;/code&gt;, &lt;code&gt;audio_analysis&lt;/code&gt;, audio previews) with them. If you also need the raw per-track numbers, start with &lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;Spotify Audio Features Is Dead. Here's What to Use Instead in 2026&lt;/a&gt; for the host swap, then come back here for the recommender layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why there's no drop-in clone — and what to build instead
&lt;/h2&gt;

&lt;p&gt;Spotify's recommender was a black box backed by its full collaborative-filtering graph — billions of listening sessions. Nobody outside Spotify has that signal, so any honest replacement is built differently. The approach that actually ships is &lt;strong&gt;content-based similarity&lt;/strong&gt;: describe each track by its audio features (tempo, key, energy, danceability, valence, acousticness…), and find neighbours in that feature space. It doesn't know that two tracks share an audience; it knows they &lt;em&gt;sound&lt;/em&gt; alike. For "here's a track, give me more like it" that turns out to be most of what you want — and it has a property the collaborative graph never did: it works on brand-new and long-tail tracks with zero listening history.&lt;/p&gt;

&lt;p&gt;FreqBlog runs that similarity index over its own pre-analysed catalogue and exposes it as two Spotify-shaped endpoints. The one twist worth knowing up front: pure audio-feature similarity is &lt;strong&gt;genre-blind&lt;/strong&gt; — an EDM track and a hard-rock track can sit close in feature space if their tempo and energy line up. So the ranking is &lt;strong&gt;genre-aware&lt;/strong&gt;: it uses FreqBlog's own catalogue genre data (around 94% coverage) to lift same-genre matches and push down a coincidentally-close cross-genre track. That's the bit that makes the results feel right rather than merely numerically near.&lt;/p&gt;

&lt;h2&gt;
  
  
  /recommendations — seed tracks in, similar tracks out
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET /recommendations&lt;/code&gt; is the direct stand-in for &lt;code&gt;/v1/recommendations&lt;/code&gt;. Give it 1–5 catalog seed tracks; it blends them into a single point in audio-feature space (a centroid) and returns the nearest catalogue tracks, re-ranked by genre affinity. Like Spotify's endpoint, you pass &lt;strong&gt;seed tracks&lt;/strong&gt; — the difference is the id format: FreqBlog &lt;code&gt;itunes_track_id&lt;/code&gt; values (e.g. &lt;code&gt;apple_ad1829eeccb70f9a&lt;/code&gt;), which you get from &lt;code&gt;/search&lt;/code&gt; or any &lt;code&gt;/lookup&lt;/code&gt; response, not Spotify URIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.freqblog.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_your_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recommend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exclude_seed_artists&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/recommendations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed_tracks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_ids&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exclude_seed_artists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude_seed_artists&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Seed: "Jump" by Van Halen (hard rock). Catalog id from /search or /lookup.
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recommend&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple_ad1829eeccb70f9a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seeds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  found=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;found&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;track_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;artist_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;genre&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response echoes your seeds with a &lt;code&gt;found&lt;/code&gt; flag (so you can tell which ids resolved), a &lt;code&gt;count&lt;/code&gt;, and a &lt;code&gt;tracks&lt;/code&gt; list of &lt;code&gt;{track, score}&lt;/code&gt; objects. For the Van Halen seed, the picks come back all hard rock — Alice Cooper, Guns N' Roses, Aerosmith, David Lee Roth — scoring around 0.93–0.95. Each &lt;code&gt;track&lt;/code&gt; is a full stub: &lt;code&gt;itunes_track_id&lt;/code&gt;, &lt;code&gt;track_name&lt;/code&gt;, &lt;code&gt;artist_name&lt;/code&gt;, &lt;code&gt;album_name&lt;/code&gt;, &lt;code&gt;genre&lt;/code&gt;, &lt;code&gt;release_date&lt;/code&gt;, &lt;code&gt;isrc&lt;/code&gt;/&lt;code&gt;mbid&lt;/code&gt; when known. Feed any of those ids straight into &lt;code&gt;/lookup&lt;/code&gt; for the full audio features.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One thing that trips people up: the list is NOT strictly score-descending.&lt;/strong&gt; &lt;code&gt;score&lt;/code&gt; is the raw audio-feature cosine similarity (0–1); the genre lift affects &lt;em&gt;ordering&lt;/em&gt;, not the printed number. So a same-genre track with a slightly lower cosine can — correctly — rank above a cross-genre track with a higher cosine. If you re-sort the list purely by &lt;code&gt;score&lt;/code&gt; on the client you'll throw away the genre-aware ranking. Trust the order it comes back in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Blending multiple seeds
&lt;/h3&gt;

&lt;p&gt;Multiple seeds work exactly like Spotify's: pass up to five and you get recommendations for the &lt;em&gt;blend&lt;/em&gt;, not for each one in turn. This is how you build a "based on your recent likes" row — take the last few tracks a user engaged with, blend them, and the centroid lands in the middle of their taste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;recent_likes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple_ad1829eeccb70f9a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# hard rock
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dz_92720102&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;# arena rock (AC/DC)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1443810719&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;# classic rock
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recommend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recent_likes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exclude_seed_artists&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; recommendations for the blend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exclude_seed_artists=true&lt;/code&gt; drops anything by the seed artists themselves — the difference between "more of the same artist" and genuine discovery. Leave it off for a "more like this" box; turn it on for a "you might also like" row.&lt;/p&gt;

&lt;h2&gt;
  
  
  /related-artists — deriving an artist graph without one
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET /related-artists&lt;/code&gt; stands in for &lt;code&gt;/v1/artists/{id}/related-artists&lt;/code&gt; — the endpoint behind every "fans also like" / "similar artists" panel. Spotify had an explicit artist-relationship graph to read from. FreqBlog has no such graph, so it &lt;em&gt;derives&lt;/em&gt; one on the fly: it builds the seed artist's audio-feature centroid from their catalogue tracks, finds the nearest tracks across the catalogue, and aggregates those by artist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/related-artists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Van Halen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;related&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;artist_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;match_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; matching tracks, sample &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sample_track_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Van Halen this leads with Danger Danger, AC/DC and Def Leppard — the right neighbourhood. Each result carries &lt;code&gt;artist_name&lt;/code&gt;, a &lt;code&gt;score&lt;/code&gt;, a &lt;code&gt;match_count&lt;/code&gt; (how many of that artist's tracks landed in the seed's neighbourhood), and a &lt;code&gt;sample_track_id&lt;/code&gt; — a representative catalog id you can hand straight to &lt;code&gt;/lookup&lt;/code&gt; or to &lt;a href="https://freqblog.com/blog/build-harmonic-dj-set-planner/" rel="noopener noreferrer"&gt;&lt;code&gt;/next-track&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two design choices keep the list sensible, and they're worth understanding because they explain results you might otherwise find surprising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A per-artist cap.&lt;/strong&gt; An artist's score sums only its &lt;em&gt;top few&lt;/em&gt; track similarities, so a prolific catalogue artist with hundreds of near-tracks can't crowd out a tighter, more relevant match. Relevance, not catalogue size, wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A genre lift and a cross-genre penalty.&lt;/strong&gt; Same as &lt;code&gt;/recommendations&lt;/code&gt;: a candidate sharing the seed artist's dominant genre gets a lift; a candidate in a clearly different genre gets a penalty, which drops stragglers that are feature-close by coincidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it together: a "discover" feed in ~40 lines
&lt;/h2&gt;

&lt;p&gt;The two endpoints compose into the classic discovery surface — recommended tracks plus the artists behind them — with no extra plumbing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_track_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed_artist&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;recs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recommend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_track_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exclude_seed_artists&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;arts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Because you like that sound:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;track_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;artist_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Fans also like:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;related&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;artist_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple_ad1829eeccb70f9a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Van Halen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. The seed ids come from one &lt;code&gt;/search&lt;/code&gt; or &lt;code&gt;/lookup&lt;/code&gt; per track name; everything else is two HTTP calls. The part that used to require Spotify's recommendation graph is now a content-based query you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each call costs
&lt;/h2&gt;

&lt;p&gt;Both endpoints run through the same monthly-quota model as &lt;code&gt;/lookup&lt;/code&gt;, and you're only charged on a served &lt;code&gt;200&lt;/code&gt; — a &lt;code&gt;404&lt;/code&gt; (no seed in the catalog) or &lt;code&gt;422&lt;/code&gt; (no/invalid seed supplied) costs nothing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Quota&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /recommendations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 requests&lt;/td&gt;
&lt;td&gt;Blends up to 5 seeds into a centroid and scores the catalogue, then genre-re-ranks the pool.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /related-artists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 requests&lt;/td&gt;
&lt;td&gt;Builds the seed artist's centroid, scores the catalogue, and aggregates by artist with the per-artist cap + genre lift.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a discover feed — one &lt;code&gt;/recommendations&lt;/code&gt; (2) plus one &lt;code&gt;/related-artists&lt;/code&gt; (2) — is 4 quota units a render. The free tier's 1,000 requests/month is plenty to prototype with before you need a paid plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it differs from Spotify's endpoint — honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content-based, not collaborative.&lt;/strong&gt; It finds tracks that &lt;em&gt;sound&lt;/em&gt; similar, not tracks that share an audience. For "more like this" that's usually what you want, and it works on brand-new tracks with no listening history — where a collaborative recommender returns nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genre-aware ranking.&lt;/strong&gt; Raw audio-feature similarity is genre-blind; FreqBlog re-ranks with its own ~94%-coverage catalogue genre so a feature-close cross-genre track doesn't outrank same-genre picks. This is FreqBlog's own open data — not ListenBrainz or any external dependency, so there's no extra latency or third-party rate limit in the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Spotify ID, no Spotify account.&lt;/strong&gt; Seeds are catalog ids resolved from track names; the artist graph is derived from the catalogue. Nothing in the chain touches a deprecated Spotify endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage, not magic.&lt;/strong&gt; Similarity is computed over FreqBlog's pre-analysed catalogue, so a seed track has to be in it (or get pulled in via on-demand backfill on a prior &lt;code&gt;/lookup&lt;/code&gt;). A seed that isn't analysed yet comes back in the &lt;code&gt;seeds&lt;/code&gt; echo with &lt;code&gt;found=false&lt;/code&gt;; resolve and cache your seeds once, the same way you would seed ids for any catalog API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try FreqBlog — free tier, no card:&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;https://freqblog.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;Spotify Audio Features Is Dead. Here's What to Use Instead in 2026.&lt;/a&gt; — the landscape and the host-swap walkthrough&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/spotify-audio-features-migration-thresholds/" rel="noopener noreferrer"&gt;Migrating from Spotify Audio Features: a Field-by-Field Threshold Guide&lt;/a&gt; — re-tune your numbers once the calls flow&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/build-harmonic-dj-set-planner/" rel="noopener noreferrer"&gt;Build a Harmonic DJ Set Planner in 50 Lines&lt;/a&gt; — the set-flow layer: &lt;code&gt;/next-track&lt;/code&gt;, &lt;code&gt;/transition&lt;/code&gt;, &lt;code&gt;/setlist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/bpm-key-from-track-name/" rel="noopener noreferrer"&gt;BPM API: Get BPM and Key From a Track Name (No Audio File Required)&lt;/a&gt; — how name-based lookup resolves your seeds&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freqblog.com/blog/music-metadata-matching/" rel="noopener noreferrer"&gt;Why Music Metadata Matching Is Harder Than It Looks&lt;/a&gt; — what makes a seed resolve or miss&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://freqblog.com/blog/spotify-recommendations-replacement/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spotify</category>
      <category>api</category>
      <category>music</category>
      <category>python</category>
    </item>
    <item>
      <title>Why Music Metadata Matching Is Harder Than It Looks</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:15:57 +0000</pubDate>
      <link>https://dev.to/birrings/why-music-metadata-matching-is-harder-than-it-looks-231k</link>
      <guid>https://dev.to/birrings/why-music-metadata-matching-is-harder-than-it-looks-231k</guid>
      <description>&lt;p&gt;Someone searched our API for &lt;em&gt;YIPPEE-KI-YAY. (The Hosed Down Remix)&lt;/em&gt; — a Kesha remix from 2025. We returned nothing. Not "rate limited," not "service down" — just &lt;em&gt;not found&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable part. That track is on Apple Music. It is on Deezer. It is on Spotify. It is in MusicBrainz. Every major music database has it, and our pipeline pulls from those same sources. The track was never missing. Our &lt;strong&gt;matching layer&lt;/strong&gt; — the code that decides "this record and that record are the same song" — couldn't see it.&lt;/p&gt;

&lt;p&gt;This is the autopsy: three separate, completely mundane reasons a track that demonstrably exists failed to match, and how we fixed each. If you build anything that reconciles music metadata across sources, you will hit all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching is the actual hard part
&lt;/h2&gt;

&lt;p&gt;A music API is a stitching job. No single source has everything: iTunes has preview clips, Deezer has wide coverage, MusicBrainz has identifiers and relationships, AcousticBrainz has audio features. You query several sources and you merge the results.&lt;/p&gt;

&lt;p&gt;"Merge" hides the hard part. To merge, you first have to &lt;em&gt;decide&lt;/em&gt; that Deezer's record and MusicBrainz's record describe the same recording. That decision is "matching," and it sounds like string equality. It is not.&lt;/p&gt;

&lt;p&gt;The enemy is simple to state: the same song is written &lt;em&gt;slightly&lt;/em&gt; differently in every database. Not wildly — slightly. And "slightly different" is exactly the gap a naive matcher falls straight through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 1 — Punctuation drift
&lt;/h2&gt;

&lt;p&gt;The query was &lt;code&gt;YIPPEE-KI-YAY (The Hosed Down Remix)&lt;/code&gt;. Deezer's stored title is &lt;code&gt;YIPPEE-KI-YAY. (The Hosed Down Remix)&lt;/code&gt;. Spot the difference? There is a period after &lt;code&gt;YIPPEE-KI-YAY&lt;/code&gt; — Kesha's 2025 album stylises the song with a trailing dot. An exact string compare fails. A substring compare fails too: &lt;code&gt;yay (&lt;/code&gt; is not a substring of &lt;code&gt;yay. (&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It gets better. MusicBrainz stores the &lt;em&gt;same&lt;/em&gt; title with &lt;strong&gt;typographic hyphens&lt;/strong&gt; — Unicode U+2010 — where you typed the ASCII hyphen-minus, U+002D. They render identically in almost every font. They are different bytes. Every byte-level comparison between them fails, silently, forever.&lt;/p&gt;

&lt;p&gt;Now add lowercase versus title-case ("remix" versus "Remix"), straight versus curly apostrophes, &lt;code&gt;&amp;amp;&lt;/code&gt; versus "and", stray brackets, and the iTunes habit of writing &lt;code&gt;Song - X Remix&lt;/code&gt; where Deezer writes &lt;code&gt;Song (X Remix)&lt;/code&gt;. Two records can describe the identical recording and disagree on a dozen characters.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;strong&gt;normalisation fold&lt;/strong&gt;: before you compare anything, collapse every string to a canonical form. Lowercase it. NFKD-decompose so accented characters fold to their base (&lt;code&gt;Beyoncé&lt;/code&gt; becomes &lt;code&gt;beyonce&lt;/code&gt;). Replace &lt;em&gt;every&lt;/em&gt; punctuation and dash variant — ASCII hyphen, typographic hyphen, en dash, em dash, the lot — with a space. Collapse runs of whitespace. &lt;em&gt;Then&lt;/em&gt; compare.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="c1"&gt;# every dash variant folds to one space; punctuation folds away
&lt;/span&gt;&lt;span class="n"&gt;_DASHES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-‐‑–—&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# ascii, U+2010, U+2011, en, em
&lt;/span&gt;&lt;span class="n"&gt;_DROP&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maketrans&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_DASHES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.,!?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;()[]&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# accents -&amp;gt; base chars
&lt;/span&gt;    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;combining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_DROP&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run both titles through &lt;code&gt;fold()&lt;/code&gt; and they collapse to the same eight words: &lt;code&gt;yippee ki yay the hosed down remix&lt;/code&gt;. A match that was impossible at the byte level is trivial after the fold.&lt;/p&gt;

&lt;p&gt;The lesson: never compare provider strings raw. Fold first. Punctuation in a music title carries almost no information and offers infinite room for disagreement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 2 — Your main source's search has blind spots
&lt;/h2&gt;

&lt;p&gt;Our primary resolver is iTunes. It is an enormous, well-curated catalog. So when iTunes search returned &lt;em&gt;zero results&lt;/em&gt; for the Hosed Down Remix, the obvious conclusion was "iTunes doesn't have it."&lt;/p&gt;

&lt;p&gt;Wrong. iTunes &lt;em&gt;has&lt;/em&gt; it. We pulled the track by direct ID lookup — that returns it instantly, with a preview clip, no problem. But the iTunes &lt;em&gt;Search&lt;/em&gt; API returned nothing. Searching the song's plain title surfaced fourteen results; the remix was not one of them.&lt;/p&gt;

&lt;p&gt;The iTunes Search API is not a window onto the whole iTunes catalog. It is a &lt;strong&gt;partial, recency- and popularity-weighted index&lt;/strong&gt;. New releases, remixes, deluxe-edition cuts and long-tail tracks can be fully present in the store — buyable, previewable, reachable by ID — and simply absent from text-search results. Every catalog API has a version of this. A search surface is never the whole catalog.&lt;/p&gt;

&lt;p&gt;Two lessons. First: "search returned nothing" is not "the track does not exist" — it is "this index did not surface it." Don't let one source's blind spot become your final answer. Second: have a fallback source — we use Deezer — and understand that the fallback is now &lt;em&gt;load-bearing&lt;/em&gt;. Which means the fallback's matcher had better be solid. Ours wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 3 — "Take the first result" is not a matcher
&lt;/h2&gt;

&lt;p&gt;Here is the cheap way to resolve a track against MusicBrainz: search by title and artist, ask for one result, take &lt;code&gt;recordings[0]&lt;/code&gt;. It is one line. It feels fine. We shipped it, and it was wrong in two distinct ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It picks silently wrong.&lt;/strong&gt; &lt;code&gt;recordings[0]&lt;/code&gt; trusts MusicBrainz's relevance ranking to put the right recording first. Often it does. When it doesn't — when a live album version, a compilation re-edit or a karaoke cover ranks first — you take that, with full confidence, and never know. An audit we ran put the wrong-recording rate of naive first-result matching at roughly &lt;strong&gt;one in seven&lt;/strong&gt;. One match in seven pointed at a different recording than the one the customer asked about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And a wrong match is worse than no match.&lt;/strong&gt; This is the part that matters. If matching returns &lt;em&gt;nothing&lt;/em&gt;, you see a null — and a null is honest: "we don't have this." If matching returns the &lt;em&gt;wrong&lt;/em&gt; recording, every field you derive from it is now wrong: the release date, the ISRC, the genre, the audio features keyed off that identifier. And it all &lt;em&gt;looks&lt;/em&gt; completely plausible. A wrong match does not fail loudly. It quietly poisons the record and everything downstream of it.&lt;/p&gt;

&lt;p&gt;The fix is to stop taking the first result and start &lt;strong&gt;scoring&lt;/strong&gt;. Pull a list of candidates — twenty-five, not one. Score each against the query on &lt;em&gt;multiple independent signals&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title&lt;/strong&gt;, folded (see failure mode 1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist&lt;/strong&gt;, folded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration.&lt;/strong&gt; This is the signal that catches the wrong-cut problem. A studio single and its six-minute live version share a title and an artist; they do not share a length. If a candidate's duration is wildly off the one you expected, it is a different recording — score it &lt;em&gt;down&lt;/em&gt;, hard.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;   &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;     &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;   &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length_ms&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;expected_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length_ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;expected_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;   &lt;span class="c1"&gt;# same cut, or a different one
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then — and this is the important rule — &lt;strong&gt;only accept a confident match.&lt;/strong&gt; Set a threshold that requires real corroboration: a title match &lt;em&gt;plus&lt;/em&gt; either an artist match or a duration match. A bare title match, with nothing else agreeing, is not enough — far too many songs share a title. If nothing clears the bar, return nothing. &lt;strong&gt;Decline to guess.&lt;/strong&gt; A null you can fix on the next pass; a wrong ID you will never even notice.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The stylised-artist wrinkle.&lt;/strong&gt; This is also why you cannot lean on any &lt;em&gt;single&lt;/em&gt; signal. Deezer returns Kesha's name as &lt;code&gt;Ke$ha&lt;/code&gt;. MusicBrainz says &lt;code&gt;Kesha&lt;/code&gt;. The &lt;code&gt;$&lt;/code&gt; is not punctuation you can fold away — it is a deliberate substitution. So for this track the artist signal is simply unavailable: &lt;code&gt;ke$ha&lt;/code&gt; and &lt;code&gt;kesha&lt;/code&gt; will not match however you normalise. The match still succeeds — because the title folds clean and the duration agrees to within a second. Two signals out of three is enough. One never is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The thread: corroboration beats cleverness
&lt;/h2&gt;

&lt;p&gt;Step back and the three fixes are one idea. Punctuation folding, fallback sources, multi-signal scoring — every one of them is a way of saying: &lt;strong&gt;never trust a single signal, and never guess.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A music metadata pipeline has an asymmetric cost structure. A missing value is cheap: it is visible, it is honest, and it gets filled on the next pass. A &lt;em&gt;wrong&lt;/em&gt; value is expensive: it is invisible, it looks right, and it spreads. Genre filters return the wrong tracks. ISRC lookups resolve to the wrong release. Audio features get attributed to the wrong recording. None of it throws an error.&lt;/p&gt;

&lt;p&gt;So the discipline is conservative by design: fold before you compare, corroborate before you commit, and when the signals disagree, return null and move on. "I don't know" is a perfectly good answer from a metadata API. "Here is a confident wrong answer" is not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on the &lt;a href="https://freqblog.com/blog/music-metadata-matching/" rel="noopener noreferrer"&gt;FreqBlog blog&lt;/a&gt;. FreqBlog runs a music-metadata API that resolves tracks across iTunes, Deezer, MusicBrainz and more.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Harmonic mixing over MCP: the DJ set-builder Spotify never shipped</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:15:57 +0000</pubDate>
      <link>https://dev.to/birrings/harmonic-mixing-over-mcp-the-dj-set-builder-spotify-never-shipped-2dgo</link>
      <guid>https://dev.to/birrings/harmonic-mixing-over-mcp-the-dj-set-builder-spotify-never-shipped-2dgo</guid>
      <description>&lt;p&gt;When Spotify deprecated Audio Features, Recommendations, and Related Artists for new apps in November 2024, a wave of "drop-in replacement" APIs appeared. Most stop at parity: you send a track, you get BPM, key and energy back. Useful — but that's the same lookup Spotify already gave you.&lt;/p&gt;

&lt;p&gt;FreqBlog went a layer further. It rebuilt the dead endpoints, then shipped the thing Spotify never had: a &lt;strong&gt;set-builder&lt;/strong&gt;. Pairwise transition scoring, next-track ranking, and full setlist ordering around the Camelot wheel. And the whole surface is exposed over an &lt;strong&gt;MCP server&lt;/strong&gt;, so an LLM or agent can plan a DJ set by calling tools directly — no glue code between the model and the music theory.&lt;/p&gt;

&lt;p&gt;This is for people building music or AI tooling. I'll show the harmonic-mixing model concretely, then call it two ways: plain REST and MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parity first: the endpoints Spotify killed
&lt;/h2&gt;

&lt;p&gt;Before the interesting part, the boring-but-necessary drop-ins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /recommendations&lt;/code&gt; (and the MCP tool &lt;code&gt;get_recommendations&lt;/code&gt;) — the replacement for the removed &lt;code&gt;/v1/recommendations&lt;/code&gt;, re-ranked by genre affinity so a feature-close cross-genre track can't outrank same-genre picks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /related-artists&lt;/code&gt; — replaces the killed related-artists endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /v1/audio-features/{id}&lt;/code&gt; returns a bare Spotify &lt;code&gt;AudioFeaturesObject&lt;/code&gt;; &lt;code&gt;GET /v1/audio-features?ids=&lt;/code&gt; returns the &lt;code&gt;{"audio_features":[...]}&lt;/code&gt; array envelope. Both mirror Spotify's own shapes, so porting existing code is a small diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The native lookup is flatter and richer. &lt;code&gt;GET /lookup&lt;/code&gt; resolves a track by name, ISRC, MusicBrainz ID or Spotify ID and returns &lt;strong&gt;one flat object&lt;/strong&gt; — over 40 fields, no nesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.freqblog.com/lookup?track=Strobe&amp;amp;artist=deadmau5&amp;amp;wait=10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Api-Key: &lt;/span&gt;&lt;span class="nv"&gt;$FREQBLOG_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shape (values illustrative) — every feature is top-level, no "audio_features" wrapper&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"track_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Strobe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"artist_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deadmau5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bpm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;128.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"danceability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"genre"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"progressive house"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing: &lt;code&gt;bpm&lt;/code&gt; and &lt;code&gt;key&lt;/code&gt; are always present and non-null, and &lt;code&gt;?wait=10&lt;/code&gt; opts into a bounded synchronous mode — up to 25 seconds — that returns the analysed track inline as a &lt;code&gt;200&lt;/code&gt; instead of the default &lt;code&gt;202 + Retry-After&lt;/code&gt; when a track isn't cached yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The differentiator: harmonic mixing you can call
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Camelot in 30 seconds
&lt;/h3&gt;

&lt;p&gt;Every musical key maps to a clock position on the &lt;strong&gt;Camelot wheel&lt;/strong&gt;: a number &lt;code&gt;1&lt;/code&gt;–&lt;code&gt;12&lt;/code&gt; plus a letter (&lt;code&gt;A&lt;/code&gt; = minor, &lt;code&gt;B&lt;/code&gt; = major). Two tracks mix without a key clash when they sit next to each other on the wheel: the &lt;strong&gt;same&lt;/strong&gt; key, the &lt;strong&gt;relative&lt;/strong&gt; major/minor (same number, flipped letter), or the &lt;strong&gt;adjacent&lt;/strong&gt; &lt;code&gt;+1&lt;/code&gt;/&lt;code&gt;-1&lt;/code&gt; neighbours. Jump &lt;code&gt;+7&lt;/code&gt; and you get the classic energy-boost mix.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;find_compatible_keys&lt;/code&gt; is pure theory — no catalog hit, &lt;strong&gt;zero quota&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// find_compatible_keys(camelot="8A", extended=true)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"same"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"relative"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;// minor &amp;lt;-&amp;gt; major&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"adjacent_down"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// -1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"adjacent_up"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// +1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"energy_boost"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// +7  (extended=true)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"camelot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"energy_drop"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// -7  (extended=true)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scoring an actual transition
&lt;/h3&gt;

&lt;p&gt;Knowing which keys &lt;em&gt;could&lt;/em&gt; mix is table stakes. &lt;code&gt;score_transition&lt;/code&gt; rates how well one real track mixes into another, &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;100&lt;/code&gt;, blending Camelot key compatibility, octave-aware BPM proximity (half/double-time counts as a match), and energy smoothness — and it hands back a human reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// score_transition(from_track_id="apple_ad1829eeccb70f9a",&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;//                  to_track_id="apple_7c1120fbe0")  — costs 1 quota&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"harmonic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tempo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8A-&amp;gt;9A +1 adjacent, 126-&amp;gt;128 BPM (+2.0), energy +0.04"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no raw key/BPM endpoint anywhere that gives you &lt;em&gt;that&lt;/em&gt; — the pairwise judgement is the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  From one pick to a whole set
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;suggest_next_track&lt;/code&gt; takes the track that's playing and returns the top-N catalog tracks to play next, each with the same score, components and reason (e.g. &lt;code&gt;"11B-&amp;gt;11B same key, 118-&amp;gt;117 BPM (-0.29), energy +0.12"&lt;/code&gt;). It's genre-aware by default, so an off-genre track that only &lt;em&gt;coincidentally&lt;/em&gt; shares your key/BPM sinks to the bottom.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;build_setlist&lt;/code&gt; orders an entire crate (2–100 tracks) into a beat-matched set that follows an energy &lt;code&gt;arc&lt;/code&gt; — &lt;code&gt;peak_time&lt;/code&gt;, &lt;code&gt;warmup&lt;/code&gt;, &lt;code&gt;cooldown&lt;/code&gt;, or &lt;code&gt;flat&lt;/code&gt; — keeping every consecutive transition harmonically and tempo-smooth. It returns an overall &lt;code&gt;flow_score&lt;/code&gt;, the tracks in play order, and the per-step transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting an agent do it over MCP
&lt;/h2&gt;

&lt;p&gt;Here's where it stops being an API and starts being a capability you hand to a model. Point any MCP client at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mcp.freqblog.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That exposes twelve tools — &lt;code&gt;search_catalog&lt;/code&gt;, &lt;code&gt;get_audio_features&lt;/code&gt;, &lt;code&gt;get_audio_features_batch&lt;/code&gt;, &lt;code&gt;find_tracks_by_bpm&lt;/code&gt;, &lt;code&gt;find_tracks_by_key&lt;/code&gt;, &lt;code&gt;find_compatible_keys&lt;/code&gt;, &lt;code&gt;get_recommendations&lt;/code&gt;, &lt;code&gt;get_related_artists&lt;/code&gt;, &lt;code&gt;score_transition&lt;/code&gt;, &lt;code&gt;suggest_next_track&lt;/code&gt;, &lt;code&gt;build_setlist&lt;/code&gt;, &lt;code&gt;tag_track&lt;/code&gt;. The agent orchestrates them itself. A single prompt like &lt;em&gt;"build me a 90-minute peak-time set from these ten tracks"&lt;/em&gt; becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;search_catalog&lt;/code&gt; on each fuzzy name → concrete &lt;code&gt;itunes_track_id&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build_setlist(track_ids=[...], arc="peak_time")&lt;/code&gt; → ordered set + &lt;code&gt;flow_score&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;feed the ordered &lt;code&gt;itunes_track_id&lt;/code&gt;s to &lt;code&gt;GET /export/rekordbox&lt;/code&gt; (also &lt;code&gt;traktor&lt;/code&gt;, &lt;code&gt;m3u&lt;/code&gt;, &lt;code&gt;cuesheet&lt;/code&gt;, &lt;code&gt;csv&lt;/code&gt;) and drop the crate straight into your DJ software&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No orchestration code on your side — the tool descriptions carry enough for the model to chain them. The set-builder tools cost a little more quota than a plain lookup (&lt;code&gt;score_transition&lt;/code&gt; 1, &lt;code&gt;get_recommendations&lt;/code&gt; 2, &lt;code&gt;suggest_next_track&lt;/code&gt; 3, &lt;code&gt;build_setlist&lt;/code&gt; 5), because each one is doing real combinatorial work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth, REST, and pricing
&lt;/h2&gt;

&lt;p&gt;Auth is an &lt;code&gt;X-Api-Key&lt;/code&gt; header (a &lt;code&gt;?key=&lt;/code&gt; query fallback exists for browser and email links). Everything above is also available as plain REST — &lt;code&gt;GET /transition&lt;/code&gt;, &lt;code&gt;GET /next-track&lt;/code&gt;, &lt;code&gt;POST /setlist&lt;/code&gt;, &lt;code&gt;GET /similar?track_id=...&lt;/code&gt; — if you'd rather not run an MCP client. It's on RapidAPI too. Pricing starts at &lt;strong&gt;£0.17/1k&lt;/strong&gt;, and the free tier is 1,000 requests/month, which is plenty to prototype a set planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest gaps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's catalog-bound.&lt;/strong&gt; The set-builder tools operate on catalog &lt;code&gt;itunes_track_id&lt;/code&gt;s, so a track has to resolve first (&lt;code&gt;search_catalog&lt;/code&gt; / &lt;code&gt;/lookup&lt;/code&gt;). Coverage is deep but not universal — niche or regional catalogs have holes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spotify-ID lookups only hit a mapped subset.&lt;/strong&gt; If you're keyed on Spotify IDs, expect misses; name or ISRC resolves far more reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features are computed, not gospel.&lt;/strong&gt; BPM/key/energy come from audio analysis (Essentia); occasionally a lookup matches the wrong recording of a title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audio hosting or streaming.&lt;/strong&gt; You get features and metadata back, plus an upload-based &lt;code&gt;/analyze&lt;/code&gt; and &lt;code&gt;/identify&lt;/code&gt; — not the audio itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No beatgrid/waveform editing.&lt;/strong&gt; It plans and orders sets; it doesn't warp cue points for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Grab a free key and the OpenAPI docs at &lt;strong&gt;&lt;a href="https://api.freqblog.com/docs" rel="noopener noreferrer"&gt;api.freqblog.com/docs&lt;/a&gt;&lt;/strong&gt;, or read more about the API on &lt;strong&gt;&lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;&lt;/strong&gt;. If you're building anything that recommends, sequences, or reasons about music — especially with an LLM in the loop — the MCP endpoint is the fastest way to give your agent an ear for what actually mixes.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>music</category>
      <category>api</category>
    </item>
    <item>
      <title>Migrating from Spotify Audio Features: a Field-by-Field Threshold Guide</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Mon, 11 May 2026 22:14:39 +0000</pubDate>
      <link>https://dev.to/birrings/migrating-from-spotify-audio-features-a-field-by-field-threshold-guide-2e9b</link>
      <guid>https://dev.to/birrings/migrating-from-spotify-audio-features-a-field-by-field-threshold-guide-2e9b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Spotify killed &lt;code&gt;/audio-features&lt;/code&gt; on 2024-11-27. FreqBlog Music API offers a drop-in at &lt;code&gt;api.freqblog.com/v1/audio-features/{id}&lt;/code&gt; — same path, same response shape. The &lt;em&gt;numbers&lt;/em&gt; are ours (signal analysis via librosa + Essentia, not Spotifys ML classifiers), so a few field distributions shift. This is the field-by-field guide to re-tuning, with real distribution data from our ~57,000-track catalog.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This is a syndicated mirror — the canonical version with full formatting is at &lt;a href="https://freqblog.com/blog/spotify-audio-features-migration-thresholds/" rel="noopener noreferrer"&gt;https://freqblog.com/blog/spotify-audio-features-migration-thresholds/&lt;/a&gt; (where the table renders properly).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration shape
&lt;/h2&gt;

&lt;p&gt;You swapped the host from &lt;code&gt;api.spotify.com&lt;/code&gt; to &lt;code&gt;api.freqblog.com&lt;/code&gt;. Your existing &lt;code&gt;GET /v1/audio-features/{id}&lt;/code&gt; handler still parses the response unchanged, the unit tests pass. Then your downstream &lt;code&gt;if features.danceability &amp;gt; 0.7&lt;/code&gt; recommender starts surfacing weird tracks. Your &lt;code&gt;acousticness &amp;gt; 0.5&lt;/code&gt; rule fires on basically &lt;em&gt;everything&lt;/em&gt;. Your &lt;code&gt;liveness &amp;gt; 0.8&lt;/code&gt; filter rejects the whole catalog.&lt;/p&gt;

&lt;p&gt;The migration is byte-compatible at the &lt;strong&gt;shape&lt;/strong&gt; level. Its &lt;strong&gt;directional&lt;/strong&gt; at the value level. Spotify built their audio features by training ML classifiers on a labelled corpus; we compute ours from signal analysis. Same names, different beasts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threshold cheat sheet (catalog medians over ~8k tracks)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bpm&lt;/code&gt; / &lt;code&gt;tempo&lt;/code&gt; — median &lt;strong&gt;118.7&lt;/strong&gt; — drop-in. Plus &lt;code&gt;bpm_alt&lt;/code&gt; corrects Spotifys known half-time bug.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key&lt;/code&gt; (0–11 / -1) — drop-in. Returned as &lt;code&gt;key_int&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode&lt;/code&gt; (0/1) — drop-in.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;energy&lt;/code&gt; — median &lt;strong&gt;0.73&lt;/strong&gt; — shifted up slightly; bump thresholds ~0.05–0.1.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;valence&lt;/code&gt; — median &lt;strong&gt;0.48&lt;/strong&gt; — closest to Spotifys distribution. Minimal re-tune.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;danceability&lt;/code&gt; — median &lt;strong&gt;0.74&lt;/strong&gt; — shifted up. Old 0.65 ≈ ours 0.80.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;acousticness&lt;/code&gt;&lt;/strong&gt; — median &lt;strong&gt;0.98&lt;/strong&gt; — &lt;em&gt;do not&lt;/em&gt; use as is-acoustic. Discriminates near 0.99.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;instrumentalness&lt;/code&gt;&lt;/strong&gt; — median &lt;strong&gt;0.23&lt;/strong&gt; — signal-shape proxy, not a vocal classifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;liveness&lt;/code&gt;&lt;/strong&gt; — median &lt;strong&gt;1.00 (saturated)&lt;/strong&gt; — currently not diagnostic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;speechiness&lt;/code&gt;&lt;/strong&gt; — median &lt;strong&gt;0.45&lt;/strong&gt; — shifted up ~5–10×. Old 0.66 ≈ ours &amp;gt;0.85.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loudness&lt;/code&gt; — median &lt;strong&gt;-14 dB&lt;/strong&gt; — 3–6 dB lower than Spotify (30s window vs full-track BS.1770); use relatively, not absolutely.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;time_signature&lt;/code&gt; — always &lt;strong&gt;4&lt;/strong&gt; in practice. Treat as informational, not as a filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;acousticness&lt;/code&gt; is 0.98 for everything
&lt;/h2&gt;

&lt;p&gt;Our formula is &lt;code&gt;1 - clip(spectral_flatness / 0.15, 0, 1)&lt;/code&gt;. Low spectral flatness means the signal has strong tonal/harmonic peaks — which &lt;em&gt;most music&lt;/em&gt; has, acoustic or electric. ~99% of our catalog scores &amp;gt;0.9. If your Spotify code said &lt;code&gt;acousticness &amp;gt; 0.7 = acoustic track&lt;/code&gt; and triggered on ~20% of your library, the same threshold under FreqBlog will trigger on ~99% of it. The field is not a useless signal — it discriminates between tonal music and noise-floor content (white-noise tracks, pure ambient, recordings with heavy distortion) — but its useful threshold is up around 0.99.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;liveness&lt;/code&gt; saturates
&lt;/h2&gt;

&lt;p&gt;Formula: &lt;code&gt;clip(noise_floor_ratio * 6.0, 0, 1)&lt;/code&gt; where &lt;code&gt;noise_floor_ratio&lt;/code&gt; is the 5th-percentile RMS divided by the 95th-percentile RMS. The intuition is that live recordings have an elevated noise floor. The problem: modern mastered music has heavy dynamic-range compression, which compresses the RMS percentiles together — and the ×6 multiplier saturates at 1.0 almost immediately. Until we ship a better detector, drop the &lt;code&gt;liveness&lt;/code&gt; filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we get right
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BPM&lt;/strong&gt; + &lt;strong&gt;key/mode/key_int&lt;/strong&gt; — Essentia is mature DSP; comparable to Spotifys ML for the common cases. Plus &lt;code&gt;bpm_alt&lt;/code&gt; is a &lt;em&gt;documented improvement&lt;/em&gt; over Spotifys known half-time detection bug (Blinding Lights: Spotify said 85; perceived is 171; we ship 85 in &lt;code&gt;bpm&lt;/code&gt; and 171 in &lt;code&gt;bpm_alt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camelot + Open Key&lt;/strong&gt; — first-class fields, ready for harmonic-mixing tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identifier flexibility&lt;/strong&gt; — &lt;code&gt;/v1/audio-features/{id}&lt;/code&gt; accepts Spotify track IDs, ISRCs (with or without hyphens), &lt;code&gt;spotify:track:…&lt;/code&gt; URIs, or &lt;code&gt;open.spotify.com/track/…&lt;/code&gt; URLs. Pass whichever your data layer gives you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-tail backfill&lt;/strong&gt; — if a track isnt in the catalog, our backfill ingests it in 30s–2min and emails you when its ready. Spotifys API never had this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The full doc (with worked examples + the formula per field) is here:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://freqblog.com/blog/spotify-audio-features-migration-thresholds/" rel="noopener noreferrer"&gt;freqblog.com/blog/spotify-audio-features-migration-thresholds/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier is 1,000 requests/month, no card. Comments / use-case feedback welcome — the caveated fields (acousticness/liveness etc.) are where concrete customer demand drives the next round of improvements.&lt;/p&gt;

</description>
      <category>spotify</category>
      <category>api</category>
      <category>music</category>
      <category>audio</category>
    </item>
    <item>
      <title>AcousticBrainz Alternative in 2026: The Honest Insider's Guide</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Fri, 08 May 2026 17:29:38 +0000</pubDate>
      <link>https://dev.to/birrings/acousticbrainz-alternative-in-2026-the-honest-insiders-guide-28kn</link>
      <guid>https://dev.to/birrings/acousticbrainz-alternative-in-2026-the-honest-insiders-guide-28kn</guid>
      <description>&lt;p&gt;If you were one of the developers, researchers, or hobbyists who built on AcousticBrainz, you already know the story. The Music Technology Group at Universitat Pompeu Fabra announced the shutdown on &lt;strong&gt;16 February 2022&lt;/strong&gt;, took the live API offline, and published the entire dataset as a one-time public dump. Four years later, there's still nothing exactly like it.&lt;/p&gt;

&lt;p&gt;This post is the honest insider's view of the post-AB landscape — written by a team that uses the frozen AcousticBrainz dump in production right now, as one of four fallback layers in our music-features API. We know what works in the dump, what doesn't, and what the realistic alternatives look like in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually lost
&lt;/h2&gt;

&lt;p&gt;AcousticBrainz had three things going for it that nothing has fully replaced:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Free, public, scriptable.&lt;/strong&gt; No API key, no rate limit, no sign-up. &lt;code&gt;GET /api/v1/&amp;lt;mbid&amp;gt;/low-level&lt;/code&gt; returned ~120 fields per recording. Researchers could pull millions of rows for a paper without negotiating commercial terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MBID-keyed.&lt;/strong&gt; Every track was identified by its MusicBrainz ID, the open community-maintained identifier. That meant data from AcousticBrainz could be joined cleanly to MusicBrainz, Discogs, ListenBrainz, lyrics databases — the whole open-data ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crowd-contributed.&lt;/strong&gt; Anyone could run the AcousticBrainz client on their own audio collection and submit features back. The dataset grew from real personal libraries, not a label-licensed catalog.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of the commercial replacements has all three. Most have none.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frozen dump — still extremely useful
&lt;/h2&gt;

&lt;p&gt;This is the under-appreciated fact: &lt;strong&gt;the entire AcousticBrainz dataset is still freely downloadable&lt;/strong&gt;. The &lt;a href="https://acousticbrainz.org/download" rel="noopener noreferrer"&gt;official dump page&lt;/a&gt; hosts both the high-level (mood, genre, instrument-detection) and low-level (BPM, key, MFCCs, ~120 descriptors) tarballs as of June 2022, plus per-month deltas up to the shutdown.&lt;/p&gt;

&lt;p&gt;What you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~7.5M unique recordings&lt;/strong&gt; by MBID, with one or more contributed analyses each&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~120 low-level descriptors per track&lt;/strong&gt; — spectral centroid, MFCCs, rhythm features, tonal features, dynamic complexity, all the numbers Essentia's &lt;code&gt;MusicExtractor&lt;/code&gt; outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11 high-level classifier outputs&lt;/strong&gt; per track — genre, mood (happy/sad/aggressive/relaxed/party), instrumentalness, danceability, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AcousticBrainz JSON schema&lt;/strong&gt; — same one the live API used, so existing client code works against the dump with zero changes if you stand up your own static endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Practical setup: serve the dump as a local API
&lt;/h3&gt;

&lt;p&gt;The dump is a giant tarball of one-JSON-per-MBID. The cleanest pattern is to extract it into SQLite and serve via a thin wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode for the loader
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tarfile&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ab_features.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    CREATE TABLE ab_features (
        mbid TEXT PRIMARY KEY,
        bpm REAL,
        key TEXT,
        scale TEXT,
        danceability REAL,
        average_loudness REAL,
        dynamic_complexity REAL,
        onset_rate REAL,
        tuning_frequency REAL,
        mood_happy REAL,
        mood_sad REAL,
        mood_aggressive REAL,
        mood_relaxed REAL,
        mood_party REAL,
        genre TEXT,
        instrumentalness REAL,
        full_json TEXT
    )
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tarfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acousticbrainz-lowlevel-features-20220623.tar.zst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;mbid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR IGNORE INTO ab_features ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mbid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect the resulting SQLite to land around 2.2 GB once you've extracted the columns you actually use. Full-JSON-per-row blows it up to ~50 GB; only do that if you need every descriptor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The dump is frozen at June 2022.&lt;/strong&gt; Nothing released after that has values. For 2024-2026 releases, you'll need a live source. Use the dump as the historical layer of a tiered system — check it first, fall back to live analysis on miss.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The MBID problem
&lt;/h3&gt;

&lt;p&gt;The dump is keyed by MBID, but most of your queries will arrive with &lt;em&gt;artist + title&lt;/em&gt; strings, not MBIDs. Resolution is a separate problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hit the live MusicBrainz API: &lt;code&gt;GET /ws/2/recording/?query=artist:"&amp;lt;artist&amp;gt;" AND recording:"&amp;lt;title&amp;gt;"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Score the candidates — usually the first hit is right but featured-artist strings ("Mark Ronson featuring Bruno Mars") and remixes/covers cause noise&lt;/li&gt;
&lt;li&gt;If you find a match, take the MBID and look it up in your AB dump table&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Realistic hit rate from name-only resolution to AB-dump features: &lt;strong&gt;~50%&lt;/strong&gt;. Half your tracks will resolve cleanly, the other half will be misses for one of: no MBID assigned, MBID exists but track not in AB, featured-artist confusing the resolver, or a release after June 2022.&lt;/p&gt;

&lt;h2&gt;
  
  
  The live alternatives
&lt;/h2&gt;

&lt;p&gt;Five categories, ranked from "closest to AB's spirit" to "closest in functionality":&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Build your own with Essentia
&lt;/h3&gt;

&lt;p&gt;AcousticBrainz &lt;em&gt;was&lt;/em&gt; Essentia + crowd contributions. The same toolkit is open-source, actively maintained, and runs on a VPS. &lt;code&gt;MusicExtractor&lt;/code&gt; takes a 30-second audio clip and returns the same ~120 fields AB stored. &lt;strong&gt;Caveat:&lt;/strong&gt; you need the audio. The dump worked because users contributed local libraries; your replacement needs an audio source — iTunes 30-second previews work but rate-limit hard, full-track licenses cost money.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Self-host with the dump as primary, Essentia as fallback
&lt;/h3&gt;

&lt;p&gt;This is what we do at FreqBlog. &lt;code&gt;ab_features.db&lt;/code&gt; covers ~50% of inbound name-based queries via the MBID resolution path; for misses we run Essentia on iTunes preview clips and cache the result. The two layers complement each other — the dump catches anything pre-2022 that has an MBID; Essentia catches everything else with a commercial preview. Coverage hits ~85% in practice. The remaining 15% is bootlegs, demos, and obscurities with no preview anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hosted music-feature APIs
&lt;/h3&gt;

&lt;p&gt;The post-AB market split into two camps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spotify-shim APIs&lt;/strong&gt; — Musicae and similar, designed to feel like the deprecated Spotify &lt;code&gt;audio_features&lt;/code&gt; endpoint. Field names match Spotify's vocabulary. Useful if you're a Spotify-deprecation refugee, but they don't expose AB's ~120 low-level descriptors — just the ~11 Spotify-style high-level fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog-style APIs&lt;/strong&gt; — including &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;FreqBlog&lt;/a&gt; (full disclosure: ours). Pass artist + title, get back BPM, key, energy, mood-vector, the four AB low-level descriptors we backfilled (onset_rate, dynamic_complexity, tuning_frequency, average_loudness), and standard cross-link IDs. Different ergonomics from AB but covers most practical use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Apple Music API
&lt;/h3&gt;

&lt;p&gt;Apple's catalog API exposes &lt;code&gt;tempo&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;timeSignature&lt;/code&gt;, and a few mood/genre tags. Free for developer accounts ($99/year to ship). Doesn't expose danceability, energy, valence, or anything below the high-level surface — closer to AB's high-level layer than its low-level descriptors.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Re-run the analysis on labelled academic datasets
&lt;/h3&gt;

&lt;p&gt;For research/non-commercial work where licensing matters, the &lt;a href="https://github.com/mdeff/fma" rel="noopener noreferrer"&gt;FMA&lt;/a&gt;, &lt;a href="http://millionsongdataset.com/" rel="noopener noreferrer"&gt;Million Song Dataset&lt;/a&gt;, GTZAN, and similar academic corpora ship with audio that you can run Essentia on yourself. None match AB's coverage but all are legally clean for paper-publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field-mapping table
&lt;/h2&gt;

&lt;p&gt;For migrating code that previously called the AcousticBrainz live API, here's roughly how the field names translate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AcousticBrainz field&lt;/th&gt;
&lt;th&gt;Frozen dump&lt;/th&gt;
&lt;th&gt;FreqBlog&lt;/th&gt;
&lt;th&gt;Essentia (DIY)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rhythm.bpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RhythmExtractor2013.bpm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tonal.key_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KeyExtractor.key&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tonal.key_scale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KeyExtractor.scale&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;highlevel.danceability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;danceability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SVM model, deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;highlevel.mood_happy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mood_vector.happy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SVM model, deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lowlevel.average_loudness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;average_loudness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Loudness&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lowlevel.dynamic_complexity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dynamic_complexity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DynamicComplexity&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rhythm.onset_rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onset_rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OnsetRate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tonal.tuning_frequency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tuning_frequency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TuningFrequency&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;highlevel.genre_*&lt;/code&gt; (multiple)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;genre&lt;/code&gt; (single)&lt;/td&gt;
&lt;td&gt;SVM models, deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lowlevel.mfcc.mean[0..12]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;not exposed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MFCC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;lowlevel.spectral_*&lt;/code&gt; (~30 fields)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;not exposed&lt;/td&gt;
&lt;td&gt;various spectral algorithms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dump is the only option if you need the deep low-level vector (~120 fields). Hosted APIs typically expose the ~10-15 fields that map cleanly to product use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What no replacement gives you back
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The crowd contribution loop.&lt;/strong&gt; AB grew because users ran the client on their personal libraries and submitted back. No commercial replacement has that bottom-up data flow — it's all top-down catalog ingestion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The high-level mood/genre classifiers.&lt;/strong&gt; MTG's own shutdown post acknowledged the high-level model quality wasn't reliable enough — that's part of why they killed AB. Essentia's documentation now flags those SVM models as deprecated. &lt;strong&gt;Reproducing them locally reproduces a known-broken system.&lt;/strong&gt; If you need genre/mood at production-grade quality, the modern path is the Essentia Labs &lt;a href="https://essentia.upf.edu/models.html" rel="noopener noreferrer"&gt;MusiCNN models&lt;/a&gt; (TensorFlow-based, much better trained), not the deprecated SVMs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MBID-keyed open-data ecosystem.&lt;/strong&gt; AB joined cleanly to MusicBrainz/Discogs/ListenBrainz because everyone shared the MBID identifier. Commercial APIs key on their own internal IDs (or Spotify track IDs, which are now deprecated for new apps). Cross-linking is harder.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Academic / research / one-shot dataset analysis&lt;/strong&gt; → download the dump. It's free, complete through June 2022, and citable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building a product that needs MIR data on current releases&lt;/strong&gt; → tier the dump under a live source (Essentia self-hosted, or a hosted API). Use the dump as the cheap-and-deep layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building a quick app that just needs BPM/key/energy&lt;/strong&gt; → a hosted catalog API (FreqBlog, Musicae, or Apple Music) is faster to integrate than running Essentia yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need the deep low-level vector (MFCCs, spectral centroid, etc.) on every track&lt;/strong&gt; → only the dump or your own Essentia worker will give you those. No hosted API exposes them at retail prices.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published at &lt;a href="https://freqblog.com/blog/acousticbrainz-alternative/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;. If you've got a specific MIR migration question (resolver issues, dataset licensing, etc.), drop it in the comments.&lt;/p&gt;

</description>
      <category>music</category>
      <category>api</category>
      <category>mir</category>
      <category>python</category>
    </item>
    <item>
      <title>BPM API: Get BPM and Key From a Track Name (No Audio File Required)</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Fri, 08 May 2026 17:29:37 +0000</pubDate>
      <link>https://dev.to/birrings/bpm-api-get-bpm-and-key-from-a-track-name-no-audio-file-required-4ibi</link>
      <guid>https://dev.to/birrings/bpm-api-get-bpm-and-key-from-a-track-name-no-audio-file-required-4ibi</guid>
      <description>&lt;p&gt;Most "music analysis" APIs want a 30-second audio clip and a multipart upload. That's fine if you're building a DJ app where the user is dragging files in. It's a wall if you're building a fitness app trying to match treadmill cadence to a Spotify playlist, a music recommender that takes a track URL and returns "more like this," or honestly, any app where the user types &lt;em&gt;Blinding Lights&lt;/em&gt; and expects the BPM back.&lt;/p&gt;

&lt;p&gt;This is the architectural split nobody talks about: &lt;strong&gt;upload-based analysis APIs&lt;/strong&gt; vs &lt;strong&gt;catalog-based lookup APIs&lt;/strong&gt;. They look superficially similar (both return BPM, key, energy, etc.), but the failure modes are completely different and the right one for your app depends on what your user actually has in their hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two architectures, in one paragraph each
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Upload-based
&lt;/h3&gt;

&lt;p&gt;You POST audio bytes; the API runs &lt;a href="https://essentia.upf.edu/" rel="noopener noreferrer"&gt;Essentia&lt;/a&gt; or &lt;a href="https://librosa.org/" rel="noopener noreferrer"&gt;librosa&lt;/a&gt; on them server-side; you get features back. Examples: AudD, Cyanite, AI Mastering. Hit rate is 100% (any audio works) but the user must have the file. No file means no answer. Pricing is usually per-call and on the higher end (compute-bound).&lt;/p&gt;

&lt;h3&gt;
  
  
  Catalog-based
&lt;/h3&gt;

&lt;p&gt;You GET with &lt;code&gt;?track=Blinding+Lights&amp;amp;artist=The+Weeknd&lt;/code&gt;; the API looks the track up in a pre-analyzed library and returns the cached features. Hit rate is &amp;lt;100% (only works for tracks already in the catalog) but no audio file required, the response is fast (typically &amp;lt;100 ms), and pricing is usually per-lookup and lower (no per-call compute). When the catalog misses, the better APIs queue an on-demand analysis and email the user when it lands.&lt;/p&gt;

&lt;p&gt;Spotify's &lt;code&gt;audio_features&lt;/code&gt; endpoint was a catalog-based API: you passed a Spotify track ID, you got the features. &lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;It died in November 2024&lt;/a&gt;, which is why this post exists at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "name-based lookup" actually does
&lt;/h2&gt;

&lt;p&gt;The pattern, in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.freqblog.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blinding Lights&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The Weeknd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# {
#   "track_name": "Blinding Lights",
#   "artist_name": "The Weeknd",
#   "audio_features": {
#     "bpm": 171.0,
#     "bpm_alt": 85.5,           # half-time variant
#     "key": "C#-Minor",
#     "camelot": "12A",
#     "open_key": "5m",
#     "energy": 0.91,
#     "danceability": 0.85,
#     ...
#   }
# }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's actually happening behind the request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Catalog hit&lt;/strong&gt; — the lookup matches by name and returns within ~10 ms. Most popular tracks are pre-analyzed and live here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog miss but reachable&lt;/strong&gt; — the API can't find the track, but it can find the iTunes preview for it. It returns HTTP 202 ("queued"), kicks off a background analysis (download preview → run Essentia → cache result), and emails the user when the data is ready (typically 30 seconds to 2 minutes later). Subsequent calls hit the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genuinely unfindable&lt;/strong&gt; — demos, bootlegs, alternate-takes that have no commercial release. The API returns 404 or an empty object. Plan for this in your UX.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The 202 path is the differentiator.&lt;/strong&gt; Most catalog APIs return 404 on miss and leave you to figure it out. The "queue and email" pattern means a track that's missing today is in the catalog tomorrow — the catalog grows toward the long tail of what your users actually search for, instead of being frozen to whatever was popular when the dataset was built.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you can ask for by name
&lt;/h2&gt;

&lt;p&gt;The full set of fields a good name-based BPM API should return:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;171.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tempo for sync/cadence/sorting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bpm_alt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;85.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Half-time variant when the detector reports the wrong octave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"C#-Minor"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Musical key (12 pitches × major/minor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;camelot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"12A"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DJ-friendly key notation for harmonic mixing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;open_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"5m"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alternative DJ notation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;energy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.91&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0-1 normalised intensity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;danceability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0-1 rhythmic groove score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;valence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0-1 musical positivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loudness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-5.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dB FS reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time_signature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Beats per bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;genre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"pop"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coarse Last.fm-style tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;isrc&lt;/code&gt; + &lt;code&gt;mbid&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"USUM72003867"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cross-link IDs to other catalogs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The hit rate is the metric that matters
&lt;/h2&gt;

&lt;p&gt;The single most important question to ask any catalog-based API: &lt;strong&gt;what fraction of the tracks my users will search for are actually in your catalog?&lt;/strong&gt; Vendors will quote you a top-line track count ("10M tracks!") which is meaningless unless you know the distribution.&lt;/p&gt;

&lt;p&gt;Three honest signals to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Charts coverage&lt;/strong&gt; — if the catalog has the current Apple Music / Spotify global top-200, you'll hit on most pop tracks people search for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decade depth&lt;/strong&gt; — if 80% of the catalog is 2020+, anything pre-2010 will miss. Music apps that surface throwback tracks need 60s/70s/80s coverage too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-demand backfill&lt;/strong&gt; — "we'll fetch and analyze if it's missing" turns a hard miss into a slow hit. Bigger long-term win than headline catalog size.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the catalog misses, what do you do?
&lt;/h2&gt;

&lt;p&gt;Three patterns work:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Queue + notify
&lt;/h3&gt;

&lt;p&gt;The 202-pattern. Show "Analyzing… we'll email you" in your UX. Comes back via webhook or the user re-querying. Best when users care about that specific track and will wait.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.freqblog.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                 &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Queued. Show a "checking..." UI. Re-poll in 30s, or wire a webhook.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 2: Bulk pre-warm
&lt;/h3&gt;

&lt;p&gt;If you know the user's library upfront (e.g. they uploaded a CSV of their playlist), POST the whole list to &lt;code&gt;/bulk&lt;/code&gt;. The API returns immediately with a mix of hits + queued items, and the queued ones land via email/webhook over the next few minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Graceful degradation
&lt;/h3&gt;

&lt;p&gt;If your UX doesn't need every field, the API returns what it has and nulls the rest. Render BPM and key when present; hide the energy/valence chips when null. Users don't know what they're missing if you don't show empty boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  BPM accuracy: a footnote that matters
&lt;/h2&gt;

&lt;p&gt;BPM detectors get confused on roughly 20% of dance tracks. The Weeknd's &lt;em&gt;Blinding Lights&lt;/em&gt; is the textbook example: the actual tempo is 171 BPM, but Spotify (and most DSP-based detectors) returns ~85.5 because the algorithm latches onto the half-time pulse. The fix: expose &lt;em&gt;both&lt;/em&gt; the detected BPM and the half/double-time alternate, and let your app decide which to display based on the genre context.&lt;/p&gt;

&lt;p&gt;Any BPM API you evaluate should either fix the half-time issue automatically or expose enough context (the alternate value, a confidence score, or both) for you to fix it client-side. Spotify's old &lt;code&gt;audio_features&lt;/code&gt; didn't, which created a long tail of "this app says my song is 85 BPM" UX problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What no name-based API will give you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-bar/per-beat timing.&lt;/strong&gt; Catalog APIs return whole-track features. If you need beat-level timing for visuals or sync, you need an upload-based API or you'll have to run a beat tracker on the audio yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracks below a popularity floor.&lt;/strong&gt; Bootlegs, demos, edit-only releases, and most non-commercial music isn't in any catalog. If your app routes around obscure music, you'll need either upload-based fallback or to live with the gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time analysis of &lt;em&gt;this&lt;/em&gt; microphone input.&lt;/strong&gt; That's a DSP problem, not an API one. Use librosa or Essentia client-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users have audio files in hand?&lt;/strong&gt; Upload-based API. Pay per call, accept the higher latency, get 100% coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users type a track name or paste a URL?&lt;/strong&gt; Catalog-based. Fast, cheap, plan for ~80-95% hit rate plus a graceful "queued" path for the misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both, sometimes?&lt;/strong&gt; Most apps end up here. Use a catalog API as the fast path and an upload API (or your own Essentia worker) as the fallback for misses + obscure content.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published at &lt;a href="https://freqblog.com/blog/bpm-key-from-track-name/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;. The &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;FreqBlog Music API&lt;/a&gt; has a free tier — no card required.&lt;/p&gt;

&lt;p&gt;Got a specific catalog-vs-upload architecture question for your app? Drop it in the comments.&lt;/p&gt;

</description>
      <category>api</category>
      <category>music</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Spotify's audio_features API died in 2024. Here's what to use in 2026.</title>
      <dc:creator>Freqblog</dc:creator>
      <pubDate>Sat, 11 Apr 2026 18:06:40 +0000</pubDate>
      <link>https://dev.to/birrings/spotifys-audiofeatures-api-died-in-2024-heres-what-i-built-to-replace-it-3dn3</link>
      <guid>https://dev.to/birrings/spotifys-audiofeatures-api-died-in-2024-heres-what-i-built-to-replace-it-3dn3</guid>
      <description>&lt;p&gt;If you maintain anything that ever called &lt;code&gt;GET /v1/audio-features/{id}&lt;/code&gt;, you already know how this story starts. On &lt;strong&gt;November 27, 2024&lt;/strong&gt;, Spotify quietly killed &lt;code&gt;audio_features&lt;/code&gt;, &lt;code&gt;audio_analysis&lt;/code&gt;, recommendations, related artists, and featured playlists in a single developer-blog post. New apps got &lt;code&gt;403&lt;/code&gt; the same day. Twenty months later there's still no official replacement, and the &lt;a href="https://developer.spotify.com/documentation/web-api/tutorials/february-2026-migration-guide" rel="noopener noreferrer"&gt;February 2026 changes&lt;/a&gt; made the rest of the Web API harder to use, not easier.&lt;/p&gt;

&lt;p&gt;This post is the honest version of "what now?" — the real options, where they fall short, and a Python migration example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually died
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;What it gave you&lt;/th&gt;
&lt;th&gt;Status (July 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/audio-features/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BPM, key, mode, danceability, energy, valence, acousticness, instrumentalness, liveness, loudness, speechiness, time_signature&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; for new apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/audio-analysis/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-bar / per-beat / per-segment timing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; for new apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/recommendations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Genre-seed and feature-seed recommendations&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; for new apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/artists/{id}/related-artists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Up to 20 related artists&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; for new apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/browse/featured-playlists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Editorial curation lists&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; for new apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`/me/top/{tracks\&lt;/td&gt;
&lt;td&gt;artists}`&lt;/td&gt;
&lt;td&gt;User listening history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apps that already had a quota extension in flight on Nov 27 2024 are still live. Everyone else gets 403. There's no waitlist, no path forward, and no public statement that this will change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Spotify isn't bringing it back
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;audio_features&lt;/code&gt; endpoint was a thin wrapper around analysis Spotify acquired when they bought The Echo Nest in 2014, plus features derived from the AcousticBrainz dataset (which itself shut down in 2022 for similar "we don't want to host this anymore" reasons). Returning eleven floats per track in a 100M-track catalog costs Spotify infrastructure for zero strategic value.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;recommendations&lt;/code&gt; endpoint was worse for them — letting any third-party build a Spotify-quality recommender by spamming &lt;code&gt;seed_genres=house&amp;amp;target_energy=0.8&lt;/code&gt; undermined the whole "premium algorithm" pitch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stop asking "will Spotify bring this back?" The right question is "what's the smallest dependency I can rebuild this on so I'm never one PR-merge away from being broken again?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The realistic options
&lt;/h2&gt;

&lt;p&gt;Five categories, ranked by how quickly you can ship the migration:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Apple Music API
&lt;/h3&gt;

&lt;p&gt;Apple exposes &lt;code&gt;tempo&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;timeSignature&lt;/code&gt;, &lt;code&gt;contentRating&lt;/code&gt;, plus mood/genre tags. Free for dev accounts, $99/year to ship. &lt;strong&gt;Missing:&lt;/strong&gt; danceability, energy, valence, acousticness, instrumentalness, liveness — six of the eleven Spotify fields gone. Best if you only need BPM + key from a major catalog and don't mind paying Apple.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build your own with Essentia
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://essentia.upf.edu/" rel="noopener noreferrer"&gt;Essentia&lt;/a&gt; is the open-source MIR toolkit Spotify &lt;em&gt;itself&lt;/em&gt; used to derive most of the deprecated values. &lt;code&gt;MusicExtractor&lt;/code&gt; takes a 30-second audio clip and returns BPM, key, danceability, average loudness, dynamic complexity, tuning frequency, onset rate. Compute cost only — but &lt;strong&gt;you need the audio&lt;/strong&gt;. Spotify never let you download it; iTunes 30-second previews work but rate-limit hard (~25 RPM). Plan for a multi-week backfill on any catalog over 10k tracks.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AcousticBrainz public dump (free, frozen)
&lt;/h3&gt;

&lt;p&gt;The MusicBrainz folks did &lt;em&gt;exactly&lt;/em&gt; what Spotify won't: published the entire AcousticBrainz dataset (7.5M tracks, 11 high-level features and ~120 low-level descriptors per track) as a one-time public dump in July 2022 before shutting the live service down. &lt;strong&gt;Catch:&lt;/strong&gt; frozen in time — nothing released after July 2022 has values. Coverage on tracks with a MusicBrainz ID is ~60% of recent commercial releases; without an MBID, nothing. Useful as a baseline layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Musicae API
&lt;/h3&gt;

&lt;p&gt;Built specifically as a Spotify shim — same field names, same value ranges, similar ergonomics. The closest drop-in if minimising migration diff matters more than anything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;FreqBlog Music API&lt;/a&gt; (full disclosure: ours)
&lt;/h3&gt;

&lt;p&gt;Different design choice: a &lt;em&gt;catalog&lt;/em&gt; first, not a wrap-an-id service. Pass a track-name + artist string (or an ISRC, MusicBrainz ID, or Spotify ID) to &lt;code&gt;GET /lookup&lt;/code&gt; and get back a single flat object — BPM, key (name + Camelot + Open Key), energy, loudness, danceability, valence, mood, time signature, ISRC, MBID, genre, and 40-odd fields in total. From £0.17 per 1,000 requests; the free tier is 1,000 requests/month with no card. Missing tracks are backfilled via a queue — a miss on a name lookup returns &lt;code&gt;202 Accepted&lt;/code&gt; and the next call has the data, or you can pass &lt;code&gt;?wait=N&lt;/code&gt; (0–25 s) to block for the analysed result inline (HTTP 200) on the first call.&lt;/p&gt;

&lt;p&gt;Two things it has that Spotify never did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A set-builder layer&lt;/strong&gt;, which steps into the gap left by the killed &lt;code&gt;/recommendations&lt;/code&gt;: &lt;code&gt;/recommendations&lt;/code&gt; (seed with tracks you already like) and &lt;code&gt;/similar&lt;/code&gt; for nearest-neighbour discovery, plus &lt;code&gt;/transition&lt;/code&gt; (score how well one track mixes into another), &lt;code&gt;/next-track&lt;/code&gt; (ranked next picks for a seed) and &lt;code&gt;/setlist&lt;/code&gt; (order a whole crate into a beat-matched, harmonic energy arc).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A remote MCP server&lt;/strong&gt; at &lt;code&gt;mcp.freqblog.com/mcp&lt;/code&gt;, so an LLM or agent can query the catalog directly — search, audio-features, BPM/key search, harmonic-key matching, recommendations and the set-builder tools — without you writing HTTP glue. There's also a RapidAPI listing if you'd rather consume it through that gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It still won't give you per-segment analysis, and the Essentia-derived acoustic-model fields (&lt;code&gt;speechiness&lt;/code&gt;, &lt;code&gt;instrumentalness&lt;/code&gt;, &lt;code&gt;liveness&lt;/code&gt;, &lt;code&gt;acousticness&lt;/code&gt;) are approximations, not Spotify's proprietary numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration walkthrough: Spotify → FreqBlog
&lt;/h2&gt;

&lt;p&gt;Most apps that depended on &lt;code&gt;audio_features&lt;/code&gt; were doing one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look up known tracks&lt;/strong&gt; — user pastes a Spotify URL, app shows BPM/key/energy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter by feature ranges&lt;/strong&gt; — "give me upbeat tracks above 120 BPM in a major key"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pattern 1: Single-track lookup
&lt;/h3&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spotify_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.spotify.com/v1/audio-features/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;spotify_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# &amp;gt;&amp;gt;&amp;gt; get_features("0VjIjW4GlUZAMYd2vXMi3b", token)
# {"danceability": 0.514, "energy": 0.730, "key": 1, "tempo": 171.005, ...}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.freqblog.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;track_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# &amp;gt;&amp;gt;&amp;gt; get_features("Blinding Lights", "The Weeknd", api_key)
# {"track_name": "Blinding Lights", "artist_name": "The Weeknd",
#  "bpm": 171.0, "key": "C#-Minor", "camelot": "12A",
#  "energy": 0.91, "danceability": 0.85, ...}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape of the request changes — name+artist instead of a Spotify ID, because we're a catalog not a Spotify-wrapper. The response is a flat object like Spotify's old one (no &lt;code&gt;audio_features&lt;/code&gt; wrapper), so the only real work is renaming a few fields: &lt;code&gt;bpm&lt;/code&gt; not &lt;code&gt;tempo&lt;/code&gt;, and &lt;code&gt;key&lt;/code&gt; is a string like &lt;code&gt;"C#-Minor"&lt;/code&gt; rather than a pitch-class integer. A 5-line adapter maps them across.&lt;/p&gt;

&lt;h3&gt;
  
  
  Field mapping
&lt;/h3&gt;

&lt;p&gt;For the 11 Spotify fields, here's how they map to the replacement landscape:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spotify field&lt;/th&gt;
&lt;th&gt;Apple Music&lt;/th&gt;
&lt;th&gt;Essentia (build-your-own)&lt;/th&gt;
&lt;th&gt;FreqBlog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tempo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tempo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bpm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;key.key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;key&lt;/code&gt; + &lt;code&gt;camelot&lt;/code&gt; + &lt;code&gt;open_key&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;— (in &lt;code&gt;key&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;key.scale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time_signature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timeSignature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;time_signature&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;danceability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;danceability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;danceability&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;energy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;derived from RMS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;energy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;valence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;via SVM model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;valence&lt;/code&gt; (where available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loudness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;average_loudness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loudness_db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acousticness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;via SVM model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;acousticness&lt;/code&gt; &lt;em&gt;(approx)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instrumentalness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;via SVM model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;instrumentalness&lt;/code&gt; &lt;em&gt;(approx)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;liveness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;via SVM model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;liveness&lt;/code&gt; &lt;em&gt;(approx)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;speechiness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;via SVM model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;speechiness&lt;/code&gt; &lt;em&gt;(approx)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Spotify's exact &lt;code&gt;speechiness&lt;/code&gt; and &lt;code&gt;instrumentalness&lt;/code&gt; numbers only live in its frozen-in-time proprietary models — the open-source approximations (Essentia's SVM models, which FreqBlog runs) won't reproduce them. Be honest with users about which you actually need vs which were nice-to-haves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Recommendations and discovery
&lt;/h3&gt;

&lt;p&gt;Spotify's &lt;code&gt;/recommendations&lt;/code&gt; — the &lt;code&gt;seed_genres=house&amp;amp;target_energy=0.8&amp;amp;min_tempo=120&lt;/code&gt; dance — is gone too. There's no exact drop-in, because that endpoint let you filter a 100M-track catalog by target features. FreqBlog works from a seed track instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;# Spotify (deprecated)
GET /v1/recommendations?seed_genres=house&amp;amp;target_energy=0.8&amp;amp;min_tempo=120

# FreqBlog: seed with tracks you already like
GET /recommendations?track=Music%20Sounds%20Better%20With%20You&amp;amp;artist=Stardust&amp;amp;limit=20

# ...or find the nearest neighbours to one track by cosine similarity
GET /similar?track_id=&amp;lt;itunes_track_id&amp;gt;&amp;amp;limit=10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/similar&lt;/code&gt; takes a catalog track id (&lt;code&gt;track_id&lt;/code&gt;, &lt;em&gt;not&lt;/em&gt; a Spotify id) and returns the acoustically nearest neighbours over an 18-feature embedding — no seed-genre tuning. If you'd rather discover by tempo or key first, &lt;code&gt;/bpm?bpm=128&lt;/code&gt; and &lt;code&gt;/key&lt;/code&gt; query the catalog directly.&lt;/p&gt;

&lt;p&gt;Set-builders go a step past a flat recommendation list: &lt;code&gt;/transition&lt;/code&gt; scores how well two tracks mix, &lt;code&gt;/next-track&lt;/code&gt; ranks the best follow-on for a seed, and &lt;code&gt;/setlist&lt;/code&gt; orders a whole crate into a beat-matched energy arc — the harmonic-mixing layer Spotify never shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What no replacement gives you
&lt;/h2&gt;

&lt;p&gt;Be realistic about the gaps. None of the alternatives — including ours — replicate Spotify's old offering one-for-one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-segment analysis with timestamps.&lt;/strong&gt; Replicating &lt;code&gt;/audio-analysis&lt;/code&gt; requires you to run a beat-tracker on the audio yourself. &lt;code&gt;librosa.beat.beat_track&lt;/code&gt; or Essentia's &lt;code&gt;RhythmExtractor2013&lt;/code&gt; get you most of the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speechiness, instrumentalness, liveness.&lt;/strong&gt; Spotify trained these on internal labelled data nobody else has. Open-source Essentia models exist but Essentia's own docs flag them as deprecated due to data-quality concerns — reproducing them locally reproduces a known-noisy system. Where a replacement (FreqBlog included) returns these fields, treat them as approximations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage of obscure tracks.&lt;/strong&gt; Spotify had every track in their catalog. Every alternative has a coverage gap on long-tail releases. Plan for a "no data yet, queue for analysis" path in your UX.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already on Apple Music?&lt;/strong&gt; Use their API, accept the field reduction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need a 1:1 Spotify shim?&lt;/strong&gt; Musicae is closest by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have audio files already?&lt;/strong&gt; Build with Essentia, pay only compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need a queryable catalog by name+artist — BPM, key, Camelot, similarity, harmonic-mixing set-builders, even a remote MCP server your agent can call?&lt;/strong&gt; &lt;a href="https://freqblog.com/" rel="noopener noreferrer"&gt;Try FreqBlog&lt;/a&gt; — free tier, no card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building research/non-commercial work?&lt;/strong&gt; AcousticBrainz dump is free and large enough.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published at &lt;a href="https://freqblog.com/blog/spotify-audio-features-replacement-2026/" rel="noopener noreferrer"&gt;freqblog.com&lt;/a&gt;. If you found this useful, the FreqBlog Music API has a free tier — no card required.&lt;/p&gt;

&lt;p&gt;Got questions about a specific migration scenario? Drop them in the comments and I'll do my best.&lt;/p&gt;

</description>
      <category>spotify</category>
      <category>api</category>
      <category>music</category>
      <category>python</category>
    </item>
  </channel>
</rss>
