mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
refactor(server): extract the chart routes into routers/chart.py (R3) (#846)
Unblocked by the DLC-path substrate (#843): chart's only server-module deps are now app + meta_db (both seam); _get_dlc_dir/_resolve_dlc_path come from dlc_paths, sloppak/loose detection from the shared lib modules. 4 routes (split/unsplit/ work/fileinfo), meta_db-only otherwise. 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 to origin/main. No test retargeting. server.py: 8,003 -> 7,909. Verified: pyflakes clean on the router; no new undefined/dead in server.py; route table identical; pytest 2401 passed (74 across work_charts/context_menu/ group_filter/packaging); eslint 0. Boot smoke: chart/work 200, chart/fileinfo resolves the real pack path through _resolve_dlc_path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c8701991cb
commit
7c87538d6b
@@ -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
|
||||
from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart
|
||||
import sloppak as sloppak_mod
|
||||
import loosefolder as loosefolder_mod
|
||||
# Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/
|
||||
@@ -4256,103 +4256,9 @@ def api_reset_work_preferred(work_key: str):
|
||||
return meta_db.work_charts(work_key)
|
||||
|
||||
|
||||
@app.post("/api/chart/{filename:path}/split")
|
||||
def api_split_chart(filename: str):
|
||||
"""'These aren't the same song' — split this chart out as its own singleton
|
||||
work. Under /api/chart (NOT /api/song) so the DELETE /api/song/{path}
|
||||
catch-all can't shadow it."""
|
||||
key = meta_db._canonical_song_filename(filename)
|
||||
meta_db.split_chart(key)
|
||||
return {"ok": True, "filename": key}
|
||||
|
||||
|
||||
@app.post("/api/chart/{filename:path}/unsplit")
|
||||
def api_unsplit_chart(filename: str):
|
||||
"""Undo a split — rejoin the chart to its work."""
|
||||
key = meta_db._canonical_song_filename(filename)
|
||||
meta_db.unsplit_chart(key)
|
||||
return {"ok": True, "filename": key}
|
||||
|
||||
|
||||
@app.get("/api/chart/{filename:path}/work")
|
||||
def api_get_chart_work(filename: str):
|
||||
"""Resolve a chart's work membership: {work_key, chart_count}. For openers
|
||||
on rows that came from an ungrouped query (the tree view) — grouped grid
|
||||
rows already carry both fields inline."""
|
||||
return meta_db.chart_work(filename)
|
||||
|
||||
|
||||
@app.get("/api/chart/{filename:path}/fileinfo")
|
||||
def api_chart_fileinfo(filename: str):
|
||||
"""The context menu's "Get info": where the file lives + what the pack
|
||||
contains. Under /api/chart — the GET /api/song/{path} catch-all would
|
||||
swallow a /api/song/…/fileinfo suffix. Read-only; demo-mode blocks it
|
||||
because it exposes filesystem paths."""
|
||||
dlc = _get_dlc_dir()
|
||||
if not dlc:
|
||||
raise HTTPException(status_code=404, detail="not configured")
|
||||
p = _resolve_dlc_path(dlc, filename)
|
||||
if p is None:
|
||||
raise HTTPException(status_code=403, detail="forbidden")
|
||||
if not p.exists():
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
# Restrict to actual charts — sloppak or loose song. Without this the route
|
||||
# would stat ANY file the user happens to keep under DLC_DIR (e.g. notes),
|
||||
# leaking its path/size; the app only recognises these two song formats.
|
||||
is_pak = sloppak_mod.is_sloppak(p)
|
||||
is_loose = loosefolder_mod.is_loose_song(p)
|
||||
if not (is_pak or is_loose):
|
||||
raise HTTPException(status_code=404, detail="not a chart")
|
||||
st = p.stat()
|
||||
info = {
|
||||
"filename": filename,
|
||||
"path": str(p),
|
||||
"folder": str(p.parent),
|
||||
"format": "sloppak" if is_pak else "loose",
|
||||
# Directory-form songs report the tree's total (covers loose folders
|
||||
# and dir-form paks); zip-form paks report the archive size. Symlinked
|
||||
# entries are skipped so a link inside the folder can't pull in — or
|
||||
# leak the size of — a file outside it.
|
||||
"size": (st.st_size if p.is_file()
|
||||
else sum(f.stat().st_size for f in p.rglob("*")
|
||||
if f.is_file() and not f.is_symlink())),
|
||||
"mtime": st.st_mtime,
|
||||
}
|
||||
if is_pak:
|
||||
try:
|
||||
m = sloppak_mod.load_manifest(p) or {}
|
||||
except Exception:
|
||||
m = {}
|
||||
arrs = [str(a.get("name", a.get("id", ""))) for a in (m.get("arrangements") or [])
|
||||
if isinstance(a, dict)]
|
||||
stems = [str(s.get("id", "")) for s in (m.get("stems") or []) if isinstance(s, dict)]
|
||||
try:
|
||||
has_cover = sloppak_mod.read_cover_bytes(p, m) is not None
|
||||
except Exception:
|
||||
has_cover = False
|
||||
# The optional identity/catalog keys, listed only when present — the
|
||||
# Get-info panel's "what this pack carries vs what's missing" readout.
|
||||
identity = {k: m.get(k) for k in
|
||||
("mbid", "isrc", "genres", "track", "disc", "album_artist",
|
||||
"feedpak_version", "language")
|
||||
if m.get(k) not in (None, "", [])}
|
||||
info["manifest"] = {
|
||||
"title": str(m.get("title", "")), "artist": str(m.get("artist", "")),
|
||||
"album": str(m.get("album", "")), "year": str(m.get("year", "") or ""),
|
||||
"arrangements": arrs, "stems": stems,
|
||||
"has_cover": has_cover, "has_lyrics": bool(m.get("lyrics")),
|
||||
"authors": [a.get("name", "") if isinstance(a, dict) else str(a)
|
||||
for a in (m.get("authors") or [])],
|
||||
"identity": identity,
|
||||
}
|
||||
# The enrichment verdict, so Get info can say "Matched (auto, 96%)" /
|
||||
# "Pinned by you" / "Not matched" alongside the file facts.
|
||||
row = meta_db.get_enrichment(filename)
|
||||
if row:
|
||||
info["match"] = {k: row.get(k) for k in
|
||||
("match_state", "match_source", "match_score",
|
||||
"canon_artist", "canon_title", "canon_album", "canon_year")}
|
||||
return info
|
||||
# ── Chart-level endpoints (split/work/fileinfo) ──────────────────────────────
|
||||
# Mounted here (registration order). Implementation in lib/routers/chart.py.
|
||||
app.include_router(chart.router)
|
||||
|
||||
|
||||
@app.get("/api/library/albums")
|
||||
|
||||
Reference in New Issue
Block a user