Borrowing Taste From Discogs

March 29, 2026 · 2112 words · 11 min read

Apple Music can't recommend music and Spotify killed the API that could, so I hacked together a new solution.

Disclaimer This is comfortably the hackiest thing I've built in a while.

I used to have a tool that fixed Apple Music's recommendations by piggybacking on Spotify's /recommendations endpoint. I would describe a vibe in the terminal, an LLM would turn it into API parameters, then Spotify would return tracks to an Apple Shortcut that would build the playlist. When it worked, it worked great, until Spotify deprecated the endpoint in November 2024 with no warning or any replacement. Since my tool died overnight, I grieved its loss, shelved it and went back to suffering through Apple Music's algorithm, which to this day recommends me playlists that are mostly bands I already listen to mixed with bands I actively dislike.

A few days ago it recommended me a "Dark Ambient" playlist that opened with Imagine Dragons. Don't get me wrong, I love Imagine Dragons, but dark ambient, they are not. I closed the app and opened my editor.

The thing I kept circling back to while thinking about a replacement was that recommendation engines are only as good as the data underneath them. That was Spotify's secret, it worked because millions of listeners were constantly generating taste signals every time they played something, but there's another pool of taste data that's arguably richer, and it's been sitting in plain sight for over twenty years without anyone exploiting it. Discogs has nearly nine million users cataloguing their record collections, and these are not casual listeners. There are people who own three thousand records and will track every detail about each release.

Why do I know that? Because to a lesser degree I one of them! I ruthlessly tag every vinyl release I own with genres and styles and if I could, I would do even more. People there keep careful track of what they want and what they have, they organise music with a specificity that no streaming algorithm comes close to. So this is a community that has, over two decades, built what amounts to a massive hand-curated map of how music relates to itself. I'm borrowing that map.

The Discogs API exposes a search endpoint that lets you filter releases by style, genre, year, country, label, and format, and it also returns community data like how many users want or have a given release, which is a surprisingly useful quality signal. A record that twelve thousand people want and eight thousand people own is probably good, but it's also a mainstream hit, while a record in the same style that forty people have catalogued might be an obscure gem or it might be forgettable, but the ratio tells you something. None of this makes a recommendation engine in the traditional sense, but a database with exceptionally good metadata maintained by obsessive humans is exactly what I need, provided that I query it right.

There's actually some recent academic work that supports why this approach works. A paper by Terence Zeng published earlier this month explores mood-assisted music recommendation, where user mood is used as a direct input to the recommendation process instead of relying solely on listening history. The study found a statistically significant improvement in recommendation quality when mood was incorporated, which makes intuitive sense, your emotional state shapes what you want to hear far more than what you listened to last week. The paper uses the energy-valence spectrum for mood mapping, which is more formal than what I'm doing, but the core insight is the same: if you let people describe how they feel instead of inferring it from their behaviour, the recommendations get better! My version of that insight is just cruder and instead of energy-valence coordinates, I type "doom metal but pretty" and let an LLM figure out what I mean.

The pipeline I built has three parts, plus a feedback loop that makes the whole thing get smarter over time. I describe a vibe to Claude via the Anthropic API, and Claude then translates that into a structured Discogs search query. The Discogs API returns releases. A different call to Claude takes those releases and composes a playlist. An Apple Shortcut puts the tracks into Apple Music and after I listen, I rate the playlist. That rating finally feeds back into which Discogs users I trust for future queries. The whole thing runs on my home server, same setup as everything else I run.

The first part is where Claude does something I couldn't get Spotify to do well, which is understand what I actually mean. When I used to say "doom metal but pretty" to the old pipeline, I had to manually translate that into seed_genres: doom-metal, target_energy: 0.3, target_valence: 0.2, and hope the engine interpreted those numbers the way I intended. With the new setup, Claude understands the vibe and maps it to Discogs parameters, the styles, genres, year ranges, and labels that correspond to the mood I described.

typescript
const query = await anthropic.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  system: `You translate music vibe descriptions into Discogs API search parameters.
Available parameters: style, genre, year (or year range), country, label, format.
Discogs styles are specific (e.g. "Doom Metal", "Shoegaze", "Gothic Metal", "Dream Pop").
Return 2-3 separate queries that approach the vibe from different angles.
Return JSON only. No explanation.
 
Format: { "queries": [{ "style": "...", "genre": "...", "year": "..." }] }`,
  messages: [{ role: "user", content: vibePrompt }],
});

For "doom metal but pretty" Claude returns queries like { style: "Doom Metal", genre: "Rock" }, { style: "Atmospheric Black Metal", genre: "Rock" }, and { style: "Shoegaze", genre: "Rock", year: "2010-2025" }. Three different angles on the same mood. That's the part I could never get right with Spotify's genre seeds, which were coarse and opaque. Discogs styles are granular because they were defined by people who care about the difference between Doom Metal and Stoner Rock, and Claude knows that difference too.

