Files
alembic/docs/ARCHITECTURE.md
T
andrew b135f11557 Security hardening and first-run/portability improvements
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>
2026-07-09 14:25:55 -06:00

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/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.