Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spotify-cli

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.


Read this first

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.proto records 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_dc cookie 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.

Requirements

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

Install

curl -fsSL https://raw.githubusercontent.com/noahcoetsee/spotify-cli/main/install.sh | bash

Then open a new shell and check it:

spotify dj --doctor

The 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 source line to your .zshrc
Options
SPOTIFY_CLI_PREFIX=~/somewhere   # install dir
SPOTIFY_CLI_PORT=8765            # localhost sink port
SPOTIFY_CLI_YES=1                # skip the confirmation prompt

Usage

spotify 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.

In your shell prompt

RPROMPT='$(spotify_prompt_info)'      # ->  ♫  Bee Gees  0:26/3:17

spotify_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.

How it works

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.js polls the client's AuthorizationAPI every 20s and posts the token to localhost whenever it changes. It polls rather than subscribing to onTokenChanged on purpose: the event-driven version had an unrecoverable startup race and would serve hours-stale tokens. Polling self-heals within one interval.
  • token-sink.py is a tiny LaunchAgent: the only writer of token.json (mode 600), and the meeting point for the command channel.
  • dj.py reads 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.py handles status/queue/love/nope/current by posting a command to the sink and waiting; the ferry picks it up and runs it against the client's own APIs.
  • state.py owns the shape of player state, shared by the sink (which writes the now-playing cache) and ctl.py (which draws it), so the two can't drift.
  • spotify.zsh routes those subcommands and delegates everything else to shpotify, unmodified.

Your token stays on your machine. Nothing is sent anywhere except Spotify.

Troubleshooting

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

Uninstall

~/.local/share/spotify-cli/uninstall.sh          # add --purge to drop cached token/logs

It prints the one .zshrc line to delete by hand — editing someone's shell config unattended is a good way to break their login shell.

Credits

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.

About

A modified shpotify to interact with spotify dj from cli. Never need to use the spotify desktop app again.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages