Skip to content

Genre from Spotify's artists, falling back to Last.fm - #20

Merged
revtex merged 1 commit into
mainfrom
feat/spotify-artist-genres
Aug 14, 2026
Merged

Genre from Spotify's artists, falling back to Last.fm#20
revtex merged 1 commit into
mainfrom
feat/spotify-artist-genres

Conversation

@revtex

Copy link
Copy Markdown
Owner

Implements the chain you specified: Spotify artist call (id-keyed cache) → Last.fm if configured → empty.

Why the tag was empty

Spotify has no genre on a track at any endpoint — it models genre as an attribute of the artist. Reflecting over SpotifyAPI.Web 7.4.2:

TypeGenresHas Id
FullTracknoneyes
SimpleArtist (on the track)noneyes
FullArtistList<string>yes
FullAlbumList<string>yes
SimpleAlbumnoneyes

FullAlbum.Genres exists, which is what SpotifyTrackMapper already read — and SpotifyTrackMapper.cs:59-62 already documented that Spotify stopped populating it for most of the catalogue in late 2024. So the mapper was faithfully copying an array that was always empty, and every Spotify-tagged recording shipped with a blank genre tag.

What changed

/v1/artists/{id} is the one place the data still lives. Three things that make it fit cleanly:

  • No new scope. Artist data is public, so the shipped scope list stays exactly user-read-currently-playing and SpotifyAuthOptionsTests is untouched. Worth calling out, since "add an endpoint" and "add a scope" usually travel together and here they don't.
  • No extra lookup. The artist id is already on the SimpleArtist the match guard just compared against.
  • Not deprecated, unlike the genre-seed endpoints Spotify killed in Nov 2024.

Cached per session by artist id, misses included. An album is one artist repeated — uncached, a fifteen-track album is fifteen identical requests against a rate limit shared with every other call the session makes. Caching the empty answer matters as much as the full one. Keyed by id rather than name, since two artists share a name often enough to matter. Capped at three to match LastFmTrackMapper, so a library tagged from both providers doesn't have two ideas of how long a genre tag is.

The fallback, and the one subtle bit

Artist genres describe a body of work, not a recording — a ballad by a metal band gets tagged metal. Last.fm's top tags are per track, which is the question actually being asked. So ProviderGenreFallback wraps Last.fm and is wired only when Spotify is selected and a Last.fm key already exists; a user with both configured gets Spotify's albums with Last.fm's genres instead of having to choose.

It runs over a throwaway copy of the track. This is the load-bearing detail. The thing behind the fallback is a full metadata provider whose mapper writes album, year, cover art and track number as readily as it writes genres — pointing it at the real track would mix two catalogues' idea of one release into a single file (Spotify's album with Last.fm's artwork, or a year from a different pressing). Only Genres is read back off the copy, and ProviderGenreFallbackTests.GetGenresAsync_LeavesTheRealTrackUntouched pins that.

It's a success-path step into a gap only: a provider that found nothing leaves a bare recording and one genre isn't worth a second request, and a provider that did supply genres isn't second-guessed. A fallback that throws is swallowed at Debug — everything else is already correct by then, and losing an album to a genre timeout would be the tail wagging the dog.

On reading metadata from the client

Checked, and it can't supply genre. SMTC does define a Genres field, but probed against your live Spotify session it returns count=0 while Title, Artist, AlbumTitle, AlbumArtist and TrackNumber are all populated. Reading it would be code that returns nothing.

Your broader question about preferring the client generally is not in this PR — it's a real idea with a wrinkle worth discussing first, and I've left it out rather than guess. See the conversation.

Verification

dotnet build clean, dotnet format --verify-no-changes clean, 1013 tests green (842 Core + 171 UI), 18 new:

  • 6 over artist genres: album-has-none → artist fills it; album-has-some → artist never asked; one call per artist across five tracks; empty answers cached; capped at three; no id → no request.
  • 6 over the enricher chain, including the failure isolation.
  • 7 over ProviderGenreFallback, centred on not contaminating the real track.

Worth an eye on a real recording: genre should now land on Spotify-tagged files, and if you have a Last.fm key set you'll get track-level tags where Spotify's artist has none.

🤖 Generated with Claude Code

Spotify has no genre on a track at any endpoint — it models genre as an
attribute of the artist. `FullTrack` and `SimpleArtist` carry none, and
`FullAlbum` has the field but Spotify stopped populating it for most of the
catalogue in late 2024, which `SpotifyTrackMapper` already documented. So the
mapper was faithfully copying an array that was always empty, and every
Spotify-tagged recording shipped with a blank genre tag.
`/v1/artists/{id}` is the one place the data still lives. The artist id is
already in hand from the track the match guard just compared against, so this
is one extra call and nothing to look up first. It needs **no new scope** —
artist data is public — so the shipped scope list is unchanged and
`SpotifyAuthOptionsTests` is untouched.
Cached per session by artist id, misses included. An album is one artist
repeated, so uncached a fifteen-track album is fifteen identical requests
against a rate limit shared with every other call the session makes; and an
artist Spotify has no genres for still has none on the next track. Keyed by id
rather than name, because two artists share a name often enough to matter.
Capped at three to match what Last.fm takes.
Artist genres describe a body of work rather than a recording, which is the
honest limitation and the reason for a second rung: Last.fm's top tags are per
*track*. The chain is Spotify artist genres, then Last.fm when a key is
configured, then empty.
The fallback runs over a throwaway copy of the track. That is the load-bearing
detail — the second provider is a full metadata provider whose mapper writes
album, year, cover art and track number as readily as genres, and letting it
near the real track would mix two catalogues' idea of one release into a single
file. Only `Genres` is read back. It fires on the success path and into a gap
only, and a failure is swallowed: everything else on the track is already
correct by then.
SMTC was checked and cannot help. It does define a `Genres` field, but probed
against the live Spotify client it returns count=0 while title, artist, album,
album artist and track number are all populated.
1013 tests green, 18 new.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@revtex
revtex merged commit 91ecfd4 into mainAug 14, 2026
1 check passed
@revtex
revtex deleted the feat/spotify-artist-genres branch August 14, 2026 05:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@revtex