alembic logo

# alembic A self-hosted control panel that turns your Spotify playlists into a real, well-tagged music library on your own server, downloaded automatically, kept up to date, and free of duplicate junk. No terminal required for day-to-day use. Everything, adding playlists, fixing a mislabeled song, cleaning up duplicates, happens through a web page. --- ## Table of contents - [What alembic does](#what-alembic-does) - [What you need before you start](#what-you-need-before-you-start) - [Quickstart](#quickstart) 1. [Get the image](#1-get-the-image) 2. [Create your folders](#2-create-your-folders) 3. [Generate a master key](#3-generate-a-master-key) 4. [Set up login (OIDC)](#4-set-up-login-oidc) 5. [Write your docker-compose file](#5-write-your-docker-compose-file) 6. [Start it up](#6-start-it-up) 7. [Log in](#7-log-in) - [Setting up your credentials](#setting-up-your-credentials) - [Adding your first playlist](#adding-your-first-playlist) - [Using alembic day to day](#using-alembic-day-to-day) - [Updating alembic](#updating-alembic) - [Troubleshooting](#troubleshooting) --- ## What alembic does Give alembic a Spotify playlist link, and from then on it will: - **Download every track** in the playlist automatically, over Soulseek - **Tag each file properly** (artist, title, album, genre) using Spotify's own metadata, so your library looks clean instead of a pile of "Track 01.mp3" files - **Import everything into your music library**, ready to show up in Navidrome or any other music server that reads a normal folder-based library - **Rebuild the playlist as a real M3U file**, so it plays back in the right order in your music app - **Keep checking on a schedule**, so when you add new songs to the Spotify playlist, they show up in your library on their own - **Show you exactly what's downloaded and what's still waiting**, side by side with the original Spotify tracklist, per playlist - **Find duplicate copies of songs** (same track downloaded twice, a different format, or the same recording under different tags) and let you review and clean them up, nothing gets deleted without your say-so - **Let you fix things by hand** when needed: correct a tag, re-tag a track from a URL, manually import a file you dropped in yourself, or delete something you don't want It also keeps a dashboard showing what's in your library, what got downloaded recently, the history of every automated job it's run, and a quick check that your traffic is actually going through a VPN if you've set one up. Everything runs in one Docker container. There's no separate database to install and no cron jobs to hand-edit, alembic manages all of that internally. ## What you need before you start alembic is built to slot into a home server that already has a few things running. It is not a starting point for a music server from scratch, it's the automation layer on top of one. Before you begin, make sure you have: 1. **A server that runs Docker**, with `docker compose` available. Any Linux box, NAS, or mini PC works. 2. **A music server that reads a plain folder of tagged files**, such as [Navidrome](https://www.navidrome.org/). alembic builds your library on disk; Navidrome (or similar) is what you actually listen with. 3. **A Soulseek account.** This is how tracks get downloaded. Sign up free at [soulseek.com](https://www.soulseek.com/). 4. **A Spotify Developer app, plus an OAuth login (not just keys).** alembic reads your playlists' track lists from Spotify (it does not download from Spotify itself, only Soulseek). Create an app free at [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) to get a Client ID and Secret — but for any app created since Spotify's 2025/2026 API change, those two values alone (plain "client-credentials" auth) are no longer enough to read playlists. You additionally need to complete a one-time **OAuth login** — alembic's **Connect Spotify** button, which redirects you to Spotify to log in and grants alembic a user token — the first time you set things up. Plan on doing both steps; see the credentials section below for exactly how. 5. **A login provider that speaks OpenID Connect (OIDC).** alembic doesn't have its own username/password login, it delegates to something you already trust. [Pocket ID](https://github.com/pocket-id/pocket-id) is a small self-hosted option built exactly for this, but Authentik, Keycloak, Authelia, or any other OIDC provider works too. Optional, add these later if you want them: - A **Bandcamp** account, if you want to also sync your Bandcamp purchases into the library automatically. - **AzuraCast** or **Qobuz** credentials, if you use either of those. - A **Telegram bot**, if you want job notifications sent to you. - A **VPN container** (such as gluetun), if you want Soulseek's network traffic routed through a VPN. Not required to get started. ## Quickstart ### 1. Get the image Pull the prebuilt image onto your Docker host: ```bash docker pull git.kretzer.club/andrew/alembic:0.6.9 ``` That is the whole install. You do not need to download the source or build anything. The `0.6.9` is the version; you can pin to it so nothing changes under you, or use `latest` to always get the newest. (If you would rather build it yourself from source, you can, but you do not need to.) ### 2. Create your folders alembic needs three places to store things: - **A music folder**, where your actual songs live. If you already have a music library, point at it directly. - **A config folder**, where alembic keeps its own database, your saved credentials (encrypted), and logs. - **A secrets folder**, which holds just one file: the master key described in the next step. This is kept separate from the config folder on purpose (see below). ```bash mkdir -p /path/to/your/music mkdir -p /path/to/alembic-config mkdir -p /path/to/alembic-secrets ``` alembic runs inside the container as a non-root user (user and group id 1000). If you created these folders as yourself or as root, the container will not be able to write to them and will stop on startup with a clear message telling you this. Give that user ownership of the config and music folders: ```bash sudo chown -R 1000:1000 /path/to/alembic-config sudo chown -R 1000:1000 /path/to/your/music ``` Everything about how alembic runs (which playlists you follow, your credentials, scheduling) is stored inside the config folder, so moving alembic to a new machine is a matter of copying it across. Back up the secrets folder separately, and do not keep it in the same backup as the config folder. The reason is in the next step. ### 3. Generate a master key alembic encrypts every credential you give it (Spotify keys, Soulseek password, and so on) before storing it in the config folder. It needs a key to do that: ```bash openssl rand -base64 32 > /path/to/alembic-secrets/master.key chmod 600 /path/to/alembic-secrets/master.key ``` Keep this file safe. If you lose it, alembic can no longer read your saved credentials and you'll need to re-enter them. Keep it separate from your config folder, which is why it goes in its own folder above. The config folder holds the encrypted credentials. If the key sat in there too, then anyone who got hold of one backup would have both the locked box and its key, and the encryption would not protect you. Storing the key on its own keeps the two apart. ### 4. Set up login (OIDC) Register alembic as an application in your OIDC provider (Pocket ID, Authentik, whichever you use). You'll need to tell it: - **Redirect URI:** `https:///auth/callback` - **Scopes:** `openid profile email` In return, it will give you three values you'll need in the next step: - An **issuer URL** (something like `https://id.example.com`) - A **client ID** - A **client secret** ### 5. Write your docker-compose file Create a file called `docker-compose.yml` on your server (put it wherever you keep your other compose files): ```yaml services: alembic: image: git.kretzer.club/andrew/alembic:0.6.9 container_name: alembic ports: - "8420:8420" environment: - POCKETID_ISSUER=https://id.example.com - POCKETID_CLIENT_ID=your-client-id - POCKETID_CLIENT_SECRET=your-client-secret - SESSION_SECRET=some-long-random-string # Optional: lock login to a single email, recommended for a personal server - ALLOWED_EMAIL=you@example.com volumes: - /path/to/your/music:/data/music - /path/to/alembic-config:/config secrets: - alembic_master_key restart: unless-stopped secrets: alembic_master_key: file: /path/to/alembic-secrets/master.key ``` A few notes on the environment values: - `SESSION_SECRET` signs your login session. You can leave it out and alembic will generate one on first start and save it inside the config folder for next time. If you would rather set it yourself, use any long random string, for example from `openssl rand -base64 32`. - `ALLOWED_EMAIL` is optional but recommended. Even if your OIDC provider only has your account today, this makes sure only that one email can ever log in. - If you want to route Soulseek traffic through a VPN container instead of exposing alembic's port directly, see the comments in `docker-compose.snippet.yml` in this repo for the `network_mode: "service:"` pattern. Most people can skip this and use the simple port mapping above. ### 6. Start it up ```bash docker compose up -d ``` On first boot, alembic seeds a few default config files into your config folder and sets up its internal database automatically. Check that it started cleanly: ```bash docker compose logs -f alembic ``` You should see `Uvicorn running on http://0.0.0.0:8420` with no errors. ### 7. Log in Open `http://:8420` in a browser. You'll be redirected to your OIDC provider to log in, then dropped onto the alembic dashboard. ## Setting up your credentials Once logged in, go to **Settings → Credentials** and fill in each service you plan to use. Nothing here is required all at once, add them as you need them. Values are write-only: once saved, they're encrypted and never shown back to you in the browser (you'll just see a "Configured" badge). Each service also shows a one-line description right on the page of what it's actually used for. Spotify, Soulseek, and Navidrome are marked **Required** — every playlist sync depends on them, so there's no way to turn them off. Bandcamp, AzuraCast, Qobuz, and Telegram are optional and each has an on/off switch: turning one off pauses whatever it drives (its scheduled sync, or just not being used for buy-link lookups) without deleting the credential you already saved. | Service | What to enter | Where to get it | |---|---|---| | **Spotify** *(required)* | Client ID, Client Secret, then **Connect Spotify** (OAuth login) | Reads your playlists' track lists. Create an app at [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) for the Client ID/Secret, save them, then click **Connect Spotify** to complete an OAuth login with your Spotify account — required for any app created after Spotify's 2025/2026 API change, since keys alone (client-credentials) are no longer sufficient (see Troubleshooting below). | | **Soulseek** *(required)* | Username, Password | Your normal Soulseek login. | | **Navidrome** *(required)* | Base URL, admin username, admin password | The address of your Navidrome server and an admin account on it. Used so alembic can trigger a library rescan after downloads. | | **Bandcamp** *(optional)* | Username, format preference, cookies | Export your Bandcamp cookies while logged into bandcamp.com in your browser, using an extension like "Get cookies.txt LOCALLY", and paste the contents in. Format preference is a space-separated list like `flac mp3-320`. | | **Qobuz** *(optional)* | Token, App ID, region | A logged-in Qobuz session token — used (alongside Bandcamp) as a source for "buy this" links stamped onto tracks you didn't purchase there. | | **AzuraCast** *(optional)* | Base URL, API key | Only relevant if you also run an AzuraCast radio station off the same library. It's not a buy-link source itself (that's Bandcamp/Qobuz, written straight to the file); this just lets alembic tell AzuraCast to immediately pick up a tag change instead of waiting for its own periodic scan. Base URL is your AzuraCast instance's address, e.g. `https://radio.example.com`. | | **Telegram** *(optional)* | Bot token, chat ID | Create a bot via [@BotFather](https://t.me/BotFather) if you want the daily status digest sent to a chat. | ### Connecting your Spotify account (OAuth) — required for any new Spotify app Spotify Client ID/Secret alone ("client-credentials" auth) used to be enough for alembic to read your playlists. Since Spotify's 2025/2026 API change, that stopped working for any app created after the change — Spotify now requires a real user to authorize the app via OAuth before it will hand over playlist data at all. If your Spotify app is that new (almost certainly, if you just created one in the step above), you must complete this OAuth login or every playlist sync will fail with a 403/401 (see Troubleshooting). Steps: 1. In your Spotify app's dashboard (Developer Dashboard → your app → Settings → Redirect URIs), add this redirect URI: `https:///connect/spotify/callback`. 2. In alembic, go to **Settings → Credentials**, enter your Spotify Client ID and Secret, and save. 3. Click **Connect Spotify**. You'll be redirected to Spotify's own login page to authorize alembic, then bounced back to the Credentials page, which now shows **Connected**. That's it, done once. alembic stores the resulting OAuth refresh token (encrypted, same as your other credentials) and uses it to silently mint a fresh access token whenever it needs one — you never have to repeat this unless you disconnect or revoke access on Spotify's side. One more limit that comes with a new Spotify app: Spotify only returns a playlist's contents to the account that owns it (or collaborates on it), even after you connect. So sync playlists that belong to the account you connect, and if someone shares a playlist with you, save your own copy of it first (or have them add you as a collaborator). If you happen to be running a Spotify app created *before* the 2025/2026 change, plain Client ID/Secret still works and this step is optional — but connecting is harmless either way, so there's no reason not to. > **If you connected Spotify before version 0.6.1:** click **Connect Spotify** again. Versions before 0.6.1 requested a narrower OAuth scope than playlist reads actually need, which could get you a token that refreshes fine but then gets 403 Forbidden loading a playlist (see Troubleshooting). Reconnecting grants the correct scope; your old connection doesn't upgrade itself. ## Adding your first playlist 1. Go to **Playlists** → **Add playlist**. 2. Paste the Spotify playlist URL and give it a name (this name becomes the folder/tag name in your library). 3. Pick a daily sync time. 4. Save. That's it. On its next scheduled run (or immediately, using the **Run now** button on the playlist's page), alembic will download every track in the playlist, tag it, import it into your library, and build a matching `.m3u8` playlist file. ## Using alembic day to day - **Dashboard** — a snapshot of your library: track counts, formats, what got downloaded in the last 24 hours, recent job activity, and a public-IP check (handy if you route alembic's traffic through a VPN container and want to confirm it's actually taking effect). - **Playlists** — add, pause, or remove synced Spotify playlists. Each playlist's page shows the original Spotify tracklist side by side with a "Downloaded" / "Waiting to download" breakdown, so you can see exactly what's made it into your library and what hasn't. - **Library** — browse and search everything in your library. Fix tags on a track, re-tag it from a URL, or delete it entirely. - **Import** — drag a file onto the dropzone (or click to browse) to add it to your import folder, then import it individually or all at once, with an optional playlist tag. - **Dedup** — click "Scan now" to check for duplicates both by filename/tags and by matching the actual audio (catches the same recording filed under different names). Nothing is ever deleted automatically: you approve each deletion, or mark a pair as "keep both" if it's not actually a duplicate. - **Genres** — preview what a genre refresh from Spotify would change, then apply it, or lock an artist's genre so it's never overwritten again. - **Settings** — three sub-pages: **Credentials** (see above), **Jobs** (every scheduled task, enable/disable each one, run it on demand, and see the history and logs of every past run), and **Artist Casing** (a list of canonical artist name spellings, e.g. "Mall Grab" not "MALL GRAB", so the same artist doesn't end up split across differently-cased folders). ## Updating alembic When a new version is released, change the version in your `docker-compose.yml` (for example `:0.6` to the new number), then pull and restart: ```bash docker compose pull docker compose up -d ``` If you used the `latest` tag instead of a version number, you can skip the edit and just run those two commands. Your library, credentials, and settings all live in your mounted folders, so updating the image never touches your data. ## Backups Everything worth keeping is in the folders you already created: - **The config folder** holds the app database, the beets library database, your rendered settings, and your encrypted credentials. Backing this up captures your playlists, schedules, and history. - **The secrets folder** holds `master.key`. Back this up too, but keep it in a separate backup from the config folder. The config folder holds your credentials in encrypted form, and the key is what unlocks them. Keeping the two apart means one leaked backup is not enough to read your credentials. - **The music folder** is your library itself. Back it up however you already back up files. To restore on a new machine: put the config and secrets folders back where they were, point your compose file at them, and start the container. If you ever lose `master.key`, alembic can still start, but it can no longer read your saved credentials, so you would re-enter them once under Settings then Credentials. ## Advanced settings Most people never touch these. They are environment variables you can add to your compose file's `environment:` block. - `TZ` sets the timezone used for scheduling and for times shown in the app, for example `TZ=America/Toronto`. Defaults to UTC. This is the standard Docker timezone variable, so if you already set it for other containers, alembic follows it too. - `ALLOWED_EMAIL` locks login to a single email address (recommended for a personal server). - `MUSIC_DATA_DIR`, `ALEMBIC_CONFIG_DIR`, `ALEMBIC_PORT`, and `ENCRYPTION_KEY_FILE` let you change the in-container paths and port. The defaults match everything in this README, so you only need these for unusual setups. You can confirm your setup at any time by visiting `/setup` in your browser (no login needed), which lists what is configured and what is missing. There is also a `/health` endpoint that returns the same information as JSON for uptime monitors. ## Migrating from the older script version If you used the earlier version of this project, when it was a set of scripts rather than this container, there is a short guide for bringing your old playlists and credentials across in [docs/MIGRATION.md](docs/MIGRATION.md). ## Troubleshooting **Something isn't working and I'm not sure what.** Open `/setup` in your browser. It checks the things a fresh install commonly gets wrong (login not configured, master key missing, config folder not writable) and tells you exactly what to fix. You do not need to be logged in to see it. **The container won't start / exits immediately.** Check the logs with `docker compose logs alembic`. The most common cause is folder ownership: the container runs as user id 1000, so the config and music folders must be owned by it. Run `sudo chown -R 1000:1000 ` on each, as described in step 2. **I can't log in / get redirected in a loop.** Double check the redirect URI registered with your OIDC provider exactly matches `https:///auth/callback`, including `http` vs `https`. **A playlist says it's synced but nothing downloaded.** Check that playlist's job history under **Settings → Jobs**, the log will usually show whether Soulseek couldn't find a track, or a credential is missing. The playlist's own page will also show everything as "Waiting to download" if nothing came through. **The playlist page shows "Spotify denied access (403)" or a 401, or nothing downloads and the logs mention one of those.** Your Spotify app can't read the playlist with app-only (client-credentials) access alone — you haven't completed the OAuth login yet. Spotify closed off key-only access for apps created after its 2025/2026 API change, on both the old `/tracks` endpoint (403) and the current `/items` endpoint (401 "valid user authentication required") — this affects even public playlists. The "extended quota mode" that would lift it is granted only to organizations, not individuals, since May 2025. The fix: go to **Settings → Credentials** and click **Connect Spotify** to complete the OAuth login (see "Connecting your Spotify account (OAuth)" above). This mints a user token that works regardless of when your app was created. If you haven't already, add the redirect URI `https:///connect/spotify/callback` in your Spotify app's dashboard first, or the connect step itself will fail with a redirect_uri mismatch. **A playlist's job log shows `Spotify returned 403 loading this playlist`, `Spotify returned this playlist without its contents`, or `0 visible tracks` even though you've connected Spotify.** (On versions before 0.6.2 these showed up as `sldl crashed (exit 134)` mentioning a 403 Forbidden; on 0.6.2 a playlist you don't own could finish "successfully" while downloading nothing.) Possible causes, in order of likelihood: 1. **The playlist isn't owned by the connected account.** For Spotify apps created after the 2025/2026 API change, Spotify only returns a playlist's contents to its owner or a collaborator on it. Being public is NOT enough. Fix: recreate the playlist under the account you connected via **Connect Spotify**, or have the owner make it collaborative and add you. 2. **You connected before version 0.6.1.** Earlier versions requested a narrower OAuth scope than playlist reads actually need, so the token refreshes fine but then gets 403 loading a playlist regardless of ownership. Fix: go to **Settings → Credentials** and click **Connect Spotify** again to grant the correct scope. (If your Spotify app is old enough to be grandfathered, both public playlists and your own keep working like before.) Either way, re-run the playlist after fixing it. **Dedup found something that isn't actually a duplicate.** Use the "Keep both" button on that pair. alembic will remember your decision and won't flag that exact pair again. **I dropped files in the import folder and they're not showing up.** Go to the **Import** page and confirm you don't have leftover files that failed a previous import, check that page's status for any error before re-running.