From e572a0a2f5add1ae73f932e0414614869709775c Mon Sep 17 00:00:00 2001 From: byrongamatos Date: Fri, 10 Jul 2026 18:44:58 +0200 Subject: [PATCH] refactor(server): extract the artist-alias routes into routers/artist_aliases.py (R3) Second router, picked by router_scan.py: the artist-aliases / Tidy-up (P4) group ranks at 0 monkeypatch.setattr targets and 0 helpers to relocate. 5 routes (list/set/merge/delete aliases + raw-artist picker), all meta_db-only. Bodies verbatim; only @app. -> @router. and meta_db -> appstate.meta_db. include_router mounts at the original site; full 143-route table identical to origin/main (paths, methods, order). No test retargets: test_artist_alias drives via TestClient(server.app) + server.meta_db, neither of which moved. Verified: pyflakes clean on the router; no new undefined in server.py; JSONResponse still used in server.py (not dead); pytest 2398 passed (18 in test_artist_alias); packaging guard green; eslint 0; boot smoke drives all 5 routes end-to-end (set ACDC->AC/DC, read back, 400 on missing fields, delete). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + docs/size-exemptions.md | 2 +- lib/routers/artist_aliases.py | 68 +++++++++++++++++++++++++++++++++++ server.py | 58 +++--------------------------- 4 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 lib/routers/artist_aliases.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ff723..67857be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 next root-level module can't ship broken. ### Added +- **`routers/` — extracting `server.py`'s route layer, cheapest-first (R3).** Each PR moves a cohesive route group into a `fastapi.APIRouter` under `lib/routers/`, mounted with `app.include_router(...)` at its original site (FastAPI matches in registration order; the full route table stays byte-identical). Bodies are verbatim — only the decorator receiver (`@app` → `@router`) and singleton reads (`meta_db` → `appstate.meta_db`, resolved at call time) change. So far: `audio_effects` (5 routes), `artist_aliases` (5 routes — the Tidy-up/P4 alias overrides). The next cut is picked by a dependency-closure scan that ranks groups by how many `monkeypatch.setattr(server, …)` targets they'd drag along. - **`routers/` — the first extracted route module (R3).** The five audio-effects mapping endpoints move out of `server.py` into `lib/routers/audio_effects.py` as a `fastapi.APIRouter`, mounted with `app.include_router(...)` **at the point in the file diff --git a/docs/size-exemptions.md b/docs/size-exemptions.md index 2255df7..d45684c 100644 --- a/docs/size-exemptions.md +++ b/docs/size-exemptions.md @@ -55,7 +55,7 @@ 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` -(9,386 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` +(9,337 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` extractions and the first `routers/` module) · `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 diff --git a/lib/routers/artist_aliases.py b/lib/routers/artist_aliases.py new file mode 100644 index 0000000..0485fec --- /dev/null +++ b/lib/routers/artist_aliases.py @@ -0,0 +1,68 @@ +"""Artist aliases / Tidy-up (P4) — canonicalize messy artist tags at DISPLAY +("ACDC" -> "AC/DC") without touching feedpak files or the scanner-derived +songs.artist. All DB-only. + +Extracted verbatim from ``server.py`` (R3); only the decorator receiver +(``@app`` -> ``@router``) and the singleton read (``meta_db`` -> +``appstate.meta_db``) changed. The read stays a module attribute so a re-imported +``server`` re-publishes a fresh DB into the seam — see ``appstate.py``. +""" + +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +import appstate + +router = APIRouter() + + +@router.get("/api/artist-aliases") +def list_artist_aliases(): + """Existing raw→canonical overrides (the Tidy-up 'current merges' list).""" + return {"aliases": appstate.meta_db.list_artist_aliases()} + + +@router.get("/api/artists/raw") +def list_raw_artists(limit: int = 2000): + """Distinct RAW artist names + song counts + current canonical — the Tidy-up + picker (you merge raw variants into one canonical).""" + return {"artists": appstate.meta_db.raw_artists(limit)} + + +@router.post("/api/artist-aliases") +def set_artist_alias(data: dict): + """Upsert one override: {raw_name, canonical_name, mb_artist_id?}. A self-alias + (raw == canonical) clears the row instead (un-merge).""" + raw = (data.get("raw_name") or "").strip() + canon = (data.get("canonical_name") or "").strip() + if not raw or not canon: + return JSONResponse({"error": "raw_name and canonical_name are required"}, 400) + result = appstate.meta_db.set_artist_alias(raw, canon, (data.get("mb_artist_id") or None)) + if not result.get("ok"): + # Would form a cycle (raw → … → raw) — refuse rather than corrupt the chain. + return JSONResponse( + {"error": "alias would create a cycle", "raw_name": raw, "canonical_name": canon}, + 409) + return {"ok": True, "raw_name": raw, "canonical_name": result.get("canonical_name", canon)} + + +@router.post("/api/artist-aliases/merge") +def merge_artist_aliases(data: dict): + """Merge several raw artist variants into one canonical: + {raw_names: [...], canonical_name}. The canonical's own self-alias is skipped. + Returns {merged: N}.""" + canon = (data.get("canonical_name") or "").strip() + raws = data.get("raw_names") + if not canon: + return JSONResponse({"error": "canonical_name is required"}, 400) + if not isinstance(raws, list) or not raws: + return JSONResponse({"error": "raw_names must be a non-empty array"}, 400) + n = appstate.meta_db.merge_artists(raws, canon) + return {"merged": n, "canonical_name": canon} + + +@router.delete("/api/artist-aliases/{raw_name:path}") +def delete_artist_alias(raw_name: str): + """Remove one override so that raw artist stands on its own again.""" + appstate.meta_db.remove_artist_alias(raw_name) + return {"ok": True} diff --git a/server.py b/server.py index 4e92b9e..a7b3ab9 100644 --- a/server.py +++ b/server.py @@ -71,7 +71,7 @@ from audio_effects_db import AudioEffectsMappingDB # 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 +from routers import audio_effects, artist_aliases import sloppak as sloppak_mod import drums as drums_mod import notation as notation_mod @@ -4847,59 +4847,9 @@ def list_tags(): # ── Artist aliases / Tidy-up (P4) ──────────────────────────────────────────── -# Canonicalize messy artist tags at DISPLAY ("ACDC" → "AC/DC") without touching -# the feedpak files or the scanner-derived songs.artist. All DB-only. - -@app.get("/api/artist-aliases") -def list_artist_aliases(): - """Existing raw→canonical overrides (the Tidy-up 'current merges' list).""" - return {"aliases": meta_db.list_artist_aliases()} - - -@app.get("/api/artists/raw") -def list_raw_artists(limit: int = 2000): - """Distinct RAW artist names + song counts + current canonical — the Tidy-up - picker (you merge raw variants into one canonical).""" - return {"artists": meta_db.raw_artists(limit)} - - -@app.post("/api/artist-aliases") -def set_artist_alias(data: dict): - """Upsert one override: {raw_name, canonical_name, mb_artist_id?}. A self-alias - (raw == canonical) clears the row instead (un-merge).""" - raw = (data.get("raw_name") or "").strip() - canon = (data.get("canonical_name") or "").strip() - if not raw or not canon: - return JSONResponse({"error": "raw_name and canonical_name are required"}, 400) - result = meta_db.set_artist_alias(raw, canon, (data.get("mb_artist_id") or None)) - if not result.get("ok"): - # Would form a cycle (raw → … → raw) — refuse rather than corrupt the chain. - return JSONResponse( - {"error": "alias would create a cycle", "raw_name": raw, "canonical_name": canon}, - 409) - return {"ok": True, "raw_name": raw, "canonical_name": result.get("canonical_name", canon)} - - -@app.post("/api/artist-aliases/merge") -def merge_artist_aliases(data: dict): - """Merge several raw artist variants into one canonical: - {raw_names: [...], canonical_name}. The canonical's own self-alias is skipped. - Returns {merged: N}.""" - canon = (data.get("canonical_name") or "").strip() - raws = data.get("raw_names") - if not canon: - return JSONResponse({"error": "canonical_name is required"}, 400) - if not isinstance(raws, list) or not raws: - return JSONResponse({"error": "raw_names must be a non-empty array"}, 400) - n = meta_db.merge_artists(raws, canon) - return {"merged": n, "canonical_name": canon} - - -@app.delete("/api/artist-aliases/{raw_name:path}") -def delete_artist_alias(raw_name: str): - """Remove one override so that raw artist stands on its own again.""" - meta_db.remove_artist_alias(raw_name) - return {"ok": True} +# Mounted here, where these routes used to be defined (FastAPI matches in +# registration order). Implementation in lib/routers/artist_aliases.py. +app.include_router(artist_aliases.router) # ── Artist pages (launch charrette PR-B) ──────────────────────────────────────