Deck is a minimal Spotify player for the terminal on macOS and Linux. It plays music itself (you do not need the Spotify app open), and it is built around your own album shelf: a hand-picked collection of records instead of playlists and endless feeds.
The top rows show what is playing and how far along it is. Below them are your lists: Curated, your favourite genre radios and all Genres, and then the artists on your shelf. The bottom row always shows the keys you can use in the current view.
Curated is the heart of Deck. Every Monday it puts together a list of 20 albums you have not heard yet, picked for your taste. Every album comes with a short reason – why this record is on the list for you – so choosing what to play next is easy:
The reason of the selected album is shown at the bottom, and also above the track list when you open the album.
The list is based on:
- your Last.fm listening history (most played artists of all time and of the last 3 years and 12 months, and most played albums), if you use Last.fm,
- what you play in Deck (the last 30 days) and, if you write one, a few lines about
the music you like (
~/.config/deck/taste.md), - your shelf (the records you value most),
- earlier lists: what was suggested before, what you put on your shelf and what you dismissed.
Albums you already know (3 or more plays on Last.fm, or without Last.fm, played in Deck
lately), albums on your shelf and earlier suggestions never appear again. Deck learns as you go: adding an album to your shelf
tells the next list that you liked it, and d (not for me) removes the album and tells
the next list to avoid records like it.
The list is put together by Claude Code following the
instructions in curate/prompt.md, and Deck checks every suggestion
against Spotify. Setting up Curated is optional, but recommended: see
Setting up Curated. Everything else in Deck
works without it.
- Shelf: your albums, grouped by artist. Add any album with
a. - Search for artists, albums and songs.
- Queue: add tracks or whole albums; the queue plays right after the current track.
- Genre radio: endless music from a genre (lofi, synthwave, shoegaze, 60s rock…), and you can add your own genres.
- Like the playing track with
l. It goes to your Liked Songs in Spotify (♥). - Media keys and Now Playing: the Mac's ⏯ ⏮ ⏭ keys control Deck, and the playing track shows in Control Center and on the lock screen with its cover.
- Deck remembers what was playing when you quit, and
Spacecontinues from there.
- Spotify Premium. Spotify only lets Premium accounts play music in third-party players.
- macOS, or Linux (tested on Fedora Asahi Remix on Apple Silicon).
- Rust, to build Deck from source.
- For Curated (optional): Claude Code, and best a free Last.fm account that has been recording what you listen to.
Install Apple's command line tools (they include the compiler Rust needs) and Rust:
xcode-select --install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shFollow the instructions of the Rust installer, then open a new terminal window so that
cargo is found. If xcode-select says the tools are already installed, that is fine.
On Linux, install Rust and the libraries Deck builds against from your distribution instead,
for example on Fedora: sudo dnf install rust cargo gcc openssl-devel alsa-lib-devel.
Deck uses Spotify's Web API for search and likes, and Spotify requires every user of the Web API to have their own "app". It takes a couple of minutes and is free:
- Go to developer.spotify.com/dashboard and log in with your Spotify account. Accept the developer terms if asked.
- Click Create app and fill in:
- App name and App description: anything, for example "Deck".
- Redirect URIs: exactly
http://127.0.0.1:8898/login– click Add after typing it. (Use127.0.0.1, notlocalhost.) - Which API/SDKs are you planning to use? Check Web API.
- Accept the terms and click Save.
- Open the app's Settings and copy the Client ID. You do not need the client secret.
The app is in "development mode", which is all Deck needs. Spotify requires the app's owner to have Premium. Only the account that created the app can sign in to it, so create the app with the same Spotify account you play music with. (To let up to five other people use your app, add them under User Management. It is simpler for each person to create their own app.)
Note: Spotify blocks requests for many hours (about 10) after too many of them. The limit is shared by all the apps of one developer account, so a second app does not help: if you experiment with Deck's code, avoid loops that call the Web API.
Create the file ~/.config/deck/config.toml and put your Client ID in it:
mkdir -p ~/.config/deck
echo 'client_id = "PASTE-YOUR-CLIENT-ID-HERE"' > ~/.config/deck/config.tomlReplace PASTE-YOUR-CLIENT-ID-HERE with the Client ID, keeping the quotes.
Deck plays at 160 kbps (Ogg Vorbis) by default. For the best sound quality, add a line
with bitrate = 320 (the allowed values are 96, 160 and 320):
echo 'bitrate = 320' >> ~/.config/deck/config.tomlgit clone https://github.com/nightdrive-sh/deck.git
cd deck
cargo install --locked --path .The first build takes a few minutes. It installs the deck command in ~/.cargo/bin,
which the Rust installer added to your PATH.
Why --locked: it makes Cargo use exactly the library versions listed in
Cargo.lock. Deck's Spotify library (librespot 0.8) does not build with some newer
versions of its own dependencies, so without --locked the build can fail.
Keep the deck folder: Curated's weekly job is installed from there. To update Deck
later, run git pull and the same cargo install command in it. deck --help lists the
commands
and deck --version shows the installed version.
Run:
deckThe first time, your browser opens twice:
- "Sign in to Spotify" – this is for playing music. Log in and accept.
- "Sign in to the Deck Spotify app" – this is your own app from step 2, for search and likes. Spotify asks you to allow access to your library: Deck needs it to read and change your Liked Songs.
After each one the browser says "Deck is signed in", and you can close the tab. Deck
saves the sign-ins in ~/.cache/deck/, so next time it starts straight away. If signing
in fails, Deck tells you why and quits.
Your shelf is empty at first: press f, search for an album, and press a to add it.
The bottom row of every view shows the most useful keys. All of them:
| Key | What it does |
|---|---|
| ↑ ↓ | Move the selection |
| Enter | Open the selected artist, list or album. In a track list: play the album from the selected track. On a genre: start its radio. In a radio: play from the selected track |
| Space | Play / pause |
| ← → | Previous / next track |
f or Ctrl+F |
Search |
n or Ctrl+N |
Now playing: go to the album (or radio) that is playing, with the playing track selected |
l or Ctrl+L |
Like the playing track, or unlike it if it is already liked |
e or Ctrl+E |
Add to queue: the selected track in a track list or radio, the whole album in album lists |
a or Ctrl+A |
Add the album to your shelf, or remove it if it is already there. In a radio: the playing track's album. In Genres: add the genre to the home view, or remove it |
d or Ctrl+D |
Remove: an album from your shelf (in an artist opened from the shelf), a track from the queue, an album from Curated (not for me) or a genre from the home view |
r or Ctrl+R |
In a radio: start a new radio of the same genre |
u or Ctrl+Q |
Show the queue |
| Esc | Back |
q |
Quit (in the search field, use Ctrl+C) |
| Ctrl+C | Quit, in any view |
In the search field, letters type into the search, so use the Ctrl versions there. The
Ctrl versions work everywhere else too. l and n always concern the playing track, not
the selected row.
- Top row:
DECK, and on the right the queue length (+2 queued), short confirmations (+ queued: Nevermind,♥ liked: Everlong) and■ stoppedwhen paused. Errors are shown on the second row in red. - Second row: artist / album / track of the playing track, ♥ if it is liked, and the elapsed time.
- Third row: progress bar.
The home view lists the artists on your shelf in alphabetical order, ignoring a leading
"The" and accents. With a Finnish or Swedish locale (LC_ALL, LC_COLLATE or LANG
starting with fi or sv), å, ä and ö sort after z as their own letters. Enter on an
artist shows their albums on your shelf, oldest first, and Enter on an album opens its
track list without starting playback. In the
track list, Enter plays the album from the selected track to the end, and the playing
track is marked with ♪. When the album ends, playback stops.
Albums on your shelf have a green ● after them in every list.
Type to search; the search starts when you pause typing. ← → move the cursor. Results are
grouped into artists, albums and songs. ↓ or Enter moves from the search field to the
results, where the keys work as in any other list: Enter opens an artist or album, a adds
an album to your shelf and e adds it to the queue. Enter on a song plays it and its album
on from there (the album opens with the song selected), e adds the song to the queue and
a puts its album on your shelf. ↑ on the first result or f goes back to the search
field. Esc goes back from search.
The queue plays right after the current track, like in Spotify, and when it is empty the
album continues where it left off. If nothing is playing, adding to the queue starts it.
u shows the queue, where d removes a track.
A genre plays as a radio: an endless stream of tracks from different artists and albums.
Your favourite genres are on the home view under Curated, and ≋ Genres at the end lists
all genres that come with Deck (26) plus your own. Favourites have a ★ in that list.
- Enter on a genre starts its radio and opens the radio view. If the genre is already
playing, Enter just opens the view. The playing genre has
♪ playingon the home view. - In Genres,
aadds a genre to the home view or removes it. On the home view,dremoves it. - In the radio view, Enter plays from the selected track,
eadds the track to the queue,aadds the track's album to your shelf andrstarts a new radio of the same genre.
The radio uses Spotify's radio stations, starting from a typical artist or track of the genre. When a station starts repeating itself, the radio continues seamlessly from another starting point. Compilation albums go on the shelf under their album artist, usually "Various Artists".
You can add genres with the help of an AI assistant such as Claude Code. Ask it to run
deck genre prompt and to add a genre, for example "50s rock". The instructions tell it
to choose about ten typical artists or tracks and to find their Spotify links. Deck checks
each one and only saves the genre if at least three of them work.
deck genre add [--dry-run] < genre.jsonchecks the genre and prints a report. Without--dry-runit saves the genre. Run it without input to see an examplegenre.json.deck genre remove <name>removes your genre.deck genre listlists all genres. Yours have"own": true.
Your genres are in ~/.config/deck/my-genres.json, and they appear in Genres the next time
you start Deck. A genre of your own with the same name as a built-in genre replaces it,
and removing yours brings the built-in one back.
l likes the playing track: it is saved to your Liked Songs in Spotify and gets a ♥.
Pressing l again removes the like. Liked tracks have a ♥ in track lists and the radio,
too. Likes need your own Spotify app (client_id in config.toml).
The Mac's media keys control Deck in any view: ⏯ works like Space and ⏮ ⏭ like ← →. The
playing track shows in Control Center's Now Playing and on the lock screen. macOS gives
the media keys to the app that started playing most recently: if Spotify or Music plays
after Deck, the keys go to it, and they come back when Deck plays again. For this, Deck
starts a small helper process (deck now-playing) that quits with Deck.
On Linux the same helper registers Deck as the MPRIS player "Deck", so media keys,
playerctl and desktop widgets work. It also writes the playing track to
$XDG_RUNTIME_DIR/deck/state.json for panels, and takes commands from the command line:
deck ctl toggle|play|pause|next|previous|likecontrols playback.deck ctl play-album <spotify:album:…>opens an album and plays it.deck ctl play-song <spotify:track:…> <spotify:album:…>opens the song's album and plays it from the song.deck ctl search <query>opens the search with the results.deck ctl queue-album <spotify:album:…>adds an album to the end of the queue, anddeck ctl queue-song <spotify:track:…> <spotify:album:…>one song of it.deck ctl unqueue <n>removes the queue's track at position n, counting from 0, anddeck ctl queue-clearempties the queue.deck ctl shelf-add <spotify:album:…>puts an album on your shelf,deck ctl shelf-remove <spotify:album:…>takes it off.deck ctl seek <ms>jumps to that position in the playing track (paused stays paused).deck ctl radio <genre id>starts the genre's radio (the ids:deck genre list), anddeck ctl favorite-add <genre id>/favorite-remove <genre id>puts a genre on the home page or takes it off.deck ctl pick [shelf|curated](Pick for me) plays an album from your shelf, Curated or both, at random but like drawing from a hat: an album comes again only once all the others have played. The whole album goes to the front of the queue and starts; what was queued plays after it. Every album you play counts, and the history is kept in~/.local/state/deck/picked.json.deck ctl curated-reject <spotify:album:…>takes an album out of Curated, as Ctrl+D does there.deck ctl quitsaves what is playing and quits, asqdoes.deck ctl restore <file>goes back to a saved playback state: a copy of~/.local/state/deck/playback.json, by its absolute path. The album or radio, the track, the position and the queue come back as when Deck starts, paused;deck ctl playgoes on. A script can keep the file before it plays something else and put things back after.
The state file also has the playing track's and album's URIs (track_uri, album_uri), whether the
track came from the queue (queued),
the queue (queue: each track's uri, title, artist, duration_ms and album_uri), the playing
genre radio (radio: its genre id, name and tracks from the playing one on, each with uri, title,
artist, album and album_uri) and Deck's latest error (error).
Deck also keeps which albums you have listened to (~/.local/state/deck/listened.json, for a window to show): a
track counts once half of it has played, and an album is listened once half of its tracks have (listened: true).
And what you have played lately, album by album, newest first (~/.local/state/deck/history.json): an album goes in
when one of its tracks has played halfway, playing it again within a few hours only moves it up, and entries older than
30 days are dropped.
For desktop launchers, deck search --json <query> (artists, albums and songs, each song with
its album), deck artist-albums --json <artist id> and deck album-tracks --json <spotify:album:…>
(the album and its tracks) print JSON without a running Deck (they need client_id).
deck serve (Linux) plays without the screen, for a window of your own: it takes the
commands above and keeps the state file up to date, and SIGTERM, Ctrl+C or deck ctl quit
ends it, saving what was playing. Errors go to its log and to the state file's error. Only one Deck runs at a time: a
second one, in the terminal or as deck serve, says so and quits.
A desktop that has such a window can offer it as a deck-window command on your PATH: then plain deck, in a
graphical session, opens that window (which starts deck serve when no Deck plays), as an app's name does.
deck tui always opens Deck's screen in the terminal.
Deck's app icon for desktop launchers is assets/deck.svg: install it as
~/.local/share/icons/hicolor/scalable/apps/deck.svg and use Icon=deck in a .desktop file.
Nightdrive uses all of these for its top bar and
launcher.
Curated is put together once a week by Claude Code. It works best from your Last.fm listening history, and without Last.fm from what you play in Deck, your shelf and a few lines you write about your taste. You need:
- Best, a Last.fm account with your listening history. Spotify can record your listening there: in Last.fm, go to Settings → Applications and connect Spotify. The more history there is, the better the list.
- With Last.fm, a Last.fm API key: create one at last.fm/api/account/create (any application name will do; the callback URL can be left empty). Copy the API key.
- Claude Code, installed and signed in, so that the
claudecommand works in the terminal. Each weekly run uses your Claude plan or API credits like any other Claude Code session.deckandclaudemust both be found in your terminal'sPATH(check withcommand -v deck claude).
With Last.fm, add two lines to ~/.config/deck/config.toml:
lastfm_api_key = "your Last.fm API key"
lastfm_user = "your Last.fm username"Check that it works (this prints your taste profile as JSON):
deck tasteIf something is missing, the error tells you what to add.
Without Last.fm, write a few lines about the music you like in
~/.config/deck/taste.md, in your own words: artists and albums you love, eras, moods,
what you want more of. Put some favourite albums on your shelf too. deck taste shows
what the curator gets. The list gets better as you play in Deck, and you can edit the
file any time.
Run Deck at least once before this (the sign-ins from First start), then run:
deck curateIt takes a few minutes. The progress is written to the terminal and to
~/.cache/deck/curate.log. You can run it again any time to replace the list with a new
one. When it is done, the albums appear under ✦ Curated on Deck's home view. Until
then the row says not set up, and opening it shows these steps in short.
scripts/install-curate.shRun this in the deck folder. It installs a background job (a launchd agent) that runs
deck curate every Monday at 9:00. If your Mac is asleep at that time, the list is made
when it wakes up; if it is shut down, that week is skipped. Background jobs do not get
your terminal's PATH, so the installer looks up where deck and claude are and
writes those folders into the job; it stops with an error if one of them is missing. Run
scripts/install-curate.sh again if deck or claude moves (for example after
reinstalling Claude Code another way). If the job cannot find claude,
~/.cache/deck/curate.log says claude not found. To remove the job:
scripts/install-curate.sh --uninstalldeck curate runs Claude Code with the instructions in curate/prompt.md (built into
Deck). Claude can only run two Deck commands, write candidate files in
~/.cache/deck/curate/ and search the web:
deck tasteprints your taste profile: Last.fm history (including every album you have already listened to) if you use it,taste.md, what you have played in Deck lately, shelf, earlier lists and earlier candidates that were not on Spotify.deck curate submit [--dry-run]reads album candidates with reasons, finds them on Spotify and reports which ones are accepted and why others are not (not on Spotify, on your shelf, suggested before, already listened to). Without--dry-runit saves the list.
Claude picks about 24 candidates, checks them, replaces the rejected ones and saves the best 20. If the run fails, the previous list stays.
Spotify blocks apps that make too many Web API requests for many hours, which would also
stop Deck's search. So deck curate submit is careful: it makes at most 50 Spotify
searches a day, remembers searches for a day, and stops at once when Spotify says "too
many requests" (429), making no more searches until the block has passed. The 50
searches are shared with deck genre add, and the block with Deck itself: whichever of
them meets it first, the others wait too.
| Path | What is there |
|---|---|
~/.config/deck/config.toml |
Settings: client_id, bitrate, lastfm_api_key, lastfm_user (other keys are an error, so a typo does not go unnoticed) |
~/.config/deck/shelf.json |
Your shelf |
~/.config/deck/genres.json |
Your favourite genres on the home view |
~/.config/deck/my-genres.json |
Your own genres |
~/.config/deck/curated.json |
This week's Curated list |
~/.config/deck/curated-history.json |
Everything Curated has suggested |
~/.config/deck/taste.md |
Optional: a few lines about your taste, for Curated |
~/.cache/deck/ |
Sign-ins (credentials.json, webapi.json), Deck's log deck.log (the previous run's log is deck.log.1), Curated's log curate.log and the caches below |
~/.cache/deck/curate-searches.json |
Spotify searches of the last day by deck curate submit and deck genre add, the daily search count, and Curated's candidates that were not found on Spotify (kept for half a year) |
~/.cache/deck/rate-limit.json |
A possible 429 block from Spotify, shared by Deck, deck curate submit and deck genre add |
~/.cache/deck/lastfm-albums.json |
Albums you have listened to on Last.fm, fetched again after a day |
~/.cache/deck/curate/ |
The weekly Curated run's working folder: Claude's candidate files from the latest run |
~/.local/state/deck/playback.json |
What was playing when you quit |
Deck writes its files safely (a crash never leaves half a file). If one of the files in
~/.config/deck/ is broken, for example after editing it by hand, Deck shows an error
and does not overwrite it, so you can fix it. You can edit the shelf and the favourite
genres while Deck is open: Deck reads the file again before it changes it, so your
edits are kept.
-
Something went wrong: look at the log,
~/.cache/deck/deck.log, or after a crash and restart the previous run's log,~/.cache/deck/deck.log.1. For more detail, start Deck withRUST_LOG=deck=debug deck. -
Signing in does not work, or you want to sign in with another account: delete the saved sign-ins and start Deck again:
rm ~/.cache/deck/credentials.json ~/.cache/deck/webapi.json
-
The browser says the redirect URI is invalid: check that your Spotify app has exactly
http://127.0.0.1:8898/loginas a Redirect URI and thatclient_idinconfig.tomlis that app's Client ID. -
Search or likes fail with 429 / "rate limiting": Spotify has blocked your app for too many requests. It passes by itself, usually within some hours. Playback and genre radio keep working meanwhile.
-
Curated did not update: see
~/.cache/deck/curate.log. If the background job did not start at all, the error is in~/Library/Logs/deck-curate.log. -
The build fails: make sure you used
cargo install --locked --path .and thatxcode-select --installhas been run.
Deck is not an official Spotify app and is not affiliated with Spotify. It plays music with librespot, an open-source Spotify client library, and uses some of Spotify's internal interfaces (for example for the radio). Spotify may change them at any time, which can break features until Deck is updated.
Inspired by cliamp by Bjarne Øverli.

