Spotitube
Synchronize your Spotify collections downloading from external providers
About

Spotitube is a CLI application to authenticate to Spotify account, fetch music collections — such as account library, playlists, albums or specific tracks —, look them up on a defined set of providers (currently YouTube and Qobuz), download them and inflate the downloaded assets with metadata collected from Spotify.
Downloaded tracks are further enriched with lyrics fetched from Genius and LRCLIB, including synced LRC when available.
Usage
By default, Spotitube will synchronize user's library:
spotitube sync
In order to synchronize further set of tracks, use the dedicated flags:
spotitube sync --playlist spotitube-sync \
--album https://open.spotify.com/album/6Jx4cGhWHewTcfKDJKguBQ?si=426ac1fd0fbe4cab \
--playlist-tracks 5s9gvhZDtfTaM8VMfBtssy \
--track 6SdAztAqklk1zAmUHhU4N7 \
--fix /path/to/already/downloaded/track.mp3
As showed in the previous example, there are several ways to indicate the ID of a resource — be it a playlist, album or track:
regardless Spotitube is given a full URL to that resource (e.g. https://open.spotify.com/playlist/2wyZKlaKzPEUurb6KshAwQ?si=426ac1fd0fbe4cab), a URI (e.g. spotify:playlist:2wyZKlaKzPEUurb6KshAwQ) or an ID (e.g. 2wyZKlaKzPEUurb6KshAwQ), it should be smart enough to solve the effective ID resolution all by itself.
Furthermore, in case of playlist, automatic aliasing of personal playlist names into their ID is applied: this enables passing playlist by name instead of ID in case user wants to synchronize personal playlists.
By default, Spotitube uses XDG Base/User Directory Specification to resolve user's Music folder (which usually maps to ~/Music), but it can be obviously overridden using a dedicated flag:
spotitube sync -o ~/MyMusic
Additional sync flags worth knowing:
--library/-l— explicitly synchronize the library (auto-enabled if no collection flag is passed).--library-limit N— cap the number of library tracks fetched (0= unlimited, default).--playlist-encoding {m3u,pls}— playlist file format produced by the Mixer (defaultm3u).--plain— disable the fancy TUI; emit plain line-oriented output (useful for cron/CI).--manual/-m— prompt for a user-supplied provider URL per track instead of letting the Decider pick.
Subcommands
Beyond sync, the following subcommands are available — list them via spotitube --help:
auth— establish a Spotify session and persist the OAuth token to${XDG_CACHE_HOME:-~/.cache}/spotitube/session.json. Pass--logout/-lto wipe the cached token before re-authenticating.attach— attach Spotify metadata (including the Spotify ID embedded in a custom ID3 frame) to an existing local file.lookup— query Spotify for a resource and print its metadata without downloading.show— show the Spotify metadata embedded in a local file.reset— remove the cached session and any local state.
Authentication scopes
spotitube auth requests both read and write OAuth scopes on the user's account:
user-library-read,user-library-modifyplaylist-read-private,playlist-read-collaborativeplaylist-modify-public,playlist-modify-private
The modify scopes are requested even though sync is read-only against Spotify — they reserve the ability to push library/playlist changes from local state in the future. Approve them only if you trust your Spotify app's client ID and secret.
Docker
In order to make Spotitube work via Docker, it has to expose its dedicated port (i.e. 65535) and mount both the cache and the music directories as volumes:
docker run -it --rm \
-p 65535:65535/tcp \
-v ~/.cache:/cache \
-v ~/Music:/data \
ghcr.io/streambinder/spotitube --help
Headless
The only real friction running Spotitube headless is the OAuth redirect.
Spotify requires the callback to be either an HTTPS URL or one of the loopback literals http://127.0.0.1:PORT / http://[::1]:PORT.
Spotitube uses the loopback form (http://127.0.0.1:65535/callback), which means the browser completing the auth must be able to reach that callback on the host actually running Spotitube.
The simplest way to bridge a desktop browser to a headless server is an SSH local port forward — no DNS, no extra DNAT, no TLS termination:
ssh -L 65535:127.0.0.1:65535 user@server
# on the server, in the forwarded session:
spotitube auth
Then open the URL Spotitube prints in your local browser.
Spotify will redirect to http://127.0.0.1:65535/callback, the tunnel hands the request through to the server, and the session token is persisted server-side at ${XDG_CACHE_HOME:-~/.cache}/spotitube/session.json.
After auth returns, the tunnel and SSH session can be closed; subsequent spotitube invocations on the server reuse the cached token.
Manual mode
It might very well happen that Spotitube is either not able to find a track asset on given providers (e.g. YouTube) or that it chooses the wrong one.
In such cases, it is possible to manually choose and pass the right asset to Spotitube, using the --manual flag:
spotitube sync --manual --track 6SdAztAqklk1zAmUHh
Spotitube will patiently wait for the user to pass the URL of the track asset to download. This can come in useful in cases where the track has been already downloaded wrong and user wants to touch on it:
spotitube sync --manual --fix /path/to/already/downloaded/track.mp3
Installation
Official releases
Binaries released officially include all the needed tokens and keys to make Spotitube work at its best (e.g. Spotify app ID and key, or Genius token).
To install, head to Spotitube Releases page (binaries published for {linux,darwin,windows} × {amd64,arm64}), or pull via Docker:
docker pull ghcr.io/streambinder/spotitube:latest
Heads up: the published Docker image is built for
linux/arm64only. Onamd64or other architectures, build the image locally from theDockerfileshipped at the repository root, or grab the matching native binary from the Releases page.
Custom build
Spotitube's been written to be as much vanilla Go as possible, so all the traditional Go build/install methods are supported:
go install github.com/streambinder/spotitube@latest
Or:
git clone https://github.com/streambinder/spotitube.git
cd spotitube
go build
go install
Runtime prerequisites
Outside of Docker, Spotitube shells out to a couple of binaries and expects them on PATH:
ffmpeg— used by the Processor to normalize volume and re-mux downloaded audio.yt-dlp— used by the YouTube provider to fetch the chosen result.
Install them via your package manager (e.g. apt install ffmpeg yt-dlp, brew install ffmpeg yt-dlp, dnf install ffmpeg yt-dlp). The published Docker image bundles both already.
Embedding API keys
By default, Spotitube will use SPOTIFY_ID, SPOTIFY_KEY and GENIUS_TOKEN environment variables to authenticate to the corresponding APIs.
If those are not found, though, it will fall back to the fallback fields defined in the corresponding source code modules (which, in turn, are empty, by default).
In order to build a binary which contains these fields, the following formula can be used:
go build -ldflags="
-X github.com/streambinder/spotitube/spotify.fallbackSpotifyID='awesomeSpotifyID'
-X github.com/streambinder/spotitube/spotify.fallbackSpotifyKey='awesomeSpotifyKey'
-X github.com/streambinder/spotitube/lyrics.fallbackGeniusToken='awesomeGeniusToken'
"
Design
Spotitube is made of a pool of routines which carry out their job independently and in parallel. Each single one of these routines, will possibly receive a work mandate from a fellow routine, process that work unit and pass the ball.
It's an assembly line, where every single step has a very constrained work to do and a dedicated queue for items to accomplish that work for. Such queues usually carry a specific track (be it part of synchronization of user's library, of an album, a playlist, or a single track), but sometimes they only represent a semaphore or other types such as playlists.
The assembly line is made of the following routines:
Indexer
Scans the music folder in order to parse all the assets that have been synchronized using Spotitube. It is achieved by reading a specific custom ID3 metadata field corresponding to the Spotify track ID (which, in turn, is stuck into the MP3 file at processing time).
This is done to ensure that tracks collisions are properly handled and that already downloaded songs are skipped.
Authenticator
Self-explainatory: handles Spotify authentication.
Fetcher
Once Indexer and Authenticator succeed, they signal their status to the Fetcher, using a semaphor-like queue (of length of one and of boolean type).
The fetcher, then, goes through any given arg (be it the library, a playlist, an album, or a single track) and handles the fetching of data for each track composing the given collection, all from Spotify APIs.
That data is then parsed into a custom Track object which is passed to the Decider queue.
Decider
For each Track passed over by the Fetcher, it queries every provider defined (currently YouTube and Qobuz), looking for a result that best matches the given track data.
Collector
This component is split in three parts:
- Downloader: downloads the result which the Decider picked for the given track.
- Composer: queries every lyrics provider defined (currently Genius and LRCLIB) and — if found — downloads it, preferring synced LRC over plain text when available.
- Painter: downloads the artwork from the URL which was given by Spotify APIs.
Processor
The Processor applies further customization to the asset, such as rebalancing the volume of the track file (via ffmpeg's volumedetect) or encoding all the metadata collected as ID3 (MP3) metadata.
Installer
Moves the file into its final location.
Mixer
For each playlist passed for synchronization, bundles it into an M3U (default) or PLS file (selectable via --playlist-encoding) containing every track of the playlist that has been successfully installed.