refactor(server): batch the small meta_db user-state endpoints into routers/library_extras.py (R3) (#850)

work keeper-chart prefs (3) + favorites toggle + tags list + saved toggle +
session/continue — 7 routes across 5 domains, all meta_db-only (0 setattr, 0
helpers). Bodies verbatim; @app -> @router, meta_db -> appstate.meta_db,
_clean_str from reqfields.

These were scattered singletons with no natural neighbor, so they're grouped as
"small library/user-state endpoints" and mounted once. All paths are distinct
and non-overlapping, so registering them together doesn't change routing:
verified the route SET is identical to origin/main (143) AND that no moved path
shadows or is shadowed by another (order-independence check).

server.py: 7,880 -> 7,833.

Verified: pyflakes clean; route set identical + order-independent; pytest 2401
passed; packaging guard 45; eslint 0. Boot smoke: tags/session GET 200,
favorites/saved toggle (400 on missing filename), work/charts 200.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-10 23:37:24 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2281cac438
commit ea8834862d
3 changed files with 82 additions and 54 deletions
+2 -2
View File
@@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable.
## Planned, NOT exempt (owned by split plans — listed so nothing falls between states) ## Planned, NOT exempt (owned by split plans — listed so nothing falls between states)
core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py` core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py`
(7,880 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` (7,833 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
extractions and seven `routers/` modules) · extractions and eight `routers/` modules) ·
`lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines `lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines
and is a monolith in its own right, to be split per-table once the router train and is a monolith in its own right, to be split per-table once the router train
lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js` lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js`
+75
View File
@@ -0,0 +1,75 @@
"""Small meta_db-backed library / user-state endpoints — work keeper-chart
prefs, favorites, personal tags, saved-for-later, and continue-playing.
Extracted verbatim from ``server.py`` (R3); edits: ``@app`` -> ``@router``,
``meta_db`` -> ``appstate.meta_db``, ``_clean_str`` from ``reqfields``. All paths
are distinct and non-overlapping, so mounting them together (rather than at each
original scattered site) does not change routing.
"""
from fastapi import APIRouter
from fastapi.responses import JSONResponse
import appstate
from reqfields import _clean_str
router = APIRouter()
@router.get("/api/work/{work_key:path}/charts")
def api_get_work_charts(work_key: str):
"""All charts in a work + which is the keeper (your pick vs auto-pick)."""
return appstate.meta_db.work_charts(work_key)
@router.put("/api/work/{work_key:path}/preferred")
def api_set_work_preferred(work_key: str, data: dict):
"""Set the keeper chart of a work: body {filename}. The filename must be a
current member of the work. Returns the refreshed chart list."""
fn = (data.get("filename") or "").strip()
if not fn:
return JSONResponse({"error": "filename is required"}, 400)
members = {c["filename"] for c in appstate.meta_db.work_charts(work_key)["charts"]}
if fn not in members:
return JSONResponse({"error": "filename is not a chart of this work"}, 400)
appstate.meta_db.set_chart_preferred(work_key, fn)
return appstate.meta_db.work_charts(work_key)
@router.delete("/api/work/{work_key:path}/preferred")
def api_reset_work_preferred(work_key: str):
"""Reset a work to auto-pick (drop the explicit preferred)."""
appstate.meta_db.clear_chart_preferred(work_key)
return appstate.meta_db.work_charts(work_key)
@router.post("/api/favorites/toggle")
def toggle_favorite(data: dict):
"""Toggle a song's favorite status."""
filename = data.get("filename", "")
if not filename:
return {"error": "No filename"}
new_state = appstate.meta_db.toggle_favorite(filename)
return {"favorite": new_state}
@router.get("/api/tags")
def list_tags():
"""All personal tags in use (over still-present songs), most-used first —
powers the tag filter UI."""
return {"tags": appstate.meta_db.all_tags()}
@router.post("/api/saved/toggle")
def api_toggle_saved(data: dict):
"""Add/remove a song on the reserved Saved-for-Later playlist."""
filename = _clean_str(data.get("filename"))
if not filename:
return JSONResponse({"error": "filename required"}, status_code=400)
return {"saved": appstate.meta_db.toggle_saved(filename)}
@router.get("/api/session/continue")
def api_session_continue():
"""The Continue-Playing card's song (most recent play) or null."""
return appstate.meta_db.continue_session()
+5 -52
View File
@@ -55,7 +55,7 @@ from dlc_paths import _get_dlc_dir, _resolve_dlc_path
# Lives in lib/ because that is the one core dir every packaging path copies. # Lives in lib/ because that is the one core dir every packaging path copies.
import appstate import appstate
# Extracted route modules. They import `appstate`, never `server` — one-way graph. # Extracted route modules. They import `appstate`, never `server` — one-way graph.
from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras
import sloppak as sloppak_mod import sloppak as sloppak_mod
import loosefolder as loosefolder_mod import loosefolder as loosefolder_mod
# Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/ # Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/
@@ -4229,31 +4229,10 @@ async def list_library(q: str = "", page: int = 0, size: int = 24, sort: str = "
# views is deferred to P5d — there's no server-side library event bus today, and # views is deferred to P5d — there's no server-side library event bus today, and
# the drawer updates itself from these responses. # the drawer updates itself from these responses.
@app.get("/api/work/{work_key:path}/charts") # ── Small library / user-state endpoints (work prefs, favorites, tags, saved, session)
def api_get_work_charts(work_key: str): # Mounted here; implementation in lib/routers/library_extras.py. Paths are all
"""All charts in a work + which is the keeper (your pick vs auto-pick).""" # distinct, so registering them together does not change routing.
return meta_db.work_charts(work_key) app.include_router(library_extras.router)
@app.put("/api/work/{work_key:path}/preferred")
def api_set_work_preferred(work_key: str, data: dict):
"""Set the keeper chart of a work: body {filename}. The filename must be a
current member of the work. Returns the refreshed chart list."""
fn = (data.get("filename") or "").strip()
if not fn:
return JSONResponse({"error": "filename is required"}, 400)
members = {c["filename"] for c in meta_db.work_charts(work_key)["charts"]}
if fn not in members:
return JSONResponse({"error": "filename is not a chart of this work"}, 400)
meta_db.set_chart_preferred(work_key, fn)
return meta_db.work_charts(work_key)
@app.delete("/api/work/{work_key:path}/preferred")
def api_reset_work_preferred(work_key: str):
"""Reset a work to auto-pick (drop the explicit preferred)."""
meta_db.clear_chart_preferred(work_key)
return meta_db.work_charts(work_key)
# ── Chart-level endpoints (split/work/fileinfo) ────────────────────────────── # ── Chart-level endpoints (split/work/fileinfo) ──────────────────────────────
@@ -4394,14 +4373,6 @@ async def list_tuning_names(provider: str = "local"):
return await _call_library_provider_async(library_provider, "tuning_names") return await _call_library_provider_async(library_provider, "tuning_names")
@app.post("/api/favorites/toggle")
def toggle_favorite(data: dict):
"""Toggle a song's favorite status."""
filename = data.get("filename", "")
if not filename:
return {"error": "No filename"}
new_state = meta_db.toggle_favorite(filename)
return {"favorite": new_state}
# ── Personal per-song metadata (difficulty / notes / tags) ─────────────────── # ── Personal per-song metadata (difficulty / notes / tags) ───────────────────
@@ -4564,11 +4535,6 @@ def batch_song_user_meta(data: dict):
return {"updated": n, "tags": meta_db.all_tags()} return {"updated": n, "tags": meta_db.all_tags()}
@app.get("/api/tags")
def list_tags():
"""All personal tags in use (over still-present songs), most-used first —
powers the tag filter UI."""
return {"tags": meta_db.all_tags()}
# ── Artist aliases / Tidy-up (P4) ──────────────────────────────────────────── # ── Artist aliases / Tidy-up (P4) ────────────────────────────────────────────
@@ -5359,19 +5325,6 @@ def api_delete_collection(pid: int):
return {"ok": True} return {"ok": True}
@app.post("/api/saved/toggle")
def api_toggle_saved(data: dict):
"""Add/remove a song on the reserved Saved-for-Later playlist."""
filename = _clean_str(data.get("filename"))
if not filename:
return JSONResponse({"error": "filename required"}, status_code=400)
return {"saved": meta_db.toggle_saved(filename)}
@app.get("/api/session/continue")
def api_session_continue():
"""The Continue-Playing card's song (most recent play) or null."""
return meta_db.continue_session()
# ── Wishlist / "wanted" API (feedBack#636 item 4) ───────────────────────────── # ── Wishlist / "wanted" API (feedBack#636 item 4) ─────────────────────────────