b135f11557
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>
71 lines
3.5 KiB
Markdown
71 lines
3.5 KiB
Markdown
# 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.
|