GET/POST /api/settings, /api/settings/reset, and the two-phase atomic
export/import bundle (/api/settings/export|import) move to lib/routers/settings.py
with their exclusive helpers (the relpath allowlist validator, the atomic writer,
the library-DB snapshot + sqlite integrity gate, the config-type validator, the
bundle schema). Bodies verbatim except @app->@router and the seam reads:
meta_db->appstate.meta_db, CONFIG_DIR->appstate.config_dir,
_running_version->appstate.running_version(), and _default_settings->
appstate.default_settings (the canonical defaults builder stays in server.py —
the scan + artist-links code share it — and is injected as a new seam callable).
server.py: 5,539 -> 4,478 (-1,061).
Verified: pyflakes clean (bar the pre-existing File/safe_join/tuning_name/ET);
route table IDENTICAL (143); full pytest 2397 passed (154 settings cases incl the
export→import round-trip + library-DB snapshot/restore + relpath-allowlist SSRF/
traversal guards, retargeted onto the settings module). eslint 0.
BEHAVIORAL — needs an on-device settings export→import round-trip sign-off before
merge (do not merge on green CI alone).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move-only. The library metadata cache -- the `MetadataDB` class (4,018 lines)
plus the query helpers it owns (keyset paging cursors, the tuning grouping key,
smart-arrangement naming, tag normalisation, the startup DB-restore swap) --
moves out of server.py into a flat `lib/` module. Every moved block is
byte-identical to its server.py original; server.py is exactly origin/main
minus the six cut ranges, minus the now-dead `import contextlib`, plus the
import-back block and the constructor call site.
server.py: 14,037 -> 9,705 lines.
The one non-verbatim change is the seam that lets the class leave server.py:
`MetadataDB.__init__` now takes `config_dir` explicitly instead of reading the
module-level CONFIG_DIR, so `lib/metadata_db.py` does no IO at import
(Principle V). The `meta_db` singleton stays in server.py, so `server.meta_db`
(282 refs) and `server.app` (67 refs) resolve unchanged and no route moves.
None of the 114 `monkeypatch.setattr(server, ...)` targets moved.
Logging still goes through the `feedBack.server` logger, so log filters and
caplog assertions resolve to the same logger object.
`tests/test_settings_export_library_db.py` imports `_apply_pending_db_restore`
from metadata_db (the test moves with its subject); no other test changed.
Verified: pyflakes clean on the new module (zero undefined names, zero unused
imports) and no new undefined name in server.py; pytest 2341 passed;
node --test 1030 passed; eslint 0 errors; uvicorn boot smoke serves
/api/version, /api/library, and all three migrated plugins' src/ module graphs
(stems, studio, editor -> 200).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the flaky pytest segfault (exit 139): the background scan and enrichment daemon threads (_scan_runner/_enrich_runner) use the shared MetadataDB connection, but test fixtures closed that connection in teardown without stopping them. A daemon thread mid-query on a freed SQLite conn is a native use-after-free → SIGSEGV. The app's startup kicks a scan, so almost any app-booting fixture was vulnerable. It only surfaced now because got-feedback/feedBack#728 added a push trigger, so ci/test runs on every push to main.
Fix: server.py retains the scan/enrich thread handles and adds _join_background_db_threads(); every test fixture now joins the workers before conn.close(). Verified: the full suite runs to completion (no segfault) where it previously crashed at ~25%.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the dev-ops lens's #1 finding from the library charrette
(got-feedback/feedBack#636 item 1): scores, favorites, playlists, and play
history — the only library state a rescan can't rebuild — were absent from
the settings backup. Now GET /api/settings/export carries an additive
`core_server_files` section:
- a CONSISTENT snapshot of web_library.db via the SQLite online-backup API
(a complete single file even while the server runs; taken under the
MetadataDB write lock), base64-encoded;
- custom playlist covers + avatar (CONFIG_DIR/playlist_covers, /avatars),
walked with the existing _walk_export_paths machinery.
Restore is DB-safe:
- POST /api/settings/import STAGES the DB to web_library.db.restore (never
over the live, open file); _apply_pending_db_restore swaps it in at the
next startup BEFORE the connection opens, clearing stale -wal/-shm so a
stale WAL can't be replayed onto the restored file. Response sets
`restart_required` + a warning; custom art applies immediately.
- The staged DB is integrity-checked (open + PRAGMA quick_check) at import
AND again at startup before the live DB is touched — a corrupt/truncated
restore is refused/discarded and the live DB is left intact, so a bad
bundle can never brick startup or lose data.
- Export hard-fails (500) if the snapshot can't be produced (no silent
DB-less backup); a partial import disarms its own staged restore.
Backward-compatible: older servers ignore the new section; a bundle without
it imports as before. Known gap: custom uploaded *song* art is still
commingled with the rebuildable thumbnail cache in art_cache/, so it isn't
bundled yet (tracked follow-up on #636).
Tests: tests/test_settings_export_library_db.py (snapshot consistency,
staged-not-live restore, sidecar clearing, corrupt-DB refusal at import +
startard, traversal rejection, export hard-fail, disarm-on-failure, full
round-trip).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>