Spotitube

Synchronize your Spotify collections downloading from external providers

About

demo

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:

Subcommands

Beyond sync, the following subcommands are available — list them via spotitube --help:

Authentication scopes

spotitube auth requests both read and write OAuth scopes on the user's account:

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/arm64 only. On amd64 or other architectures, build the image locally from the Dockerfile shipped 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:

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:

design

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:

  1. Downloader: downloads the result which the Decider picked for the given track.
  2. Composer: queries every lyrics provider defined (currently Genius and LRCLIB) and — if found — downloads it, preferring synced LRC over plain text when available.
  3. 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.