From 4525a9201a05a068db11ffa6313ccf768b941c38 Mon Sep 17 00:00:00 2001 From: byrongamatos Date: Fri, 10 Jul 2026 22:09:55 +0200 Subject: [PATCH] refactor(server): extract the wishlist routes into routers/wanted.py (R3) Free after _clean_str moved to lib (#841): wanted's deps are JSONResponse, _clean_str (reqfields), app + meta_db (seam). 3 routes (list/add/remove), 0 setattr targets, 0 helpers to relocate. Bodies verbatim; @app -> @router, meta_db -> appstate.meta_db. include_router at the original site; 143-route table identical. No test retargeting. server.py: 8,003 -> 7,974 (this branch is independent of the chart PR). Verified: pyflakes clean; route table identical; pytest 2401 passed (52 in test_wanted_api); eslint 0. Boot smoke: add wishlist entry -> list -> delete, 400 on missing artist+title (_clean_str path). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/size-exemptions.md | 4 ++-- lib/routers/wanted.py | 45 +++++++++++++++++++++++++++++++++++++++++ server.py | 35 +++----------------------------- 3 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 lib/routers/wanted.py diff --git a/docs/size-exemptions.md b/docs/size-exemptions.md index 44c2dea3..d637d136 100644 --- a/docs/size-exemptions.md +++ b/docs/size-exemptions.md @@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable. ## 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` -(7,909 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` -extractions and six `routers/` modules) · +(7,880 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` +extractions and seven `routers/` modules) · `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 lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js` diff --git a/lib/routers/wanted.py b/lib/routers/wanted.py new file mode 100644 index 00000000..22f4b2a5 --- /dev/null +++ b/lib/routers/wanted.py @@ -0,0 +1,45 @@ +"""Wishlist / "wanted" API (feedBack#636) — songs the user wants but doesn't own. + +Extracted verbatim from ``server.py`` (R3); edits: ``@app`` -> ``@router``, +``meta_db`` -> ``appstate.meta_db``, ``_clean_str`` from ``reqfields``. +""" + +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +import appstate +from reqfields import _clean_str + +router = APIRouter() + + +@router.get("/api/wanted") +def api_list_wanted(): + """The wishlist — songs the user wants but doesn't own yet (newest first).""" + return {"wanted": appstate.meta_db.list_wanted()} + + +@router.post("/api/wanted") +def api_add_wanted(data: dict): + """Add a not-owned song to the wishlist. `artist`/`title` are required (at + least one non-empty); `source`/`source_ref`/`note` are optional. Idempotent + on identity so producers (find_more ownership-diff, manual add) can re-post.""" + if not isinstance(data, dict): + return JSONResponse({"error": "body must be an object"}, status_code=400) + artist = _clean_str(data.get("artist")) + title = _clean_str(data.get("title")) + if not artist and not title: + return JSONResponse({"error": "artist or title required"}, status_code=400) + row = appstate.meta_db.add_wanted( + artist=artist, title=title, + source=_clean_str(data.get("source")) or "manual", + source_ref=_clean_str(data.get("source_ref")), + note=_clean_str(data.get("note")), + ) + return {"ok": True, "wanted": row} + + +@router.delete("/api/wanted/{wanted_id}") +def api_remove_wanted(wanted_id: int): + """Remove a wishlist entry by id.""" + return {"ok": appstate.meta_db.remove_wanted(wanted_id)} diff --git a/server.py b/server.py index 9a7d81df..277a7a1e 100644 --- a/server.py +++ b/server.py @@ -55,7 +55,7 @@ # Lives in lib/ because that is the one core dir every packaging path copies. import appstate # Extracted route modules. They import `appstate`, never `server` — one-way graph. -from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart +from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted import sloppak as sloppak_mod import loosefolder as loosefolder_mod # Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/ @@ -5375,37 +5375,8 @@ def api_session_continue(): # ── Wishlist / "wanted" API (feedBack#636 item 4) ───────────────────────────── - -@app.get("/api/wanted") -def api_list_wanted(): - """The wishlist — songs the user wants but doesn't own yet (newest first).""" - return {"wanted": meta_db.list_wanted()} - - -@app.post("/api/wanted") -def api_add_wanted(data: dict): - """Add a not-owned song to the wishlist. `artist`/`title` are required (at - least one non-empty); `source`/`source_ref`/`note` are optional. Idempotent - on identity so producers (find_more ownership-diff, manual add) can re-post.""" - if not isinstance(data, dict): - return JSONResponse({"error": "body must be an object"}, status_code=400) - artist = _clean_str(data.get("artist")) - title = _clean_str(data.get("title")) - if not artist and not title: - return JSONResponse({"error": "artist or title required"}, status_code=400) - row = meta_db.add_wanted( - artist=artist, title=title, - source=_clean_str(data.get("source")) or "manual", - source_ref=_clean_str(data.get("source_ref")), - note=_clean_str(data.get("note")), - ) - return {"ok": True, "wanted": row} - - -@app.delete("/api/wanted/{wanted_id}") -def api_remove_wanted(wanted_id: int): - """Remove a wishlist entry by id.""" - return {"ok": meta_db.remove_wanted(wanted_id)} +# Mounted here (registration order). Implementation in lib/routers/wanted.py. +app.include_router(wanted.router) # ── Loops API ────────────────────────────────────────────────────────────────