spotuify is a Spotify player you drive from your terminal: a keyboard-native TUI and a fully scriptable CLI for the same thing. Search, play, queue, switch devices, build playlists, read synced lyrics, see album art, all without leaving the shell. It also ships an MCP server so a coding agent can run your music the way you do, but that's a bonus. The TUI and CLI are the point.
Run spotuify and you're in. If you're not logged in yet, it walks you through it: log in through the browser, land in a synced UI. No Spotify app to register, no config files to hand-edit.
Fair question. spotify-player, ncspot, and the original spotify-tui already proved a terminal Spotify client is worth living in, and spotuify builds on what they shipped: embedded librespot for playback, a keyboard TUI, local library search. It pushes hard in one direction the others treat as a side feature. The CLI is the product, not a wrapper around the TUI.
- Pipeable everywhere, not just on one command. Every read, list, status, and search surface speaks
--format json,jsonl,csv, orids.spotuify search "lo-fi beats" --type playlist --format idsreturns bare URIs you can pipe into anything, in any language. Other terminal clients give you JSON on a command or two, or an interactive UI you can't script at all. - Your agents can run it. Because the CLI is the contract, an LLM controls Spotify through ordinary commands (or the built-in MCP server). Writes are preview-first:
--dry-runshows what would change,--yescommits, andspotuify ops undoreverses the last one. A back button for your library, which you want the moment an agent is the one clicking. - The music keeps playing after you close the window. A background daemon owns playback, queue, and devices; the TUI, CLI, and agents are all just views of it. Quit the TUI and the song keeps going. Run a command from another shell and it shows up instantly.
- Search runs off a local cache. A SQLite store plus a rebuildable index answer library and search queries from disk, so navigation is instant and an agent gets the same results twice.
Want the most polished desktop experience? Use the official app. Want Spotify as something you can type at, pipe, script, and hand to an agent? That's this.
- One-step first-run login. No Spotify Developer app to register; just log in through the browser.
- Browser login with the refresh token stored in the platform's native credential vault under service
spotuify. - Owned config file at
~/.config/spotuify/spotuify.toml. - Config commands for
path,init,get, andset. - Embedded librespot registers spotuify as a Spotify Connect device at daemon start.
- Playback controls: play, pause, next, previous, seek, volume, shuffle, repeat.
- Search across tracks, albums, playlists, and podcast episodes.
- Queue viewing and add-to-queue support.
- Playlist browsing and quick add-current-to-playlist flow.
- Device list and Spotify Connect transfer.
- Cover art rendering through Kitty, iTerm2, Sixel, or half-block fallback.
- Fully keyboard navigable with vim-style movement, pane switching, help overlay, paging, and back navigation.
- Local analytics:
listen_factsplusspotuify analytics top/habits/rediscoveryfor Wrapped-style insights, with shell-hook recipes for ListenBrainz, Last.fm, and Discord. - Operation log + undo: mutating commands are recorded;
spotuify ops undo --dry-runpreviews andspotuify ops undo --yesapplies reversible undo. MCP exposesundo_lastas a safety net for agent runs. - MCP server over stdio or loopback HTTP for agents.
- Audio visualization through embedded sink taps or loopback capture.
- A Spotify Premium account (required for librespot streaming).
- A terminal with good image support for best visuals. Kitty works well; other terminals fall back through
ratatui-imagesupport.
Prebuilt binaries ship for macOS (Apple Silicon and Intel) and Linux x86_64 on each GitHub Release; the Homebrew tap is the quickest path. Other targets (Windows, Linux musl or arm, other distros) build from source with cargo install or Nix. Pick your platform:
brew tap planetaryescape/spotuify
brew install spotuify
spotuify daemon install-service # registers a launchd LaunchAgent
spotuify # first run kicks off onboardingTo update an existing Homebrew install:
brew update
brew upgrade spotuifyRelease archives include SHA256 checksums and GitHub artifact provenance attestations. macOS binaries are not notarized today, so Gatekeeper may still block the first launch:
xattr -d com.apple.quarantine /opt/homebrew/bin/spotuifyInstall the latest prebuilt archive with checksum verification:
curl -fsSLO https://raw.githubusercontent.com/planetaryescape/spotuify/main/install.sh
bash install.sh
spotuifyGrab the linux-x86_64 tarball from Releases and put spotuify on your PATH:
tar xzf spotuify-v*-linux-x86_64.tar.gz
install -Dm755 spotuify ~/.local/bin/spotuify
spotuify daemon install-service # registers a systemd --user unit
spotuifySpotuify uses Secret Service (GNOME Keyring / KWallet) for credential storage. Headless encrypted-file credential fallback is planned but not exposed as a stable login flag yet.
cargo install --git https://github.com/planetaryescape/spotuify --locked spotuify
spotuify daemon install-serviceNo prebuilt Windows binary yet. Build from source (the Windows paths exist, including Credential Manager storage):
cargo install --git https://github.com/planetaryescape/spotuify --locked spotuify
spotuify daemon install-service # registers a Task Scheduler logon triggerDaemon-mode media-key handling on Windows is currently limited: SMTC requires a foreground window handle, so background-only operation cannot register media keys. Workaround: keep the TUI process alive.
nix run github:planetaryescape/spotuify
# or in a flake:
inputs.spotuify.url = "github:planetaryescape/spotuify";Plain first-time brew install spotuify only works after brew tap planetaryescape/spotuify. Without that tap, Homebrew searches homebrew/core, where spotuify is not published.
From this repository:
cargo build
./target/debug/spotuify --helpInstall into your Cargo bin path:
cargo install --path .
spotuify --helpIf ~/.cargo/bin is not on your PATH, either add it or run ./target/release/spotuify directly from the repo.
For platform-specific embedded librespot builds, pick the right audio backend feature flag:
# Linux (alsa, routes through pipewire-alsa shim on modern distros):
cargo install --git https://github.com/planetaryescape/spotuify --locked \
--features 'embedded-playback,system-integrations,loopback-cpal,alsa-backend'
# Linux musl (pure-Rust rodio backend):
cargo install --git https://github.com/planetaryescape/spotuify --locked \
--no-default-features \
--features 'embedded-playback,system-integrations,loopback-cpal,rodio-backend'
# macOS (PortAudio bridge to CoreAudio):
cargo install --git https://github.com/planetaryescape/spotuify --locked \
--features 'embedded-playback,system-integrations,loopback-cpal,portaudio-backend'
# Windows (rodio writes to WASAPI):
cargo install --git https://github.com/planetaryescape/spotuify --locked \
--features 'embedded-playback,system-integrations,loopback-cpal,rodio-backend'Releases are managed by Release Please and GitHub Actions.
The flow:
- Merge normal feature/fix PRs into
mainusing conventional commit messages such asfix: improve diagnosticsorfeat: add playlist view. - Release Please opens or updates a release PR with the next version,
CHANGELOG.md,Cargo.toml, andCargo.lockupdates. - Merge the Release Please PR.
- Release Please creates the GitHub release and tag.
- The release workflow builds macOS arm64 and Intel binaries, uploads them to the GitHub release, generates a Homebrew formula, and updates the tap.
Required GitHub setup:
Secret: HOMEBREW_TAP_TOKEN
HOMEBREW_TAP_TOKEN must be a token with write access to the Homebrew tap repository:
planetaryescape/homebrew-spotuify
The tap repo should contain a Formula/ directory. The release workflow will create or update Formula/spotuify.rb.
Manual release rebuild:
GitHub Actions -> Release -> Run workflow -> tag v0.1.0
Run the app:
spotuifyThat is the low-friction path. If config or OAuth are missing, spotuify starts setup automatically, syncs Spotify data, then opens the TUI.
You can also run setup intentionally:
spotuify onboardThe onboarding flow does this in order:
- Creates
~/.config/spotuify/spotuify.tomlif it does not exist. - Opens your browser to log in to Spotify.
- Stores the resulting refresh token in the system keychain.
- The daemon mints a full-access Web API token from your session and starts syncing.
There is no Client ID or Client Secret to enter. spotuify uses Spotify's first-party login (the same mechanism librespot, spotify-player, and ncspot use), so playback and library writes (creating playlists, saving tracks) work without registering a Developer app. Premium is required for playback.
After setup succeeds, plain spotuify opens the TUI directly on later runs:
spotuifyPlain spotuify and spotuify onboard open the browser for you. To rerun the login by hand:
spotuify loginThat opens Spotify in your browser, and once you approve, spotuify stores the refresh token in the system keychain. Nothing else to configure.
Most people should skip this. If you specifically want spotuify to authenticate with your own Spotify Developer app instead of the first-party login, set SPOTUIFY_CLIENT_ID (and create the app at https://developer.spotify.com/dashboard with redirect URI http://127.0.0.1:8888/callback):
export SPOTUIFY_CLIENT_ID=your-app-client-id
spotuify loginNote: apps in Spotify's Development Mode cannot create playlists or save tracks (Spotify returns 403). That is exactly why the default login does not use one.
Default config path:
~/.config/spotuify/spotuify.toml
Default config template:
# spotuify config
# Nothing to set here to get started: run `spotuify` and log in via the
# browser. Set client_id only if you want to use your own Spotify app
# (see "Use your own Spotify app").
# client_id = ""
# redirect_uri = "http://127.0.0.1:8888/callback"
[player]
backend = "embedded"
bitrate = 320
[notifications]
enabled = false
summary = "{track}"
body = "{artist} - {album}"
on_track_change = true
on_pause = false
on_resume = false
on_skip = false
on_error = trueSupported environment overrides:
SPOTUIFY_CONFIG=/path/to/spotuify.toml
# Optional: use your own Spotify app instead of the first-party login.
SPOTUIFY_CLIENT_ID=...
SPOTUIFY_CLIENT_SECRET=...
SPOTUIFY_REDIRECT_URI=http://127.0.0.1:8888/callbackConfig commands:
spotuify config path
spotuify config init
spotuify config get client_id
spotuify config get redirect_uri
spotuify config get player.backend
# Prints <redacted> unless you pass --reveal-secret.
spotuify config get client_secret
spotuify config set client_id "..."
spotuify config set client_secret "..."
spotuify config set redirect_uri "http://127.0.0.1:8888/callback"
spotuify config set player.device_name "spotuify"
spotuify config set notifications.enabled trueValid config keys:
client_id
client_secret
redirect_uri
player.backend
player.bitrate
player.device_name
player.normalization
player.audio_cache_mib
player.pulse_props
player.event_hook # legacy alias for analytics.hook_command
analytics.hook_command
analytics.hook_timeout_ms
cache.cover_cache_mb
cache.cover_cache_ttl_days
notifications.enabled
notifications.summary
notifications.body
notifications.on_track_change
notifications.on_pause
notifications.on_resume
notifications.on_skip
notifications.on_error
spotuify
spotuify onboard
spotuify login
spotuify logout
spotuify doctor
spotuify logs path
spotuify logs tail
spotuify config path
spotuify config init
spotuify config get <key>
spotuify config set <key> <value>
spotuify status --format json
spotuify devices
spotuify search "query" --type track --source local
spotuify play "query"
spotuify play-uri spotify:track:...
spotuify pause
spotuify resume
spotuify next
spotuify previous
spotuify seek +30s
spotuify volume 50
spotuify shuffle on
spotuify repeat track
spotuify queue
spotuify queue add spotify:track:...
spotuify playlists
spotuify playlist plan "brief" --format json
spotuify playlist create "Name" --from candidates.jsonl --dry-run
spotuify library tracks
spotuify analytics top --kind tracks --format json
spotuify lyrics show --track spotify:track:...
spotuify viz enable
spotuify hooks test
spotuify mpris status
spotuify sync search-cache --prune --older-than 7d
spotuify cache status
spotuify cache reset --confirm
spotuify ops log
spotuify ops undo --dry-run
spotuify reload
spotuify reconnect
spotuify bug-report --include-logs 200
spotuify generate completions zsh
spotuify mcpCommand behavior:
spotuifyopens the TUI. If config or OAuth are missing, it starts setup first, syncs, then opens the TUI.spotuify onboardruns the same setup flow intentionally: a browser login, then the first sync.spotuify loginopens the browser to log in and stores the refresh token in the keychain.spotuify logoutremoves the Keychain token.spotuify doctorchecks config, token status, API access timings, visible devices, recent playback, queue, playlists, logs, cache version, lyrics, MCP, and player backend state.spotuify logs pathprints the log file path.spotuify logs tail --follow --format jsonstreams structured log lines.spotuify config ...reads or writesspotuify.tomlwithout requiring valid Spotify credentials.- One-shot commands talk to the daemon over local IPC. Use
--no-daemon-startwhen scripts should fail instead of starting the daemon.
Global keys:
? open or close help
q quit
1 home
2 search
3 library
4 playlists
5 queue
6 devices
7 diagnostics
8 lyrics
Tab next pane
Shift-Tab previous pane
j / Down move down
k / Up move up
gg jump to top
G jump to bottom
Ctrl-d / PageDown page down
Ctrl-u / PageUp page up
Enter activate selected item
Esc / b back from playlist tracks
u refresh Spotify data
Playback keys:
Space play or pause
Space on idle Home, play the selected saved item
n next
p previous
Left seek backward 15 seconds
Right seek forward 15 seconds
+ / = volume up
- volume down
s toggle shuffle
r cycle repeat mode
l save current track or episode
Search and library keys:
/ focus search input
Enter search when input is focused
Enter play selected home, search, library, or playlist item
e add selected item to queue
A or a add current track or episode to selected playlist
Playlist keys:
Enter open selected playlist
Enter play selected playlist track
a add current item to selected playlist
Esc / b return from playlist tracks to playlist list
Device keys:
Enter transfer playback to selected device
x transfer playback to selected device
spotuify registers itself as a Spotify Connect device through an in-process librespot session at daemon start — no separate subprocess. Premium account required (librespot streaming constraint).
Prefer a specific device name visible to other Spotify clients:
spotuify config set player.device_name "spotuify"Audio cache (disk), bitrate, normalization, and PulseAudio property hints are all on the [player] config section.
After you log in, the daemon mints a Web API token from your session and pulls your Spotify state into the local cache:
- Current playback state.
- Visible Spotify Connect devices.
- Queue state.
- User playlists.
Run spotuify doctor any time to confirm auth, daemon, device visibility, and Spotify API access are working.
spotuify exposes its daemon as a Model Context Protocol server so LLM clients (Claude Code, Cursor, Continue, agent harnesses) can use it as a tool.
# Claude Code
claude mcp add spotuify --command spotuify --args mcp
# Cursor: add to .cursor/mcp.json
{
"mcpServers": {
"spotuify": { "command": "spotuify", "args": ["mcp"] }
}
}
# Continue: add to ~/.continue/config.json under mcpServers
"spotuify": { "command": "spotuify", "args": ["mcp"] }Tools exposed:
- Read:
search,now_playing,devices_list,queue_show,playlists_list,playlist_tracks,library_list - Transport:
play,play_uri,pause,resume,next,previous,seek,volume,shuffle,repeat - Destructive (require
confirm: true):queue_add,transfer_device,playlist_create,playlist_add,playlist_remove,library_save,library_unsave - Lyrics:
lyrics - Analytics:
analytics_top,analytics_habits,analytics_search,analytics_rediscovery - Ops:
ops_log,undo_last(undo_lastis the safety net)
Resources:
spotuify://playback— current playback statespotuify://devices— visible Spotify Connect devicesspotuify://playlists— user playlistsspotuify://doctor— latest health-check report
Destructive tools called without confirm: true return a preview the LLM can show to the user. The LLM is expected to relay it and ask before retrying with confirm: true. Patterns adopted from spotify-player commit #966.
| If you want... | spotuify chooses... |
|---|---|
| A terminal-first controller for scripts and agents | CLI and MCP surfaces first; the TUI is another client |
| Playback that keeps running after the UI exits | Daemon-backed control through local IPC |
| A local library/search runtime | SQLite cache plus rebuildable search index |
| Maximum desktop integration polish today | Use an official Spotify client or a desktop-first app instead |
| The smallest possible binary with no daemon | spotuify is not optimizing for that trade-off |
Spotify's public Web API exposes queue viewing and add-to-queue, but not queue remove or queue reorder. spotuify shows the queue honestly and does not pretend those unsupported actions exist.
Playback control requires Spotify Premium. Without Premium, auth can succeed but playback API calls may fail.
Run the doctor first:
spotuify doctorThen check the log file:
spotuify logs path
spotuify logs tailOn macOS, logs are written to:
~/Library/Logs/spotuify/spotuify.log
If the TUI looks frozen, press Ctrl+C. The event loop listens for OS Ctrl+C directly. Regular Spotify refresh runs in a background task, so slow Spotify endpoints should not block keyboard input.
Generic Spotify request failure:
error: Spotify request failed
Run:
spotuify doctor
spotuify logs taildoctor prints each API section with timing and the full endpoint/status error, for example playback, devices, queue, playlists, and recently played.
Spotify API timeouts:
operation timed out
Check whether the default route to Spotify is broken:
curl -I --max-time 8 https://api.spotify.com/v1
curl -4 -I --max-time 8 https://api.spotify.com/v1If the first command times out and the -4 command returns 401, IPv6 routing to Spotify is broken on your network. spotuify forces Spotify API calls over IPv4 to avoid that path.
Redirect URI mismatch:
redirect_uri: Not matching configuration
Fix:
spotuify config get redirect_uriMake sure the exact value is listed in Spotify Dashboard -> App -> Settings -> Redirect URIs. Use 127.0.0.1, not localhost, and avoid trailing slashes.
Not logged in:
not logged in; run `spotuify login`
Fix:
spotuify loginThat opens the browser login. Plain spotuify does the same automatically on first run. (If you set SPOTUIFY_CLIENT_ID to use your own app and see a 403 on playlist writes, that app is in Development Mode; unset SPOTUIFY_CLIENT_ID to use the first-party login instead.)
No token or expired token:
spotuifyThat opens the browser login automatically when no token is stored. Use spotuify login if you only want to re-run the login without the full setup flow.
No devices visible:
spotuify doctorThen check that at least one Spotify Connect device is active. The embedded librespot device registers at daemon start; check spotuify daemon status for the player-ready event. Premium account required.
Art does not render:
Use Kitty or another terminal supported by ratatui-image. The app falls back to block rendering when terminal image protocols are unavailable.
Visualizer shows no bars on macOS loopback:
macOS does not expose system-output loopback devices by default. Use the embedded playback backend for sink-tap visualization, or install BlackHole/Loopback Audio and select it as the loopback input source. Without a virtual loopback device, spotuify may fall back to the microphone input so the visualizer stays quiet while music plays.
Windows daemon media keys:
Windows media-key integration needs a UI/window handle. The headless daemon still handles CLI, TUI, and MCP playback commands, but global Windows media keys may require a foreground TUI session until the hidden-window driver is complete.
Embedded librespot fails to register:
spotuify doctor
spotuify daemon statusThe daemon panics at startup when librespot can't bind an audio backend. Rebuild with one of --features alsa-backend / pipewire-backend / rodio-backend / portaudio-backend for your platform.
- The login refresh token is stored in macOS Keychain under service
spotuify. The Web API access token is minted on demand and never written to disk. spotuify logoutremoves the stored token from Keychain.- To re-authenticate, run
spotuify loginagain. spotuify auth bearerandspotuify config get client_secretrequire--reveal-secretbefore printing secrets.- Config files are written with mode
0600on Unix. If you use your own Spotify app (SPOTUIFY_CLIENT_ID), preferSPOTUIFY_CLIENT_SECRETwhen you do not want the client secret written to disk.
Common checks:
cargo fmt --check
scripts/cargo-test -p spotuify-spotify --tests
cargo clippy -p spotuify-spotify --all-targets -- -D warningsUse package-scoped checks while iterating. Full workspace clippy/tests and release builds are release gates, not the default edit/verify loop. To smoke an already-built binary with the fake provider:
scripts/smoke.shTo have smoke build the release binary first:
SPOTUIFY_SMOKE_BUILD=1 scripts/smoke.shRun locally:
cargo run
cargo run -- onboard
cargo run -- doctorsrc/main.rs unified binary entrypoint and legacy adapters
crates/spotuify-protocol daemon IPC protocol and shared wire types
crates/spotuify-daemon daemon state, handlers, diagnostics, sync coordination
crates/spotuify-cli reusable CLI argument/output helpers
crates/spotuify-tui Ratatui app state, actions, and rendering
crates/spotuify-spotify Spotify Web API, OAuth, config, rate limits
crates/spotuify-player embedded librespot backend (sole supported runtime)
crates/spotuify-store SQLite cache, migrations, operation log
crates/spotuify-search local search index
crates/spotuify-mcp MCP stdio/HTTP server
crates/spotuify-lyrics Spotify/LRCLIB lyrics providers
crates/spotuify-system cover cache, notifications, media-control helpers
crates/spotuify-audio audio analyzer and visualization sources