The second part hits the Discogs API with each query and collects releases, but it doesn't just grab whatever comes back, it also tracks who contributed each release to the Discogs database.

typescript
const searchDiscogs = async (params: Record<string, string>) => {
  const query = new URLSearchParams({
    ...params,
    type: "release",
    per_page: "25",
  });
 
  const res = await fetch(`https://api.discogs.com/database/search?${query}`, {
    headers: {
      Authorization: `Discogs token=${DISCOGS_TOKEN}`,
      "User-Agent": "VibePipeline/1.0",
    },
  });
 
  const data = await res.json();
  return data.results.map((r: any) => ({
    id: r.id,
    title: r.title,
    year: r.year,
    style: r.style,
    genre: r.genre,
    community: r.community,
    user_data: r.user_data,
  }));
};
 
const allQueries = JSON.parse(queryResponse).queries;
const results = await Promise.all(allQueries.map(searchDiscogs));
const pool = results.flat();

Two or three queries with twenty-five results each gives me a pool of fifty to seventy-five releases. Some duplicates, some obvious picks I already know, but also a lot of records I've never heard of, that were tagged by someone who clearly listens to the same kind of music I do. That's the whole point, asking a community of collectors what belongs next to the records I already love, instead of delegating that judgement to an opaque algorithm that doesn't even understand the genres it's trying to recommend within.

The third part is where Claude takes that pool of releases, looks at what's in them, and composes an actual playlist that matches the original vibe. Before composing, it checks my local trust database to see if any releases in the pool came from users I've implicitly rated highly in the past, and weights those more heavily.

typescript
const playlist = await anthropic.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 2048,
  system: `You are composing a playlist. The user described a vibe and a search returned 
a pool of releases from Discogs. Pick 20-25 specific tracks from these releases 
that best match the original vibe. Prioritise discovery — lean toward artists the 
user is unlikely to already know. Releases marked as "trusted" come from users 
whose taste has been validated — weight these more heavily. You may include a few 
tracks from outside the pool if they fit perfectly. Return JSON only. No explanation.
 
Format: { "tracks": [{ "artist": "...", "title": "..." }] }`,
  messages: [
    {
      role: "user",
      content: `Vibe: "${vibePrompt}"\n\nReleases:\n${formatReleases(pool, trustScores)}`,
    },
  ],
});

This is the step the old Spotify version didn't have at all. Spotify's endpoint returned tracks directly, which was convenient but meant you had no say in the curation. Here, Claude can look at a Woods of Ypres album and pick the atmospheric tracks instead of the heavier ones because it understands what "pretty" means in the context of doom metal. It can also throw in a few tracks from outside the pool if it knows something that fits, which keeps the playlists from feeling mechanical.

The last step is getting the playlist onto my phone, since there's no native integration with Apple Music. The server emails me a link with the encoded playlist JSON embedded in a Shortcuts URL scheme. I tap it on my phone, it opens the Shortcut, and the Shortcut iterates the track list, searches Apple Music for each one, and adds the matches to a new playlist.

url
shortcuts://run-shortcut?name=ImportPlaylist&input=text&text={encoded_json}

The Shortcut is about fifteen actions. For each track it runs "Search Apple Music", takes the first result, and appends it to a playlist. It's not perfect, some tracks don't match because of naming differences between catalogues, some regional gaps, occasionally a live version instead of the studio cut. But once it's done, the Shortcut calls back the server with the success rate, how many tracks out of the total it actually found and added. That number gets stored alongside the playlist metadata, which I haven't yet used, but should be useful for tuning the pipeline. If a playlist only matched twelve out of twenty-five tracks, the vibe-to-query translation probably drifted too far into obscure territory. If it matched twenty-three, the queries were solid. It's a small feedback signal but it'll add up.

Now here's the part that makes the whole thing feel alive instead of static. After I've listened to a playlist, I rate it on a simple 1-5 scale. That score gets stored in a SQLite database on the home server alongside the Discogs users who contributed the releases that ended up in that playlist.

sql
CREATE TABLE user_trust (
    discogs_username TEXT PRIMARY KEY,
    trust_score REAL DEFAULT 0,
    playlists_contributed INTEGER DEFAULT 0,
    last_updated TEXT
);
 
