Security (P0): - Remove committed session-secret default; auto-generate and persist a random secret to the config volume when SESSION_SECRET is unset (prevents forgeable session cookies / auth bypass). - Validate playlist names to a safe charset and render sldl configs via literal Python substitution instead of sed (closes a command-injection and path-traversal path through playlist names). - shlex-quote credential values written to shell-sourced env files, and strip newlines from values patched into .conf files. - Render playlist .conf files 0600; warn at startup if the master key is co-located with the config volume; document keeping it separate. Portability: - Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton. - Remove personal defaults (navidrome user "andrew", ephemeral.club URLs). - Ship generic example seeds; move the cross-album dedup keep-list and the legacy playlist import to editable config files; drop the personal _upgrade.csv. - Generic VPN reference in docker-compose.snippet.yml. First-run experience: - Redirect to /setup instead of 500 when OIDC is unconfigured; surface a missing master key inline; entrypoint exits with an actionable message when the config folder is not writable. - Add unauthenticated /health (JSON) and /setup (checklist) diagnostics. Docs: - Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced but missing); expand README with ownership, backups, advanced settings, and migration guidance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.5 KiB
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/musicis your library. alembic writes tagged files here in a normalArtist/Album/Title.extlayout that Navidrome (or any folder-based music server) can read directly./configholds everything else: the app database, the beets database, the rendered pipeline config and credential files, and job logs.master.keyis 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/configon 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
- The app writes the current playlist list to
/config/pipeline/playlists.jsonand 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. sldlreads a playlist's config, fetches the tracklist from Spotify, and downloads each track from Soulseek into a per-playlist dropbox folder.- The downloaded files are tagged. Spotify is treated as the source of truth for artist, title, album, and genre.
- beets imports the tagged files, moving them into
/data/musicin the canonical folder layout. - A matching
.m3u8playlist file is written so the playlist plays back in order. - 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.