Genre from Spotify's artists, falling back to Last.fm - #20
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
FullTrackSimpleArtist(on the track)FullArtistList<string>FullAlbumList<string>SimpleAlbumFullAlbum.Genresexists, which is whatSpotifyTrackMapperalready read — andSpotifyTrackMapper.cs:59-62already 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:user-read-currently-playingandSpotifyAuthOptionsTestsis untouched. Worth calling out, since "add an endpoint" and "add a scope" usually travel together and here they don't.SimpleArtistthe match guard just compared against.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
ProviderGenreFallbackwraps 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
Genresis read back off the copy, andProviderGenreFallbackTests.GetGenresAsync_LeavesTheRealTrackUntouchedpins 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
Genresfield, but probed against your live Spotify session it returnscount=0while 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 buildclean,dotnet format --verify-no-changesclean, 1013 tests green (842 Core + 171 UI), 18 new: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