mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
157 lines
6.1 KiB
Python
157 lines
6.1 KiB
Python
"""Tuner plugin — persist last selected tuning and custom tunings in config_dir."""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import Response
|
|
|
|
DEFAULT_TUNING = "Standard"
|
|
DEFAULT_INSTRUMENT = "guitar-6"
|
|
|
|
_INSTRUMENT_BY_STRING_COUNT = {4: "bass-4", 5: "bass-5", 7: "guitar-7", 8: "guitar-8"}
|
|
|
|
|
|
def _migrate_custom_tuning(name: str, value) -> dict:
|
|
"""Return {instrument, strings} for both old flat-list and new dict formats."""
|
|
if isinstance(value, list):
|
|
instrument = _INSTRUMENT_BY_STRING_COUNT.get(len(value), "guitar-6")
|
|
return {"instrument": instrument, "strings": value}
|
|
if isinstance(value, dict) and "strings" in value:
|
|
return value
|
|
return {"instrument": "guitar-6", "strings": []}
|
|
|
|
|
|
def setup(app: FastAPI, context: dict):
|
|
config_dir = Path(context["config_dir"])
|
|
config_file = config_dir / "tuner.json"
|
|
log = context.get("log") or logging.getLogger("slopsmith.plugin.tuner")
|
|
|
|
def _read() -> dict:
|
|
defaults = {
|
|
"lastTuning": DEFAULT_TUNING,
|
|
"lastInstrument": DEFAULT_INSTRUMENT,
|
|
"freeTune": False,
|
|
"customTunings": {},
|
|
"disabledTunings": [],
|
|
"showFloatingButton": True,
|
|
"visualizationMode": "default",
|
|
"audioInputMode": "auto",
|
|
}
|
|
if not config_file.exists():
|
|
return defaults
|
|
try:
|
|
data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
if not isinstance(data, dict):
|
|
return defaults
|
|
|
|
res = {}
|
|
res["lastTuning"] = str(data.get("lastTuning", DEFAULT_TUNING))
|
|
res["lastInstrument"] = str(data.get("lastInstrument", DEFAULT_INSTRUMENT))
|
|
res["freeTune"] = bool(data.get("freeTune", False))
|
|
res["customTunings"] = data.get("customTunings", {})
|
|
res["disabledTunings"] = data.get("disabledTunings", [])
|
|
res["showFloatingButton"] = bool(data.get("showFloatingButton", True))
|
|
res["visualizationMode"] = str(data.get("visualizationMode", "default"))
|
|
raw_mode = str(data.get("audioInputMode", "auto"))
|
|
res["audioInputMode"] = raw_mode if raw_mode in ("auto", "browser") else "auto"
|
|
|
|
if not isinstance(res["customTunings"], dict):
|
|
res["customTunings"] = {}
|
|
if not isinstance(res["disabledTunings"], list):
|
|
res["disabledTunings"] = []
|
|
|
|
# Migrate custom tunings from old flat-list format
|
|
res["customTunings"] = {
|
|
name: _migrate_custom_tuning(name, val)
|
|
for name, val in res["customTunings"].items()
|
|
}
|
|
|
|
# Strip legacy disabledTunings entries that lack compound "instrument:name" format
|
|
res["disabledTunings"] = [
|
|
e for e in res["disabledTunings"]
|
|
if isinstance(e, str) and ":" in e
|
|
]
|
|
|
|
return res
|
|
except Exception:
|
|
return defaults
|
|
|
|
def _write(data: dict) -> None:
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
current = _read()
|
|
# Strip keys that belong to core, not to this plugin's config.
|
|
for key in ("defaultTunings", "referencePitch"):
|
|
data = {k: v for k, v in data.items() if k != key}
|
|
current.update(data)
|
|
config_file.write_text(json.dumps(current, indent=2), encoding="utf-8")
|
|
|
|
def _get_custom_tunings() -> dict:
|
|
"""Return custom tunings in DEFAULT_TUNINGS format: {instrument: {name: [freqs]}}."""
|
|
cfg = _read()
|
|
result: dict[str, dict] = {}
|
|
for name, val in cfg.get("customTunings", {}).items():
|
|
inst = val.get("instrument", "guitar-6")
|
|
strings = val.get("strings", [])
|
|
if strings:
|
|
result.setdefault(inst, {})[name] = strings
|
|
return result
|
|
|
|
# Register this plugin as a tuning provider for its custom tunings.
|
|
context["register_tuning_provider"]("tuner", _get_custom_tunings)
|
|
log.info("tuner: registered custom tuning provider")
|
|
|
|
_viz_dir = Path(__file__).parent / "visualization"
|
|
_viz_assets_dir = Path(__file__).parent / "visualization" / "assets"
|
|
_workers_dir = Path(__file__).parent / "workers"
|
|
_utils_dir = Path(__file__).parent / "utils"
|
|
|
|
_ASSET_MEDIA_TYPES = {".svg": "image/svg+xml", ".png": "image/png"}
|
|
|
|
def _serve_js_from(base_dir: Path, filename: str) -> Response:
|
|
target = (base_dir / filename).resolve()
|
|
try:
|
|
target.relative_to(base_dir.resolve())
|
|
except ValueError:
|
|
return Response("", status_code=404)
|
|
if target.suffix == ".js" and target.is_file():
|
|
return Response(target.read_text(encoding="utf-8"), media_type="application/javascript")
|
|
return Response("", status_code=404)
|
|
|
|
def _serve_asset_from(base_dir: Path, filename: str) -> Response:
|
|
target = (base_dir / filename).resolve()
|
|
try:
|
|
target.relative_to(base_dir.resolve())
|
|
except ValueError:
|
|
return Response("", status_code=404)
|
|
media_type = _ASSET_MEDIA_TYPES.get(target.suffix.lower())
|
|
if media_type and target.is_file():
|
|
return Response(target.read_bytes(), media_type=media_type)
|
|
return Response("", status_code=404)
|
|
|
|
@app.get("/api/plugins/tuner/visualization/{filename}")
|
|
def get_viz_file(filename: str):
|
|
return _serve_js_from(_viz_dir, filename)
|
|
|
|
@app.get("/api/plugins/tuner/viz-assets/{filename}")
|
|
def get_viz_asset(filename: str):
|
|
return _serve_asset_from(_viz_assets_dir, filename)
|
|
|
|
@app.get("/api/plugins/tuner/workers/{filename}")
|
|
def get_worker_file(filename: str):
|
|
return _serve_js_from(_workers_dir, filename)
|
|
|
|
@app.get("/api/plugins/tuner/utils/{filename}")
|
|
def get_utils_file(filename: str):
|
|
return _serve_js_from(_utils_dir, filename)
|
|
|
|
@app.get("/api/plugins/tuner/config")
|
|
def get_config():
|
|
return _read()
|
|
|
|
@app.post("/api/plugins/tuner/config")
|
|
async def set_config(req: Request):
|
|
body = await req.json()
|
|
_write(body)
|
|
return {"ok": True}
|