spopy
A production-quality Spotify CLI in a single Python file. Runs anywhere with uv — no install step, no virtualenv, no package manager.
Designed for both local use and self-hosting on a remote gateway (Dokku, VPS, etc.).
Features
- Single file — one script, zero config files, inline dependencies via PEP 723
- Two auth modes — local browser login or headless gateway bootstrap (paste-back flow)
- Persistent tokens — authenticate once, use forever (auto-refresh)
- Full playback control — play, pause, seek, volume, shuffle, repeat, queue
- Search and browse — tracks, albums, artists, playlists
- Playlist management — create, add, remove, reorder, clear, replace
- Library operations — save, unsave, check, list saved tracks/albums
- Discovery — top tracks/artists, recently played, genre/mood search
- Three output modes — rich (human), plain (pipes), JSON (machines)
- Honest API handling — unsupported endpoints are reported clearly, never faked
- Safe for servers — never leaks tokens, no secrets in logs or output
Install
PyPI (recommended)
uv tool install spopy
Or with pipx:
pipx install spopy
Script installer
curl -fsSL https://raw.githubusercontent.com/amitkot/spopy/main/install.sh | bash
Standalone script
curl -fsSL https://raw.githubusercontent.com/amitkot/spopy/main/spopy.py -o ~/.local/bin/spopy && chmod +x ~/.local/bin/spopy
The standalone script uses PEP 723 inline deps and runs with uv run spopy.py.
Quick Start
No setup required — spopy includes a built-in Spotify app.
spopy auth login
spopy status
spopy play "bohemian rhapsody"
Remote / headless server
spopy auth url
# Open the printed URL in a browser on another machine
# After approving, copy the command from the callback page
spopy auth callback-url '<paste_the_url>'
Transfer token from local to remote
# On local machine
spopy auth login
spopy auth export-token-info --raw --yes > token.json
# Copy token.json to remote, then:
spopy auth import-token-info token.json
Configuration
All configuration is via environment variables. None are required — spopy works out of the box with PKCE auth.
Auth (all optional)
| Variable | Default | Description |
|---|---|---|
SPOTIFY_CLIENT_ID |
(built-in) | Override with your own Spotify app |
SPOTIFY_CLIENT_SECRET |
Set to switch to classic OAuth flow | |
SPOTIFY_REDIRECT_URI |
(auto) | Override redirect URI |
Setting
SPOTIFY_CLIENT_SECRETswitches spopy to the classic OAuth flow with your own Spotify app. Seespopy auth setup-guidefor details.
Runtime (all optional)
| Variable | Default | Description |
|---|---|---|
SPOTIFY_CACHE_PATH |
~/.config/spopy/token_cache |
Token cache file path |
SPOTIFY_USERNAME |
Spotify username (for multi-user) | |
SPOTIFY_SCOPES |
(sensible defaults) | Override OAuth scopes |
SPOTIFY_DEFAULT_DEVICE_ID |
Fallback device ID | |
SPOTIFY_DEFAULT_DEVICE_NAME |
Fallback device name | |
SPOTIFY_MARKET |
ISO country code for market | |
SPOTIFY_OUTPUT |
rich |
Default output: rich, plain, json |
SPOTIFY_TIMEOUT_SECONDS |
15 |
API request timeout |
SPOTIFY_RETRIES |
3 |
Max retry attempts |
SPOTIFY_BACKOFF_FACTOR |
0.5 |
Exponential backoff factor |
SPOTIFY_DEBUG |
0 |
Enable debug logging (1) |
SPOTIFY_OPEN_BROWSER |
1 |
Allow browser opening (0 to disable) |
SPOTIFY_NO_COLOR |
0 |
Disable color output (1) |
SPOTIFY_STATE_FILE |
Path to persist auth state |
Commands
Global flags
--json JSON output
--plain Plain text output (pipe-friendly)
--debug Debug logging
--market CC Spotify market (ISO country code)
--device-id ID Target device ID
--device-name N Target device name
--limit N Result limit
--offset N Result offset
--yes Skip confirmations
--exact Prefer exact name matches
--interactive Interactive selection from results
--version Show version
Auth
| Command | Description |
|---|---|
auth setup-guide |
First-time setup instructions |
auth status |
Show auth config and token status |
auth url |
Print authorization URL (for gateway flow) |
auth login |
Interactive login (local browser) |
auth callback-url <url> |
Exchange redirect URL for tokens |
auth code <code> |
Exchange raw auth code for tokens |
auth import-token-info <path> |
Import token JSON (- for stdin) |
auth export-token-info |
Export token JSON (--raw for real tokens) |
auth whoami |
Show current user |
auth logout |
Remove token cache |
Playback
| Command | Description |
|---|---|
play [query] |
Play or resume. Search query, URI, URL, or ID |
pause |
Pause playback |
resume |
Resume playback |
stop |
Stop (alias for pause — no true stop API) |
next |
Skip to next track |
previous |
Skip to previous |
seek <pos> |
Seek: 1:30, +10s, -15s, or milliseconds |
volume <0-100> |
Set volume |
repeat <off|track|context> |
Set repeat mode |
shuffle <on|off|toggle> |
Set shuffle |
Search
| Command | Description |
|---|---|
search <query> |
Search (--type track,album,artist,playlist) |
Devices
| Command | Description |
|---|---|
devices list |
List available devices |
devices transfer <device> |
Transfer playback (name or ID) |
Track
| Command | Description |
|---|---|
track show <query> |
Track details |
track play <query> |
Play a track |
track queue <query> |
Add to queue |
track save <query> |
Save to library |
track unsave <query> |
Remove from library |
track check <query> |
Check if saved |
track open <query> |
Print Spotify URL |
track audio <query> |
Audio features (restricted API) |
Album
| Command | Description |
|---|---|
album show <query> |
Album details |
album play <query> |
Play album |
album tracks <query> |
List album tracks |
album save <query> |
Save to library |
album unsave <query> |
Remove from library |
album check <query> |
Check if saved |
Artist
| Command | Description |
|---|---|
artist show <query> |
Artist details |
artist top <query> |
Top tracks (search-based) |
artist albums <query> |
List albums |
artist follow <query> |
Follow artist |
artist unfollow <query> |
Unfollow artist |
artist related <query> |
Related artists (restricted API) |
Playlist
| Command | Description |
|---|---|
playlist list |
Your playlists (--all for full list) |
playlist show <pl> |
Playlist details |
playlist create <name> |
Create (--description, --public/--private) |
playlist rename <pl> <name> |
Rename |
playlist describe <pl> <desc> |
Set description |
playlist set-public <pl> |
Make public |
playlist set-private <pl> |
Make private |
playlist follow <pl> |
Follow playlist |
playlist unfollow <pl> |
Unfollow playlist |
playlist items <pl> |
List items |
playlist add <pl> <tracks...> |
Add tracks |
playlist remove <pl> <tracks...> |
Remove tracks |
playlist clear <pl> |
Remove all items |
playlist reorder <pl> |
Reorder (--from, --to, --length) |
playlist replace <pl> <tracks...> |
Replace all items |
Library
| Command | Description |
|---|---|
library tracks |
Saved tracks |
library albums |
Saved albums |
library save <query> |
Save item |
library unsave <query> |
Remove item |
library check <query> |
Check if saved |
Queue
| Command | Description |
|---|---|
queue list |
Current queue |
queue add <query> |
Add to queue |
queue clear |
Not supported (no API) |
queue remove |
Not supported (no API) |
Discovery
| Command | Description |
|---|---|
status |
Current playback summary |
current |
Detailed now-playing info |
recent |
Recently played tracks |
top tracks |
Your top tracks (--time-range) |
top artists |
Your top artists (--time-range) |
discover |
Discovery suggestions from your history |
radio <query> |
Build a queue from a seed (search heuristic) |
genre list |
Well-known genres |
genre search <genre> |
Search by genre |
mood search <mood> |
Search by mood (heuristic) |
doctor |
Diagnose auth, config, devices, connectivity |
Output Modes
Rich (default)
$ spopy status
╭─ Bohemian Rhapsody — Queen (A Night at the Opera) ───╮
│ Playing 2:15 / 5:55 │
│ Device: MacBook Pro | Volume: 65% | Shuffle: off │
╰──────────────────────────────────────────────────────────╯
JSON
$ spopy --json status
{
"ok": true,
"command": "status",
"data": {
"name": "Bohemian Rhapsody",
"artists": "Queen",
"playing": true,
"progress": "2:15",
"duration": "5:55",
"device": "MacBook Pro"
}
}
Plain
$ spopy --plain status
Bohemian Rhapsody Queen A Night at the Opera playing 2:15/5:55 MacBook Pro
AI Agent Skill
A Claude Code skill is included for AI agents.
To install it, copy the skills/ directory into your project or Claude Code config:
# Project-level
cp -r skills/ /path/to/your/project/.claude/skills/
# Or user-level
cp -r skills/ ~/.claude/skills/
The skill teaches the agent how to invoke the CLI, parse JSON output, and handle errors.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Invalid user input |
| 3 | Auth/config error |
| 4 | Spotify API error (Premium required, no device, 403, 404) |
| 5 | Rate limit exhausted |
| 10 | Internal error |
Device Selection
For playback commands, devices are selected in this order:
--device-idflag--device-nameflag- Currently active Spotify device
SPOTIFY_DEFAULT_DEVICE_IDenv varSPOTIFY_DEFAULT_DEVICE_NAMEenv var- Error with helpful message
Known Limitations
These are Spotify API limitations, not CLI bugs:
- Queue clear/remove — no API endpoint exists
- Recommendations — removed from Spotify API (Nov 2024)
- Audio features — restricted to apps with extended API access
- Related artists — restricted to apps with extended API access
- Artist top tracks — removed from API (Feb 2026); CLI uses search fallback
- Search limit — max 10 results per type (Spotify API limit)
- Volume control — some devices (phones, smart speakers) don't support remote volume
- Premium required — all playback control requires Spotify Premium
License
MIT License. See LICENSE.
Metadata
Release files for spopy 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| spopy-0.3.1.tar.gz | 31.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| spopy-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.2 kB
Release files / spopy-0.3.1.tar.gz
| Download URL | spopy-0.3.1.tar.gz |
|---|---|
| Size | 31.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
da48ead240434209e5125f32b2aa65f6998b1d96420b5bdef16354486e7852c6
|
|
BLAKE2b-256 checksum How to use checksums |
d00d55a857d9057f4cc75e3481eca153524c31f4be98882e99ee34ec0725a411
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spopy-0.3.1-py3-none-any.whl
| Download URL | spopy-0.3.1-py3-none-any.whl |
|---|---|
| Size | 31.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a28c528b06aee42d55a0978ffecb2309ae1b71eeb907429b2f100324a914b324
|
|
BLAKE2b-256 checksum How to use checksums |
b12bda3b93a298b5f151861dac156c70c91741ba372d36836f61fc4b277b4994
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|