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>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user