[GH-ISSUE #287] feat: add support for DJ mode #173

Open
opened 2026-02-28 14:25:59 +03:00 by kerem · 4 comments
Owner

Originally created by @hkcryos on GitHub (Jan 28, 2026).
Original GitHub issue: https://github.com/devgianlu/go-librespot/issues/287

Spotify's AI DJ mode uses a special context type (lexicon_set_type: your_dj) that doesn't work with the standard context resolution. The current implementation fails because:

  1. DJ tracks have an empty uri field in ContextTrack - the actual URI is in metadata["canonical_track_uri"]
  2. The context URL points to a session-specific lexicon endpoint that returns 404 from other sessions
  3. No transfer commands are sent for track advancement in DJ mode

I've got this working locally and am enjoying it now. My implementation does it like this:

  • Adding ContextTrackUri() helper that falls back to canonical_track_uri metadata
  • Creating a single-track context from TransferState.Playback.CurrentTrack for DJ transfers
  • Caching NextTracks from ClusterUpdate messages (the service broadcasts the full queue before transfer)
  • Using the cached tracks to advance playback when a DJ track ends
  • Skipping autoplay resolution for DJ contexts (that gives a 404)

I haven't looked at the TTS DJ-speak between tracks (I don't like that!) or the cross-fading between tracks (again, I dislike it, so didn't implement it).

Happy to commit a branch if there's interest in supporting this.

Originally created by @hkcryos on GitHub (Jan 28, 2026). Original GitHub issue: https://github.com/devgianlu/go-librespot/issues/287 Spotify's AI DJ mode uses a special context type (`lexicon_set_type: your_dj`) that doesn't work with the standard context resolution. The current implementation fails because: 1. DJ tracks have an empty `uri` field in `ContextTrack` - the actual URI is in `metadata["canonical_track_uri"]` 2. The context URL points to a session-specific lexicon endpoint that returns 404 from other sessions 3. No transfer commands are sent for track advancement in DJ mode I've got this working locally and am enjoying it now. My implementation does it like this: - Adding `ContextTrackUri()` helper that falls back to `canonical_track_uri` metadata - Creating a single-track context from `TransferState.Playback.CurrentTrack` for DJ transfers - Caching `NextTracks` from `ClusterUpdate` messages (the service broadcasts the full queue before transfer) - Using the cached tracks to advance playback when a DJ track ends - Skipping autoplay resolution for DJ contexts (that gives a 404) I haven't looked at the TTS DJ-speak between tracks (I don't like that!) or the cross-fading between tracks (again, I dislike it, so didn't implement it). Happy to commit a branch if there's interest in supporting this.
Author
Owner

@fegauthier commented on GitHub (Jan 28, 2026):

Would be great to support that!

Thanks!

<!-- gh-comment-id:3811648600 --> @fegauthier commented on GitHub (Jan 28, 2026): Would be great to support that! Thanks!
Author
Owner

@devgianlu commented on GitHub (Jan 29, 2026):

Hi @hkcryos and thank you for the interest in the project! Unfortunately DJ X is not available in my country so I woudn't be able to test and merge your code straightaway without the help of someone else.

Additionally, getting the tracks from ClusterUpdate messages doesn't seem like the best strategy, the client should be able to pull the track list from somewhere if it's not in the context transfer already.

From the related librespot issue I can see that lexicon_context_url is provided in the context metadata. The URL starts with hm://, but that usually means that it can be transformed in a HTTP API by stripping the prefix. We already do that here.

If you think you can get this working I think it'll be a pretty nice addition, even without TTS or crossfade.

<!-- gh-comment-id:3817800034 --> @devgianlu commented on GitHub (Jan 29, 2026): Hi @hkcryos and thank you for the interest in the project! Unfortunately DJ X is not available in my country so I woudn't be able to test and merge your code straightaway without the help of someone else. Additionally, getting the tracks from `ClusterUpdate` messages doesn't seem like the best strategy, the client should be able to pull the track list from somewhere if it's not in the context transfer already. From the [related librespot issue](https://github.com/librespot-org/librespot/issues/1604) I can see that `lexicon_context_url` is provided in the context metadata. The URL starts with `hm://`, but that usually means that it can be transformed in a HTTP API by stripping the prefix. We already do that [here](https://github.com/devgianlu/go-librespot/blob/1642ca147db2d238131c4225bd84a0b71f4e0a2a/spclient/context_resolver.go#L100). If you think you can get this working I think it'll be a pretty nice addition, even without TTS or crossfade.
Author
Owner

@hkcryos commented on GitHub (Jan 30, 2026):

Thanks @devgianlu - as you say, that's really unfortunate.

I entirely agree about lexicon_context_url. I should've said - I tried that approach initially but when you follow it, you get a 404.

Looking at the behaviour from the web player (which seems to be the same for the Windows client):

The app never calls the lexicon URL - it appears in Pathfinder response metadata but no HTTP request is ever made to it
Pathfinder returns 0 tracks for the DJ playlist (content.items: [], totalCount: 0)
Track list comes from dealer WebSocket - ClusterUpdate messages contain NextTracks with the full queue
The ClusterUpdate caching approach appears to be how the apps work too, which feels odd to me given we have the lexicon - but that is what I saw.

Happy to dig further if it would help? Or we could see if others could confirm it isn't just me?

<!-- gh-comment-id:3823555821 --> @hkcryos commented on GitHub (Jan 30, 2026): Thanks @devgianlu - as you say, that's really unfortunate. I entirely agree about lexicon_context_url. I should've said - I tried that approach initially but when you follow it, you get a 404. Looking at the behaviour from the web player (which seems to be the same for the Windows client): The app never calls the lexicon URL - it appears in Pathfinder response metadata but no HTTP request is ever made to it Pathfinder returns 0 tracks for the DJ playlist (content.items: [], totalCount: 0) Track list comes from dealer WebSocket - ClusterUpdate messages contain NextTracks with the full queue The ClusterUpdate caching approach appears to be how the apps work too, which feels odd to me given we have the lexicon - but that is what I saw. Happy to dig further if it would help? Or we could see if others could confirm it isn't just me?
Author
Owner

@devgianlu commented on GitHub (Feb 1, 2026):

@hkcryos I would try seeing what the desktop client does, you can intercept HTTPS requests with something like mitmproxy.

<!-- gh-comment-id:3831365515 --> @devgianlu commented on GitHub (Feb 1, 2026): @hkcryos I would try seeing what the desktop client does, you can intercept HTTPS requests with something like [mitmproxy](https://www.mitmproxy.org/).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/go-librespot#173
No description provided.