Control the Spotify desktop app from your terminal — including talking to the AI DJ, which the desktop client has no UI for.
$ spotify dj "something more upbeat"
dj: something more upbeat
(4.5s)
done — the DJ should respond shortly.It also keeps every command from shpotify
(play, pause, next, vol, status, share, …), so it's a drop-in replacement for
the spotify function in oh-my-zsh's macos plugin.
Commands never steal focus, raise a window, or send keystrokes — you can fire one mid-sentence in your terminal and keep typing.
This is unofficial. Spotify has no public API for the DJ. spotify dj talks to an
undocumented, unversioned internal endpoint (…interactionmanager.v0…) that was
reconstructed by reverse-engineering the Android client. Consequences:
- It can break with any Spotify release, without warning. Nothing here is a stable
contract.
docs/dj-interaction-manager.protorecords how it was derived so it can be re-derived. - It cuts against Spotify's Terms of Service, which is worth knowing regardless of how carefully you use it.
- It never circumvents an anti-automation control. The
sp_dccookie route is deliberately not implemented, because the only way through it is defeating a TOTP check that exists specifically to block non-browser clients. This uses the token your own logged-in desktop client already holds. - Please keep usage human-paced. Don't script it in a loop. One person prompting their own DJ is indistinguishable from using the app; automated volume against an internal endpoint is a different thing entirely, and would spoil it for everyone.
Use at your own risk.
| macOS | drives the desktop app via AppleScript |
| Spotify Premium | the DJ is Premium-only |
| DJ available in your market | still not everywhere |
| Spotify desktop, direct download | the Mac App Store build can't be patched — get it from spotify.com/download |
| Spicetify | installed for you if Homebrew is present |
| Python 3 | stdlib only, no packages |
curl -fsSL https://raw.githubusercontent.com/noahcoetsee/spotify-cli/main/install.sh | bashThen open a new shell and check it:
spotify dj --doctorThe installer is idempotent — re-run it any time. It needs no sudo. It will:
- install to
~/.local/share/spotify-cli - add one Spicetify extension and apply it (this restarts Spotify; reversible with
spicetify restore) - install a LaunchAgent for the token sink
- append one
sourceline to your.zshrc
Options
SPOTIFY_CLI_PREFIX=~/somewhere # install dir
SPOTIFY_CLI_PORT=8765 # localhost sink port
SPOTIFY_CLI_YES=1 # skip the confirmation promptspotify dj # change it up
spotify dj "play something warm and soulful"
spotify dj "less talking, more music"
spotify dj --doctor # check every link in the chain
spotify recent # the last 10 tracks, newest first
spotify recent 25 # ...or however many
spotify queue teenage fanclub # add to the queue, leaving playback alone
spotify queue # show what's queued up
spotify love # save what's playing
spotify love --undo # ...take it back out of your library
spotify nope # skip it, and stop it coming back
spotify nope --undo # ...lift that ban
spotify current # what's playing
spotify status # now playing, drawn
spotify status artist # ...or album, or track
spotify status --json # machine-readable
spotify status --format '%artist - %track'
spotify play take five # find a song and play it — without raising Spotify
spotify play album in rainbows # ...or artist, or track
spotify play list lo-fi beats # a playlist, picked from the top 10 matches
spotify play uri spotify:track:0eGsygTp906u18L0Oimnem
spotify play|pause|next|prev # ...and everything else shpotify does
spotify vol up$ spotify status
┌──────────────────────────────────────────────────────────────┐
│ ► You Rock My World ✓ │
│ Michael Jackson ▪ Invincible │
│ │
│ ▮▮▮▮▮▮▮▮▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯ 0:40 / 5:39 │
│ shuffle off ▪ repeat off │
└──────────────────────────────────────────────────────────────┘status replaces shpotify's, which shelled out to osascript six times per
invocation. One round trip to the client instead: ~60ms against ~740ms, and no
chance of the app taking focus. status artist|album|track still answer exactly as
before.
Format tokens: %track %artist %album %uri %state %icon %position %duration %saved %banned %context.
You don't need the DJ playing first — prompting works from any playback state.
Searching needs no credentials. play and queue search inside the Spotify client
itself, so there is no Spotify developer app to register and no CLIENT_ID/CLIENT_SECRET
to set up. If ~/.shpotify.cfg does happen to hold credentials, they're used as a fallback
when the client can't be reached — otherwise nothing is needed.
recent reads the client's own play history
nope always skips, and additionally bans the track within whatever you're currently
playing — a playlist, album, or DJ session. It does not touch your library; a word
that short shouldn't be able to delete a saved track. When there's no real context to ban
in (spotify play <song> leaves a single track as the context) it skips and says so
rather than reporting a ban that would do nothing.
RPROMPT='$(spotify_prompt_info)' # -> ♫ Bee Gees 0:26/3:17spotify_prompt_info ships with spotify.zsh. It reads a warm cache with pure zsh
builtins — no fork, no round trip, about 0.04ms — so it's safe on a prompt that
redraws per keystroke, and it repaints itself on a timer while the shell sits idle.
Powerlevel10k needs a custom segment rather than RPROMPT, and its instant prompt
needs a one-line stub. See docs/prompt.md for both, plus the
tuning knobs and the JSON feed for a custom format.
The awkward part is auth. The DJ endpoint needs a first-party bearer token, and the only place to get one is the running desktop client. But a Spicetify extension is browser JS: it can't write files, and it can't call the endpoint itself (spclient rejects the CORS preflight). So the token has to be ferried out to something native.
token out
Spotify client ──► token-ferry.js ──► token-sink.py ──► token.json
(Spicetify ext) polls 20s (LaunchAgent) │
▼
dj.py ──► spclient.wg.spotify.com
commands in
ctl.py ──► token-sink.py ◄── token-ferry.js ──► Spicetify.Platform.*
POST /cmd long-polls /cmd (inside the client)
token-ferry.jspolls the client'sAuthorizationAPIevery 20s and posts the token to localhost whenever it changes. It polls rather than subscribing toonTokenChangedon purpose: the event-driven version had an unrecoverable startup race and would serve hours-stale tokens. Polling self-heals within one interval.token-sink.pyis a tiny LaunchAgent: the only writer oftoken.json(mode 600), and the meeting point for the command channel.dj.pyreads the token, reads this machine's device id straight out of Spotify's own cache (PersistentCache/device_settings), encodes a three-field protobuf, and POSTs it.ctl.pyhandlesstatus/queue/love/nope/currentby posting a command to the sink and waiting; the ferry picks it up and runs it against the client's own APIs.state.pyowns the shape of player state, shared by the sink (which writes the now-playing cache) andctl.py(which draws it), so the two can't drift.spotify.zshroutes those subcommands and delegates everything else to shpotify, unmodified.
Your token stays on your machine. Nothing is sent anywhere except Spotify.
Start with spotify dj --doctor; it names the broken link rather than just failing.
| Symptom | Cause |
|---|---|
no usable token |
Spotify isn't running, or the ferry hasn't posted yet — give it ~20s after launch |
| token stays stale | extension not loaded: spicetify config extensions should list token-ferry.js |
token sink isn't running |
launchctl load ~/Library/LaunchAgents/com.spotify-cli.token-sink.plist; see ~/.config/spotify-cli/sink.err |
rate limited (429) |
back off; it won't retry for you, by design |
| succeeds but nothing happens | wrong device id — the endpoint returns success for any well-formed one. Check --doctor, and that the device you're listening on is the active one |
~/.local/share/spotify-cli/uninstall.sh # add --purge to drop cached token/logsIt prints the one .zshrc line to delete by hand — editing someone's shell config
unattended is a good way to break their login shell.
src/spotify.sh is shpotify by Harish Narayanan
and contributors, MIT licensed, vendored unmodified. The DJ support and the packaging
around it are this project's. See LICENSE.