Commit Graph

3 Commits

Author SHA1 Message Date
andrew fe98afaaf4 Add beets_service, spotify_client, status_service
beets_service: read-only SQLite queries against the beets DB (mode=ro,
matches the WAL setup in db.py's enable_beets_db_wal). Decodes the `path`
column, which beets stores as a BLOB not TEXT. stats() gives dashboard/
migration-verification parity with `beet stats`.

spotify_client: client-credentials OAuth (token cached in-process),
paginated playlist-track fetch. Verified with mocked httpx responses:
pagination across pages, null-track filtering (removed/local tracks),
ISRC extraction, and token reuse across calls.

status_service: reconciles a playlist's live Spotify tracklist against
beets (IN_LIBRARY, by ISRC or normalized artist+title), sldl's per-playlist
_sldl.m3u8 index (DOWNLOADED_PENDING_IMPORT), and the tag-guard quarantine
dir (QUARANTINED, matched by filename since quarantined files have no
tags by definition) -- everything else is NOT_YET_ATTEMPTED.
ATTEMPTED_NO_MATCH is deliberately not implemented (the only signal is
grepping sldl's per-run text logs, which the migration plan already flags
as unreliable rather than authoritative).

Verified end-to-end against a fake beets DB + sldl index + quarantine dir:
all four statuses reconcile correctly, including both the ISRC-match and
normalized-artist+title-fallback paths.
2026-07-08 13:40:10 -06:00
andrew 5eaae8e813 Add playlist_service and credential_service
playlist_service: CRUD for the playlists table, playlists.json export,
regen.sh invocation, and a one-time seed_legacy() importing the 17-entry
legacy array. Creating/updating a playlist automatically re-renders confs
and re-patches credentials into any newly-generated .conf file.

credential_service: encrypted credential storage (via security/crypto.py)
and per-scope rendering to the exact files the pipeline scripts read --
every <playlist>.conf (spotify/soulseek), navidrome/admin.env,
bandcamp/config.env+cookies.txt, azuracast/api_key, qobuz/{token,app_id,region},
telegram/notify.env. set_credentials() batches multi-field saves so a render
never sees a half-populated scope.

Also fixes a real bug found via integration testing: regen.sh's embedded
python3 -c snippet used backslash-escaped quotes inside a single-quoted
bash string, which is invalid Python syntax (backslash passed through
literally) -- switched to double-quoted bash wrapper + single-quoted Python
strings.

Verified end-to-end: seeded 17 playlists, regenerated all 17 .conf files,
saved spotify/soulseek/navidrome credentials and confirmed correct
patching into rendered files (including bash-sourcing admin.env with
special characters in the password), and confirmed delete/create both
correctly add/remove .conf files with credentials pre-patched on create.
2026-07-08 13:32:37 -06:00
andrew bd56c8153f Add FastAPI app skeleton: settings, db, models, security, auth
- settings.py: pydantic-settings config for the two mount points
  (MUSIC_DATA_DIR, ALEMBIC_CONFIG_DIR), OIDC client config, encryption key path
- db.py: SQLite engine with WAL mode, plain-SQL schema migration runner
  (schema/*.sql, tracked by schema_version -- not the alembic migration tool)
- models.py: SQLAlchemy ORM models matching schema/0001_init.sql
- security/crypto.py: Fernet encrypt/decrypt for the secrets table
- security/oidc.py + routers/auth.py: Pocket ID OIDC login/callback/logout
- security/deps.py: require_auth dependency (redirect-to-login on no session)
- main.py: app factory, lifespan (init_db + beets WAL enable), session
  middleware, minimal dashboard route

Verified boots end-to-end via TestClient: unauthenticated GET / redirects to
/auth/login, static files serve, and all 13 schema tables get created on
first run.
2026-07-08 13:24:18 -06:00