CREATE TABLE playlist_ratings (
    id INTEGER PRIMARY KEY,
    vibe TEXT,
    rating INTEGER CHECK (rating BETWEEN 1 AND 5),
    created_at TEXT,
    releases JSON
);
typescript
const ratePlaylist = async (playlistId: number, rating: number) => {
  db.run(`UPDATE playlist_ratings SET rating = ? WHERE id = ?`, [
    rating,
    playlistId,
  ]);
 
  const releases = db
    .prepare(`SELECT releases FROM playlist_ratings WHERE id = ?`)
    .get(playlistId);
 
  for (const release of JSON.parse(releases.releases)) {
    const username = release.submitter;
    if (!username) continue;
 
    db.run(
      `INSERT INTO user_trust (discogs_username, trust_score, playlists_contributed, last_updated)
       VALUES (?, ?, 1, datetime('now'))
       ON CONFLICT(discogs_username) DO UPDATE SET
         trust_score = (trust_score * playlists_contributed + ?) / (playlists_contributed + 1),
         playlists_contributed = playlists_contributed + 1,
         last_updated = datetime('now')`,
      [username, rating, rating],
    );
  }
};

The trust score is a running weighted average that Claude vibecoded for me. If a Discogs user's submissions keep ending up in playlists I rate highly, their score climbs and if their stuff consistently lands in playlists I rate poorly, it drops. Over time I get a map of which collectors have taste that aligns with mine, without them ever knowing about it. It's parasitic, but I'm not too worried about ethics here, I'm just using the data they've already made public on the platform, and I'm not sharing it with anyone else.

The interesting part is what happens when a user's trust score gets high enough. Once someone crosses a threshold (I'm using 4.0 out of 5 for now), the pipeline starts pulling from their Discogs collection outside the original genre query. This is actually where the hidden gems live. If a user has consistently great taste in say, doom metal, there's a decent chance their post-punk picks or their ambient collection is equally good. So for high-trust users, I fetch their full collection via the Discogs API and let Claude browse it for tracks that are outside the original vibe but might still resonate.

typescript
const getHighTrustCollections = async () => {
  const trusted = db
    .prepare(
      `SELECT discogs_username FROM user_trust 
       WHERE trust_score >= 4.0 AND playlists_contributed >= 3`,
    )
    .all();
 
  const collections = [];
  for (const user of trusted) {
    const res = await fetch(
      `https://api.discogs.com/users/${user.discogs_username}/collection/folders/0/releases?per_page=50`,
      {
        headers: {
          Authorization: `Discogs token=${DISCOGS_TOKEN}`,
          "User-Agent": "VibePipeline/1.0",
        },
      },
    );
    const data = await res.json();
    collections.push({
      username: user.discogs_username,
      releases: data.releases,
    });
  }
 
  return collections;
};

This is the feature I'm most excited about. It turns the tool from a search engine into something that actually discovers music the way humans do, by trusting someone's taste in one area and then seeing what else they're into. It's how I've found some of my favourite records in real life! A friend who has impeccable taste in metal recommends a jazz album and you trust them enough to try it, and suddenly you're listening to something you'd never have found through any algorithm. I build a way to automate that instinct.

I've been testing this for the past couple of days and the results have surprised me. "Doom metal but pretty" surfaced bands I'd never heard of that clearly live in the same neighbourhood as Katatonia and Alcest but never appear in any streaming recommendation. "If Rainbow and Portishead had a band together" returned records from labels I didn't know existed. And the trust system is already starting to pay off, a handful of Discogs users keep showing up in my high rated playlists, and their collections outside the genres I've been querying are full of stuff I want to explore. One of them has this incredible ambient collection that I would never have stumbled into through any genre-based search.

The discovery quality is different from what Spotify gave me, less polished, more raw, occasionally weird or downright incorrect, but that's the character of the data. Discogs users tag things because they care about accuracy, there's no algorithmic incentive, and that's what shows up in the results.

The whole thing runs on my home server alongside everything else, accessible over Tailscale. The Discogs API is free with a personal token and generous with rate limits (sixty requests per minute for authenticated users), the Anthropic API costs a few cents per playlist, the SQLite database is a single file that barely grows, and the Apple Shortcut is the same fifteen actions it's always been in the previous iteration that relied on Spotify. Total infrastructure cost is basically zero on top of what I'm already running.

It's not a real recommendation engine, at least not yet! It doesn't learn from my listening history in the streaming sense, it doesn't know what I played yesterday. But I do collect that data as well! I haven't put it yet to use, so for now I rely on the main functionality I set out to build, which is to learn which humans have good taste, and it gets better at finding music the more I use it.

I'm still annoyed at Spotify for killing the endpoint that started all of this. But in a way, losing it pushed me toward something I like more. Algorithms are fine, I guess, but nothing beats borrowing taste from someone who owns three thousand records and has opinions about all of them.

The code is on GitHub but it's not public yet, it currently relies way too much on my personal setup, but I'll open it up once I can get a more general version to work well. For now it's mine and it's messy and I'm fine with that.