mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-18 14:32:43 +00:00
refactor(server): extract /api/tunings into routers/tunings.py + lib/appconfig.py (R3) (#858)
ship-ci / ci (push) Waiting to run
ship-ci / ci (push) Waiting to run
The merged-tuning-catalog route moves to lib/routers/tunings.py, verbatim except
@app->@router, CONFIG_DIR->appstate.config_dir, and the two seam substrates it
needed:
- lib/appconfig.py — the pure config.json reader `_load_config` (used by ~11
server sites + future config-reading routers). server.py re-imports it, so
those call sites and any `server._load_config` test reference are unchanged.
- appstate.tuning_providers — the TuningProviderRegistry instance injected by
reference (a stable object mutated in place via register()/unregister()), so
the router reads the same registry plugins populate through plugin_context.
The instance stays defined in server.py, so `server.tuning_providers` still
resolves — zero test retargets.
The tuning constants (DEFAULT_REFERENCE_PITCH, TUNING_PRESET_MIDIS,
freqs_to_midis) already live in lib/tunings.py and are imported directly.
server.py: 6,960 -> 6,917.
Verified: pyflakes clean (bar the pre-existing unused `tuning_name` import);
route table IDENTICAL (143); full pytest 2400 passed (110 tuning/config cases);
eslint 0. Boot smoke: /api/tunings serves referencePitch + tunings + tuningMidis.
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
cce95cbd1e
commit
508829c012
+1
-1
@@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Perf harness now measures 2D-highway frame time (R3c gate).** `scripts/perf-baseline.mjs` gains a `--song` mode that reports per-frame draw-cost p50/p95/p99 (draw-tagged via `highway.addDrawHook`), the metric that gates the `highway.js` split. Maintainer/CI-only; baseline recorded in `docs/perf-baseline.md`.
|
- **Perf harness now measures 2D-highway frame time (R3c gate).** `scripts/perf-baseline.mjs` gains a `--song` mode that reports per-frame draw-cost p50/p95/p99 (draw-tagged via `highway.addDrawHook`), the metric that gates the `highway.js` split. Maintainer/CI-only; baseline recorded in `docs/perf-baseline.md`.
|
||||||
- **`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), `artist_aliases` (5), `loops` (3), `playlists` (12 + covers), `ws_highway` (the 902-line highway chart WebSocket), `chart` (split/unsplit/work/fileinfo — unblocked by the DLC-path substrate), `library_extras`, `wanted`, `shop`, `progression`, `profile`, `stats` (the `/api/stats/{path}` catch-all stays registered last so it can't shadow `/recent` `/best` `/top`), `version` (`/api/version`; VERSION-file lookup adjusted for the router subdir depth), and `diagnostics` (`/api/diagnostics/export|preview|hardware`; the plugins-root lookup adjusted for the router subdir depth, `_running_version` reached through the `appstate` seam, pure payload-cap helpers re-exported for the `server._diag_*` tests). The DLC library-path resolution (`_get_dlc_dir`, pure `_resolve_dlc_path`) moved to `lib/dlc_paths.py`, reading paths through the seam; `config_dir`/`dlc_dir`/`dlc_dir_env` now ride the `appstate` seam (env-derived, so the pop-and-reimport fixtures reconfigure it for free), and the shared request-field sanitizer `_clean_str` moved to `lib/reqfields.py`. 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/` — 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), `artist_aliases` (5), `loops` (3), `playlists` (12 + covers), `ws_highway` (the 902-line highway chart WebSocket), `chart` (split/unsplit/work/fileinfo — unblocked by the DLC-path substrate), `library_extras`, `wanted`, `shop`, `progression`, `profile`, `stats` (the `/api/stats/{path}` catch-all stays registered last so it can't shadow `/recent` `/best` `/top`), `version` (`/api/version`; VERSION-file lookup adjusted for the router subdir depth), and `diagnostics` (`/api/diagnostics/export|preview|hardware`; the plugins-root lookup adjusted for the router subdir depth, `_running_version` reached through the `appstate` seam, pure payload-cap helpers re-exported for the `server._diag_*` tests), and `tunings` (`/api/tunings`; the pure `config.json` reader moved to `lib/appconfig.py`, the tuning-provider registry read through the `appstate` seam so plugin-contributed tunings still merge). The DLC library-path resolution (`_get_dlc_dir`, pure `_resolve_dlc_path`) moved to `lib/dlc_paths.py`, reading paths through the seam; `config_dir`/`dlc_dir`/`dlc_dir_env` now ride the `appstate` seam (env-derived, so the pop-and-reimport fixtures reconfigure it for free), and the shared request-field sanitizer `_clean_str` moved to `lib/reqfields.py`. 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
|
- **`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
|
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
|
`fastapi.APIRouter`, mounted with `app.include_router(...)` **at the point in the 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`
|
||||||
(6,960 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
|
(6,917 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
|
||||||
extractions and fourteen `routers/` modules) ·
|
extractions and fifteen `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`
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Reading the app's config.json — the one shared, pure helper (R3).
|
||||||
|
|
||||||
|
Extracted verbatim from server.py so route modules that need a config value
|
||||||
|
(reference pitch, server_config, …) can read it without reaching back into the
|
||||||
|
host file. server.py re-imports it, so its ~11 call sites and any
|
||||||
|
`server._load_config` test reference keep resolving unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config(config_file):
|
||||||
|
"""Read and parse config.json. Returns the parsed dict, or None if
|
||||||
|
the file is missing, unreadable, invalid JSON, or parses to a
|
||||||
|
non-dict (e.g. the file contains `[]` or `42`). Callers treat None
|
||||||
|
as "fall back to defaults". Shared between GET and POST so both
|
||||||
|
handle bad files the same way."""
|
||||||
|
if not config_file.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
# Explicit UTF-8: save_settings()/import write config.json as
|
||||||
|
# UTF-8 bytes, so the read must not depend on the platform's
|
||||||
|
# default text encoding (cp1252 on Windows would mojibake or
|
||||||
|
# UnicodeDecodeError on a non-ASCII DLC path).
|
||||||
|
parsed = json.loads(config_file.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
+5
-1
@@ -61,6 +61,10 @@ copies a hardcoded file list — that regression is what moved this file here.
|
|||||||
# The singletons routers may read. Every name here must also be a `_SLOTS` key.
|
# The singletons routers may read. Every name here must also be a `_SLOTS` key.
|
||||||
meta_db = None
|
meta_db = None
|
||||||
audio_effect_mappings = None
|
audio_effect_mappings = None
|
||||||
|
# The tuning-provider registry instance (built-ins + plugin-contributed). A
|
||||||
|
# stable object mutated in place via register()/unregister() — injected here by
|
||||||
|
# reference so routers read the same registry plugins populate.
|
||||||
|
tuning_providers = None
|
||||||
|
|
||||||
# Config paths. server.py derives these from the environment (fresh on every
|
# Config paths. server.py derives these from the environment (fresh on every
|
||||||
# import, so the ~49 pop-and-reimport fixtures keep working) and injects them
|
# import, so the ~49 pop-and-reimport fixtures keep working) and injects them
|
||||||
@@ -90,7 +94,7 @@ builtin_diagnostic_filename = None
|
|||||||
running_version = None
|
running_version = None
|
||||||
|
|
||||||
_SLOTS = frozenset({
|
_SLOTS = frozenset({
|
||||||
"meta_db", "audio_effect_mappings",
|
"meta_db", "audio_effect_mappings", "tuning_providers",
|
||||||
"config_dir", "dlc_dir", "dlc_dir_env",
|
"config_dir", "dlc_dir", "dlc_dir_env",
|
||||||
"static_dir", "sloppak_cache_dir", "audio_cache_dir",
|
"static_dir", "sloppak_cache_dir", "audio_cache_dir",
|
||||||
"get_progression_content", "builtin_diagnostic_filename",
|
"get_progression_content", "builtin_diagnostic_filename",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""The merged tuning catalog (/api/tunings).
|
||||||
|
|
||||||
|
Extracted verbatim from server.py (R3) except @app->@router, CONFIG_DIR->
|
||||||
|
appstate.config_dir, _load_config imported from lib/appconfig, and the tuning
|
||||||
|
registry read through the appstate seam (appstate.tuning_providers — the same
|
||||||
|
instance plugins register into via the plugin_context in server.py).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
import appstate
|
||||||
|
from appconfig import _load_config
|
||||||
|
from tunings import DEFAULT_REFERENCE_PITCH, TUNING_PRESET_MIDIS, freqs_to_midis
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tunings")
|
||||||
|
def get_tunings():
|
||||||
|
cfg = _load_config(appstate.config_dir / "config.json") or {}
|
||||||
|
ref = cfg.get("reference_pitch", DEFAULT_REFERENCE_PITCH)
|
||||||
|
try:
|
||||||
|
ref = float(ref)
|
||||||
|
if not (430.0 <= ref <= 450.0):
|
||||||
|
ref = DEFAULT_REFERENCE_PITCH
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
ref = DEFAULT_REFERENCE_PITCH
|
||||||
|
merged = appstate.tuning_providers.get_merged(ref)
|
||||||
|
# tuningMidis: the same catalog as exact integer MIDI notes (low → high).
|
||||||
|
# Built-ins come straight from TUNING_PRESET_MIDIS (no float round-trip);
|
||||||
|
# provider-contributed entries are recovered from their frequencies at the
|
||||||
|
# served reference pitch. Every consumer today (the v3 badges, plugins)
|
||||||
|
# reconstructs midis client-side via log2 — a rounding footgun at non-440
|
||||||
|
# references — so serve the integers once, host-side. Additive: the
|
||||||
|
# existing referencePitch/tunings shape is unchanged.
|
||||||
|
tuning_midis: dict[str, dict[str, list[int]]] = {}
|
||||||
|
for key, names in merged.items():
|
||||||
|
builtin = TUNING_PRESET_MIDIS.get(key, {})
|
||||||
|
resolved: dict[str, list[int]] = {}
|
||||||
|
for name, freqs in names.items():
|
||||||
|
midis = builtin.get(name) or freqs_to_midis(freqs, ref)
|
||||||
|
if midis:
|
||||||
|
resolved[name] = list(midis)
|
||||||
|
if resolved:
|
||||||
|
tuning_midis[key] = resolved
|
||||||
|
return {"referencePitch": ref, "tunings": merged, "tuningMidis": tuning_midis}
|
||||||
@@ -26,10 +26,11 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
|
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
|
||||||
|
|
||||||
from safepath import safe_join
|
from safepath import safe_join
|
||||||
|
from appconfig import _load_config
|
||||||
from tunings import (
|
from tunings import (
|
||||||
DEFAULT_REFERENCE_PITCH, DEFAULT_TUNINGS, PROFILE_IDS, PROFILE_PATHWAYS,
|
DEFAULT_REFERENCE_PITCH, DEFAULT_TUNINGS, PROFILE_IDS, PROFILE_PATHWAYS,
|
||||||
TUNING_PRESET_MIDIS, apply_flat_instrument_patch_to_profiles,
|
apply_flat_instrument_patch_to_profiles,
|
||||||
apply_reference_pitch, freqs_to_midis, normalize_instrument_profile,
|
apply_reference_pitch, normalize_instrument_profile,
|
||||||
normalize_instrument_profiles, settings_with_instrument_profiles,
|
normalize_instrument_profiles, settings_with_instrument_profiles,
|
||||||
tuning_name,
|
tuning_name,
|
||||||
)
|
)
|
||||||
@@ -56,6 +57,7 @@ from dlc_paths import _get_dlc_dir, _resolve_dlc_path
|
|||||||
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, library_extras, shop, progression, profile, stats, version, diagnostics
|
from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras, shop, progression, profile, stats, version, diagnostics
|
||||||
|
from routers import tunings as tunings_router
|
||||||
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/
|
||||||
@@ -1171,6 +1173,7 @@ def _get_progression_content() -> dict:
|
|||||||
appstate.configure(
|
appstate.configure(
|
||||||
get_progression_content=_get_progression_content,
|
get_progression_content=_get_progression_content,
|
||||||
builtin_diagnostic_filename=_builtin_diagnostic_filename,
|
builtin_diagnostic_filename=_builtin_diagnostic_filename,
|
||||||
|
tuning_providers=tuning_providers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -4816,54 +4819,8 @@ def _default_settings():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _load_config(config_file):
|
# GET /api/tunings → routers/tunings.py (R3, reads config + appstate.tuning_providers)
|
||||||
"""Read and parse config.json. Returns the parsed dict, or None if
|
app.include_router(tunings_router.router)
|
||||||
the file is missing, unreadable, invalid JSON, or parses to a
|
|
||||||
non-dict (e.g. the file contains `[]` or `42`). Callers treat None
|
|
||||||
as "fall back to defaults". Shared between GET and POST so both
|
|
||||||
handle bad files the same way."""
|
|
||||||
if not config_file.exists():
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
# Explicit UTF-8: save_settings()/import write config.json as
|
|
||||||
# UTF-8 bytes, so the read must not depend on the platform's
|
|
||||||
# default text encoding (cp1252 on Windows would mojibake or
|
|
||||||
# UnicodeDecodeError on a non-ASCII DLC path).
|
|
||||||
parsed = json.loads(config_file.read_text(encoding="utf-8"))
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return parsed if isinstance(parsed, dict) else None
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/tunings")
|
|
||||||
def get_tunings():
|
|
||||||
cfg = _load_config(CONFIG_DIR / "config.json") or {}
|
|
||||||
ref = cfg.get("reference_pitch", DEFAULT_REFERENCE_PITCH)
|
|
||||||
try:
|
|
||||||
ref = float(ref)
|
|
||||||
if not (430.0 <= ref <= 450.0):
|
|
||||||
ref = DEFAULT_REFERENCE_PITCH
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
ref = DEFAULT_REFERENCE_PITCH
|
|
||||||
merged = tuning_providers.get_merged(ref)
|
|
||||||
# tuningMidis: the same catalog as exact integer MIDI notes (low → high).
|
|
||||||
# Built-ins come straight from TUNING_PRESET_MIDIS (no float round-trip);
|
|
||||||
# provider-contributed entries are recovered from their frequencies at the
|
|
||||||
# served reference pitch. Every consumer today (the v3 badges, plugins)
|
|
||||||
# reconstructs midis client-side via log2 — a rounding footgun at non-440
|
|
||||||
# references — so serve the integers once, host-side. Additive: the
|
|
||||||
# existing referencePitch/tunings shape is unchanged.
|
|
||||||
tuning_midis: dict[str, dict[str, list[int]]] = {}
|
|
||||||
for key, names in merged.items():
|
|
||||||
builtin = TUNING_PRESET_MIDIS.get(key, {})
|
|
||||||
resolved: dict[str, list[int]] = {}
|
|
||||||
for name, freqs in names.items():
|
|
||||||
midis = builtin.get(name) or freqs_to_midis(freqs, ref)
|
|
||||||
if midis:
|
|
||||||
resolved[name] = list(midis)
|
|
||||||
if resolved:
|
|
||||||
tuning_midis[key] = resolved
|
|
||||||
return {"referencePitch": ref, "tunings": merged, "tuningMidis": tuning_midis}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/settings")
|
@app.get("/api/settings")
|
||||||
|
|||||||
Reference in New Issue
Block a user