refactor(server): extract the wishlist routes into routers/wanted.py (R3) (#847)

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) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-10 22:20:10 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7c87538d6b
commit cbc65458e3
3 changed files with 50 additions and 34 deletions
+3 -32
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.
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 ────────────────────────────────────────────────────────────────