# Architecture This is a short tour of how alembic is put together, for anyone who wants to understand or modify it. If you only want to run it, the README is enough. ## One container, everything inside alembic ships as a single Docker image. The image is code only. It holds no playlists, no credentials, and no music. Everything that changes over time lives in mounted folders, so you can rebuild or upgrade the image without touching your data. There are two data mounts, plus one small secret: - `/data/music` is your library. alembic writes tagged files here in a normal `Artist/Album/Title.ext` layout that Navidrome (or any folder-based music server) can read directly. - `/config` holds everything else: the app database, the beets database, the rendered pipeline config and credential files, and job logs. - `master.key` is mounted as a Docker secret at `/run/secrets/alembic_master_key`. It is the key that encrypts your saved credentials, and it is kept separate from `/config` on purpose (see the README on why). ## The pieces - **The web app** is FastAPI. It serves the UI, handles login through your OIDC provider, and owns all the settings. It reads and writes two SQLite databases: its own (`/config/alembic.db`, playlists, jobs, encrypted credentials, dedup and genre review state) and the beets library database. - **The scheduler** is APScheduler, running in the same process as the web app. It fires playlist syncs and maintenance jobs on their cron schedules. It keeps its schedule in memory and rebuilds it from code plus the database on every startup, so there is no separate scheduler process or job store to manage. - **The pipeline** is a set of shell and Python scripts under `pipeline/`. The app runs them as subprocesses. They do the actual downloading, tagging, importing, deduplicating, and so on. `sldl` (the Soulseek downloader) is vendored in the image and also run as a subprocess. A single lock serializes pipeline work. Only one pipeline job runs at a time; if a second is triggered while one is running, it is recorded as skipped rather than run concurrently. This mirrors the old single-lock behavior and avoids two jobs fighting over the same files. ## What happens on a playlist sync 1. The app writes the current playlist list to `/config/pipeline/playlists.json` and renders one sldl config file per playlist from a template. Your Spotify and Soulseek credentials are patched into those config files, decrypted from the database just before use. 2. `sldl` reads a playlist's config, fetches the tracklist from Spotify, and downloads each track from Soulseek into a per-playlist dropbox folder. 3. The downloaded files are tagged. Spotify is treated as the source of truth for artist, title, album, and genre. 4. beets imports the tagged files, moving them into `/data/music` in the canonical folder layout. 5. A matching `.m3u8` playlist file is written so the playlist plays back in order. 6. Navidrome is asked to rescan so the new tracks show up. Every run gets a row in the job history and its own log file, both visible in the UI under Settings then Jobs. ## A note on the name "alembic" here is this project. It is not the Python database-migration tool also called Alembic. This project uses plain SQL files under `schema/` for its own tiny migrations and does not depend on that tool. The name is a nod to a still, which distills a raw input into something refined, which is roughly what this does to a Spotify playlist.