Add README and proper favicon/PWA icon set
README covers what alembic does, prerequisites, a full deploy quickstart, credential setup, and a page-by-page usage tour, written for someone running Docker but not necessarily comfortable coding or sysadmin work. Favicon/icons were prepared in a separate theme scratch dir but never wired in; moved them into app/static and pointed base.html at the full icon set (SVG, PNG fallbacks, apple-touch-icon, manifest) instead of the single inline data-URI SVG it had before.
@@ -0,0 +1,232 @@
|
||||
<p align="center">
|
||||
<img src="app/static/icon.svg" width="112" height="112" alt="alembic logo">
|
||||
</p>
|
||||
|
||||
# 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
|
||||
- **Find duplicate copies of songs** (the same track downloaded twice, or in two different formats) 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, and the history of every automated job it's run.
|
||||
|
||||
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 free Spotify Developer app.** This lets alembic read playlist contents (it does not download from Spotify itself, only Soulseek). Takes two minutes, see the credentials section below.
|
||||
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
|
||||
|
||||
Clone this repository onto your Docker host and build the image locally:
|
||||
|
||||
```bash
|
||||
git clone <this-repo-url> alembic
|
||||
cd alembic
|
||||
docker build -t alembic:latest .
|
||||
```
|
||||
|
||||
This takes a few minutes the first time. You don't need to touch anything inside the `app/` or `pipeline/` folders, the Dockerfile handles all of it.
|
||||
|
||||
### 2. Create your folders
|
||||
|
||||
alembic needs exactly two 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, and logs.
|
||||
|
||||
```bash
|
||||
mkdir -p /path/to/your/music
|
||||
mkdir -p /path/to/alembic-config
|
||||
```
|
||||
|
||||
Everything else (which playlists you follow, your credentials, scheduling) is stored inside that config folder, so if you ever need to move alembic to a new machine, copying those two folders is all it takes.
|
||||
|
||||
### 3. Generate a master key
|
||||
|
||||
alembic encrypts every credential you give it (Spotify keys, Soulseek password, and so on) before storing it. It needs a key to do that:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32 > /path/to/alembic-config/master.key
|
||||
chmod 600 /path/to/alembic-config/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.
|
||||
|
||||
### 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://<wherever-you-host-alembic>/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 `docker-compose.yml` next to your alembic folder:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
alembic:
|
||||
image: alembic:latest
|
||||
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-config/master.key
|
||||
```
|
||||
|
||||
A few notes on the environment values:
|
||||
|
||||
- `SESSION_SECRET` can be any long random string, generate one with `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:<vpn-container>"` 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://<your-server>: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 **Manage 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 "set" badge).
|
||||
|
||||
| Service | What to enter | Where to get it |
|
||||
|---|---|---|
|
||||
| **Spotify** | Client ID, Client Secret | Create a free app at [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard). You don't need any special access, the basic free tier is enough to read playlist contents. |
|
||||
| **Soulseek** | Username, Password | Your normal Soulseek login. |
|
||||
| **Navidrome** | 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`. |
|
||||
| **AzuraCast** *(optional)* | API key | From your AzuraCast station's API settings, if you run one. |
|
||||
| **Qobuz** *(optional)* | Token, App ID, region | From your Qobuz account, if you use it. |
|
||||
| **Telegram** *(optional)* | Bot token, chat ID | Create a bot via [@BotFather](https://t.me/BotFather) if you want job notifications sent to a chat. |
|
||||
|
||||
## Adding your first playlist
|
||||
|
||||
1. Go to **Manage 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, and recent job activity.
|
||||
- **Playlists** — add, pause, or remove synced Spotify playlists, and see per-playlist download status.
|
||||
- **Library** — browse and search everything in your library. Fix tags on a track, re-tag it from a URL, or delete it entirely.
|
||||
- **Import** — drop a file into your import folder (or upload it through the browser) to manually add something outside of the normal playlist flow.
|
||||
- **Dedup** — review duplicate tracks alembic has found. Nothing is ever deleted automatically, you approve each deletion, or mark a pair as "keep both" if it's not actually a duplicate.
|
||||
- **Genres** — review genre tags alembic has written or wants to write, and lock an artist's genre so it's never changed again.
|
||||
- **Jobs** — see the history and status of every scheduled and manual job, with full logs.
|
||||
|
||||
## Updating alembic
|
||||
|
||||
Pull the latest code, rebuild, and restart:
|
||||
|
||||
```bash
|
||||
cd alembic
|
||||
git pull
|
||||
docker build -t alembic:latest .
|
||||
docker compose up -d --force-recreate alembic
|
||||
```
|
||||
|
||||
Your library, credentials, and settings all live in your two mounted folders, so updating the image never touches your data.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**I can't log in / get redirected in a loop.**
|
||||
Double check the redirect URI registered with your OIDC provider exactly matches `https://<your-host>/auth/callback`, including `http` vs `https`.
|
||||
|
||||
**A playlist says it's synced but nothing downloaded.**
|
||||
Check that playlist's job history under **Jobs**, the log will usually show whether Soulseek couldn't find a track, or a credential is missing.
|
||||
|
||||
**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.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 697 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#14151f"></stop>
|
||||
<stop offset="100%" stop-color="#05050a"></stop>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx="50%" cy="46%" r="55%">
|
||||
<stop offset="0%" stop-color="#a78bfa" stop-opacity="0.55"></stop>
|
||||
<stop offset="55%" stop-color="#67e8f9" stop-opacity="0.18"></stop>
|
||||
<stop offset="100%" stop-color="#67e8f9" stop-opacity="0"></stop>
|
||||
</radialGradient>
|
||||
<linearGradient id="flaskGrad" x1="146" y1="113" x2="301" y2="377" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#7dd3fc"></stop>
|
||||
<stop offset="50%" stop-color="#a78bfa"></stop>
|
||||
<stop offset="100%" stop-color="#f9a8d4"></stop>
|
||||
</linearGradient>
|
||||
<filter id="blur" x="-60%" y="-60%" width="220%" height="220%">
|
||||
<feGaussianBlur stdDeviation="34"></feGaussianBlur>
|
||||
</filter>
|
||||
<path id="star" d="M12 0c.6 5.7 1.8 8.6 4.2 10.6 2.4 2 5.3 2.6 7.8 3.4-2.5.8-5.4 1.4-7.8 3.4-2.4 2-3.6 4.9-4.2 10.6-.6-5.7-1.8-8.6-4.2-10.6C5.4 15.4 2.5 14.8 0 14c2.5-.8 5.4-1.4 7.8-3.4C10.2 8.6 11.4 5.7 12 0Z"></path>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="512" height="512" rx="112" fill="url(#bgGrad)"></rect>
|
||||
<rect x="1.5" y="1.5" width="509" height="509" rx="110.5" fill="none" stroke="#ffffff" stroke-opacity="0.06"></rect>
|
||||
|
||||
<circle cx="256" cy="236" r="150" fill="url(#glow)" filter="url(#blur)"></circle>
|
||||
|
||||
<use href="#star" x="118" y="108" width="30" height="30" fill="#e9e6ff" opacity="0.55"></use>
|
||||
<use href="#star" x="366" y="352" width="22" height="22" fill="#bff3ff" opacity="0.5"></use>
|
||||
|
||||
<g>
|
||||
<path d="M211 113.5h90v78l64.5 129a39 39 0 0 1 -34.95 56.4H181.45A39 39 0 0 1 146.5 320.5L211 191.5V113.5z" fill="url(#flaskGrad)"></path>
|
||||
<path d="M211 113.5h90M199 205h114" stroke="#08090d" stroke-width="13.5" stroke-linecap="round" opacity="0.35"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Alembic",
|
||||
"short_name": "Alembic",
|
||||
"description": "Playlist ingestion, tagging, dedupe, and library tools for Alembic.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#08090d",
|
||||
"theme_color": "#08090d",
|
||||
"icons": [
|
||||
{ "src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/static/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,12 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}alembic{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='24' y2='24'%3E%3Cstop offset='0%25' stop-color='%237dd3fc'/%3E%3Cstop offset='50%25' stop-color='%23a78bfa'/%3E%3Cstop offset='100%25' stop-color='%23f9a8d4'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23g)' d='M9 2.5h6v5.2l4.3 8.6a2.6 2.6 0 0 1-2.33 3.76H7.03A2.6 2.6 0 0 1 4.7 16.3L9 7.7V2.5z'/%3E%3C/svg%3E">
|
||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/static/favicon-48.png">
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/static/site.webmanifest">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||