bulid 63
This commit is contained in:
526
hockey_data/vmix_portable.py
Normal file
526
hockey_data/vmix_portable.py
Normal file
@@ -0,0 +1,526 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, TYPE_CHECKING
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from broadcast_settings import settings as settings_lib
|
||||
from broadcast_settings.joins import flatten_dict
|
||||
from broadcast_settings.transform import apply_configured_json
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .service import HockeyDataService
|
||||
|
||||
SUPPORTED_SOURCE_TYPES = {
|
||||
"info",
|
||||
"scoreboard",
|
||||
"home_lineup",
|
||||
"away_lineup",
|
||||
"officials",
|
||||
"penalties",
|
||||
"events",
|
||||
"full_match",
|
||||
}
|
||||
|
||||
SOURCE_LABELS = {
|
||||
"info": "Информация о матче",
|
||||
"scoreboard": "Верхний счёт",
|
||||
"home_lineup": "Состав хозяев",
|
||||
"away_lineup": "Состав гостей",
|
||||
"officials": "Судьи",
|
||||
"penalties": "Активные удаления",
|
||||
"events": "События матча",
|
||||
"full_match": "Полный матч",
|
||||
}
|
||||
|
||||
|
||||
def _read_json(path: Path, fallback: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, dict) else deepcopy(fallback)
|
||||
except Exception:
|
||||
return deepcopy(fallback)
|
||||
|
||||
|
||||
def configure_hockey_vmix_settings(base_dir: Path) -> None:
|
||||
default_settings = {
|
||||
"version": 1,
|
||||
"active_vmix_json": "scoreboard",
|
||||
"selection": {},
|
||||
}
|
||||
default_vmix_json = _read_json(
|
||||
base_dir / "hockey_data" / "default_vmix_json.json",
|
||||
{"version": 1, "configs": []},
|
||||
)
|
||||
default_vmix_functions = _read_json(
|
||||
base_dir / "hockey_data" / "default_vmix_functions.json",
|
||||
{"version": 1, "categories": []},
|
||||
)
|
||||
settings_lib.configure(
|
||||
project_dir=base_dir,
|
||||
app_dir=base_dir,
|
||||
settings_dir=base_dir / "settings",
|
||||
settings_file="vmix_settings.json",
|
||||
vmix_json_file="vmix_json.json",
|
||||
vmix_functions_file="vmix_functions.json",
|
||||
default_settings=default_settings,
|
||||
default_vmix_json=default_vmix_json,
|
||||
default_vmix_functions=default_vmix_functions,
|
||||
)
|
||||
settings_lib.ensure_settings_files()
|
||||
# Existing projects may have an older config file. Normalize it on startup.
|
||||
settings_lib.write_vmix_json(normalize_hockey_vmix_json(settings_lib.read_vmix_json()))
|
||||
|
||||
|
||||
def _safe_key(value: Any, fallback: str = "json") -> str:
|
||||
text = re.sub(r"[^A-Za-z0-9_]+", "_", str(value or "").strip()).strip("_")
|
||||
return text or fallback
|
||||
|
||||
|
||||
def endpoint_for_key(key: str) -> str:
|
||||
return f"/vmix/hockey/channel/{{channel}}/{{language}}/custom/{_safe_key(key)}.json"
|
||||
|
||||
|
||||
def normalize_hockey_vmix_json(data: Any) -> dict[str, Any]:
|
||||
source = data if isinstance(data, dict) else {}
|
||||
configs = source.get("configs") if isinstance(source.get("configs"), list) else []
|
||||
normalized: list[dict[str, Any]] = []
|
||||
used: set[str] = set()
|
||||
for index, raw in enumerate(configs, start=1):
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
cfg = deepcopy(raw)
|
||||
base_key = _safe_key(cfg.get("key"), f"json_{index}")
|
||||
key = base_key
|
||||
suffix = 2
|
||||
while key in used:
|
||||
key = f"{base_key}_{suffix}"
|
||||
suffix += 1
|
||||
used.add(key)
|
||||
source_type = str(cfg.get("source_type") or "scoreboard").strip().lower()
|
||||
if source_type not in SUPPORTED_SOURCE_TYPES:
|
||||
cfg.setdefault("imported_source_type", source_type)
|
||||
source_type = "scoreboard"
|
||||
cfg.update(
|
||||
{
|
||||
"key": key,
|
||||
"title": str(cfg.get("title") or key),
|
||||
"description": str(cfg.get("description") or ""),
|
||||
"endpoint": endpoint_for_key(key),
|
||||
"source_type": source_type,
|
||||
"root_key": str(cfg.get("root_key") or "rows"),
|
||||
"output_mode": "all" if str(cfg.get("output_mode") or "columns").lower() == "all" else "columns",
|
||||
"default_limit": max(0, int(cfg.get("default_limit") or 0)),
|
||||
"enabled": cfg.get("enabled", True) is not False,
|
||||
"columns": cfg.get("columns") if isinstance(cfg.get("columns"), list) else [],
|
||||
"joins": cfg.get("joins") if isinstance(cfg.get("joins"), list) else [],
|
||||
}
|
||||
)
|
||||
normalized.append(cfg)
|
||||
return {"version": int(source.get("version") or 1), "configs": normalized}
|
||||
|
||||
|
||||
class HockeyVmixPortable:
|
||||
def __init__(self, service: "HockeyDataService") -> None:
|
||||
self.service = service
|
||||
|
||||
@staticmethod
|
||||
def _data_language(language: str) -> str:
|
||||
return "en" if language == "en" else "ru"
|
||||
|
||||
def session_or_error(self, token: str) -> dict[str, Any]:
|
||||
session = self.service.get_session(str(token or "").strip())
|
||||
if session is None:
|
||||
raise KeyError("Сессия не найдена")
|
||||
return session
|
||||
|
||||
def _context(self, token: str, language: str) -> dict[str, Any]:
|
||||
session = self.session_or_error(token)
|
||||
data_language = self._data_language(language)
|
||||
game_id = str(session.get("game_external_id") or "")
|
||||
tournament_id = str(session.get("tournament_external_id") or "")
|
||||
details = self.service.game_details(game_id, language=data_language) if game_id else None
|
||||
tournament = self.service.tournament(tournament_id, language=data_language) if tournament_id else None
|
||||
control = self.service.game_control(game_id, language=data_language) if game_id else None
|
||||
return {
|
||||
"session": session,
|
||||
"language": language,
|
||||
"data_language": data_language,
|
||||
"details": details or {},
|
||||
"tournament": tournament or {},
|
||||
"control": control or {},
|
||||
"game": (details or {}).get("game") or {},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _timer_text(control: dict[str, Any]) -> tuple[str, dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
|
||||
shootout = control.get("shootout") or {}
|
||||
timer_rules = control.get("timer_rules") or {}
|
||||
strength = control.get("strength") or {}
|
||||
timer_state = control.get("timers") or {}
|
||||
main_timer = timer_state.get("main_timer") or {}
|
||||
current_ms = max(
|
||||
0,
|
||||
int(float(main_timer.get("current_ms", timer_rules.get("duration_ms", 20 * 60 * 1000)) or 0)),
|
||||
)
|
||||
total_seconds = current_ms // 1000
|
||||
return (
|
||||
f"{total_seconds // 60:02d}:{total_seconds % 60:02d}",
|
||||
shootout,
|
||||
timer_rules,
|
||||
strength,
|
||||
timer_state,
|
||||
)
|
||||
|
||||
def info_payload(self, token: str, language: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
game = ctx["game"]
|
||||
return {
|
||||
"session_token": token, # legacy field
|
||||
"channel_key": token,
|
||||
"language": language,
|
||||
"tournament": ctx["tournament"],
|
||||
"game": game,
|
||||
"game_id": ctx["session"].get("game_external_id", ""),
|
||||
"home_team": (game.get("home") or {}).get("name", ""),
|
||||
"away_team": (game.get("away") or {}).get("name", ""),
|
||||
"arena": game.get("arena", ""),
|
||||
"date": game.get("date", ""),
|
||||
"start_time": game.get("time", ""),
|
||||
"referees": ctx["details"].get("referees", []),
|
||||
"control": ctx["control"],
|
||||
}
|
||||
|
||||
def scoreboard_payload(self, token: str, language: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
game = ctx["game"]
|
||||
control = ctx["control"]
|
||||
timer_text, shootout, timer_rules, strength, timer_state = self._timer_text(control)
|
||||
main_timer = timer_state.get("main_timer") or {}
|
||||
flags = control.get("flags") or {}
|
||||
return {
|
||||
"session_token": token, # legacy field
|
||||
"channel_key": token,
|
||||
"language": language,
|
||||
"game_id": ctx["session"].get("game_external_id", ""),
|
||||
"home_score": (game.get("home") or {}).get("score", 0),
|
||||
"away_score": (game.get("away") or {}).get("score", 0),
|
||||
"period": control.get("current_period", "1"),
|
||||
"time": timer_text,
|
||||
"timer_running": bool(main_timer.get("running", False)),
|
||||
"timer_paused": bool(main_timer.get("paused", True)),
|
||||
"timer_default_minutes": timer_rules.get("minutes", 20),
|
||||
"timer_phase": timer_rules.get("phase", "regulation"),
|
||||
"strength": strength.get("strength_label", ""),
|
||||
"strength_phase": strength.get("phase", "regulation"),
|
||||
"strength_advantage_side": strength.get("advantage_side", ""),
|
||||
"strength_state_key": strength.get("state_key", ""),
|
||||
"strength_state_label": strength.get("state_label", ""),
|
||||
"home_strength": strength.get("home_skaters", 0),
|
||||
"away_strength": strength.get("away_skaters", 0),
|
||||
"home_powerplay_label": strength.get("home_label", ""),
|
||||
"away_powerplay_label": strength.get("away_label", ""),
|
||||
"home_delayed_penalty": bool(flags.get("home_delayed_penalty", False)),
|
||||
"away_delayed_penalty": bool(flags.get("away_delayed_penalty", False)),
|
||||
"home_empty_net": bool(flags.get("home_empty_net", False)),
|
||||
"away_empty_net": bool(flags.get("away_empty_net", False)),
|
||||
"status": game.get("status", "not_loaded"),
|
||||
"shootout_home": (shootout.get("home") or {}).get("goals", 0),
|
||||
"shootout_away": (shootout.get("away") or {}).get("goals", 0),
|
||||
"shootout_attempts": shootout.get("attempts", []),
|
||||
}
|
||||
|
||||
def line_up_payload(self, token: str, language: str, side: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
items = ((ctx["game"].get(side) or {}).get("players") or [])
|
||||
return {"session_token": token, "channel_key": token, "language": language, "items": items}
|
||||
|
||||
def officials_payload(self, token: str, language: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
return {"session_token": token, "channel_key": token, "language": language, "items": ctx["details"].get("referees", [])}
|
||||
|
||||
def penalties_payload(self, token: str, language: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
timer_state = (ctx["control"].get("timers") or {})
|
||||
board = timer_state.get("penalty_board") or {}
|
||||
raw_items = board.get("penalties")
|
||||
items = [item for item in raw_items if isinstance(item, dict)] if isinstance(raw_items, list) else []
|
||||
def _remaining(item: dict[str, Any]) -> float:
|
||||
try:
|
||||
return float(item.get("remainingMs", item.get("remaining_ms", 0)) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
home_items = sorted(
|
||||
[item for item in items if str(item.get("side") or (item.get("player") or {}).get("side") or "").lower() == "home"],
|
||||
key=lambda item: (_remaining(item), float(item.get("createdAt", 0) or 0)),
|
||||
)
|
||||
away_items = sorted(
|
||||
[item for item in items if str(item.get("side") or (item.get("player") or {}).get("side") or "").lower() == "away"],
|
||||
key=lambda item: (_remaining(item), float(item.get("createdAt", 0) or 0)),
|
||||
)
|
||||
return {
|
||||
"session_token": token, # legacy field
|
||||
"channel_key": token,
|
||||
"language": language,
|
||||
"game_id": ctx["session"].get("game_external_id", ""),
|
||||
"home": home_items,
|
||||
"away": away_items,
|
||||
"home_next": home_items[0] if home_items else {},
|
||||
"away_next": away_items[0] if away_items else {},
|
||||
"items": items,
|
||||
"strength": ctx["control"].get("strength") or {},
|
||||
"timer_rules": ctx["control"].get("timer_rules") or {},
|
||||
"updated_at": timer_state.get("updated_at", ""),
|
||||
}
|
||||
|
||||
def events_payload(self, token: str, language: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
details = ctx["details"]
|
||||
items = details.get("events")
|
||||
if not isinstance(items, list):
|
||||
items = details.get("play_by_play")
|
||||
if not isinstance(items, list):
|
||||
items = []
|
||||
return {"session_token": token, "channel_key": token, "language": language, "items": items}
|
||||
|
||||
def full_match_payload(self, token: str, language: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context or self._context(token, language)
|
||||
return {
|
||||
"session": ctx["session"],
|
||||
"language": language,
|
||||
"tournament": ctx["tournament"],
|
||||
"details": ctx["details"],
|
||||
"control": ctx["control"],
|
||||
"scoreboard": self.scoreboard_payload(token, language, context=ctx),
|
||||
}
|
||||
|
||||
def source_payload(self, token: str, language: str, source_type: str, context: dict[str, Any] | None = None) -> Any:
|
||||
source_type = str(source_type or "scoreboard").strip().lower()
|
||||
if source_type not in SUPPORTED_SOURCE_TYPES:
|
||||
raise ValueError(f"Неизвестный хоккейный источник: {source_type}")
|
||||
ctx = context or self._context(token, language)
|
||||
if source_type == "info":
|
||||
return self.info_payload(token, language, context=ctx)
|
||||
if source_type == "scoreboard":
|
||||
return self.scoreboard_payload(token, language, context=ctx)
|
||||
if source_type == "home_lineup":
|
||||
return self.line_up_payload(token, language, "home", context=ctx)
|
||||
if source_type == "away_lineup":
|
||||
return self.line_up_payload(token, language, "away", context=ctx)
|
||||
if source_type == "officials":
|
||||
return self.officials_payload(token, language, context=ctx)
|
||||
if source_type == "penalties":
|
||||
return self.penalties_payload(token, language, context=ctx)
|
||||
if source_type == "events":
|
||||
return self.events_payload(token, language, context=ctx)
|
||||
return self.full_match_payload(token, language, context=ctx)
|
||||
|
||||
def source_rows(self, token: str, language: str, source_type: str, context: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
||||
payload = self.source_payload(token, language, source_type, context=context)
|
||||
if isinstance(payload, list):
|
||||
return [item for item in payload if isinstance(item, dict)]
|
||||
if not isinstance(payload, dict):
|
||||
return [{"value": payload}]
|
||||
if source_type in {"home_lineup", "away_lineup", "officials", "penalties", "events"}:
|
||||
items = payload.get("items")
|
||||
if isinstance(items, list):
|
||||
return [item for item in items if isinstance(item, dict)]
|
||||
return [payload]
|
||||
|
||||
def source_fields(self, token: str, language: str, source_type: str) -> dict[str, Any]:
|
||||
rows = self.source_rows(token, language, source_type)
|
||||
keys: list[str] = []
|
||||
for row in rows[:30]:
|
||||
for key, value in flatten_dict(row).items():
|
||||
if isinstance(value, (dict, list)):
|
||||
continue
|
||||
key = str(key)
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
return {"ok": True, "source_type": source_type, "count": len(rows), "fields": keys}
|
||||
|
||||
def configured_output(self, token: str, language: str, json_key: str, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
config = settings_lib.get_vmix_config(json_key)
|
||||
context = self._context(token, language)
|
||||
cache: dict[str, list[dict[str, Any]]] = {}
|
||||
|
||||
def rows_for(source_type: str) -> list[dict[str, Any]]:
|
||||
source_type = str(source_type or "scoreboard").strip().lower()
|
||||
if source_type not in cache:
|
||||
cache[source_type] = self.source_rows(token, language, source_type, context=context)
|
||||
return deepcopy(cache[source_type])
|
||||
|
||||
def source_resolver(join: dict[str, Any]) -> Any:
|
||||
source_type = str(
|
||||
join.get("source_type")
|
||||
or join.get("source")
|
||||
or join.get("type")
|
||||
or "scoreboard"
|
||||
).strip().lower()
|
||||
return rows_for(source_type)
|
||||
|
||||
rows = rows_for(str(config.get("source_type") or "scoreboard"))
|
||||
return apply_configured_json(rows, config, limit=limit, source_resolver=source_resolver)
|
||||
|
||||
|
||||
def _extract_import_json_from_zip(file_name: str, content: bytes) -> tuple[dict[str, Any], list[str]]:
|
||||
try:
|
||||
archive = zipfile.ZipFile(io.BytesIO(content))
|
||||
except zipfile.BadZipFile as exc:
|
||||
raise ValueError("ZIP-файл не открывается или повреждён") from exc
|
||||
names = [name for name in archive.namelist() if not name.endswith("/")]
|
||||
json_names = [name for name in names if name.lower().endswith(".json")]
|
||||
if not json_names:
|
||||
raise ValueError("В ZIP не найдено JSON-файлов с настройками")
|
||||
|
||||
def load_member(name: str) -> Any:
|
||||
with archive.open(name) as file:
|
||||
return json.loads(file.read().decode("utf-8-sig"))
|
||||
|
||||
preferred_vmix = next(
|
||||
(name for name in json_names if name.lower().replace("\\", "/").endswith("settings/vmix_json.json")),
|
||||
None,
|
||||
) or next((name for name in json_names if name.lower().endswith("vmix_json.json")), None)
|
||||
preferred_settings = next(
|
||||
(name for name in json_names if name.lower().replace("\\", "/").endswith("settings/settings.json")),
|
||||
None,
|
||||
) or next((name for name in json_names if name.lower().endswith("settings.json")), None)
|
||||
result: dict[str, Any] = {"_import_archive": file_name}
|
||||
used: list[str] = []
|
||||
if preferred_settings:
|
||||
result["settings"] = load_member(preferred_settings)
|
||||
used.append(preferred_settings)
|
||||
if preferred_vmix:
|
||||
result["vmix_json"] = load_member(preferred_vmix)
|
||||
if isinstance(result["vmix_json"], dict):
|
||||
result["configs"] = result["vmix_json"].get("configs")
|
||||
used.append(preferred_vmix)
|
||||
if used:
|
||||
result["_import_used_files"] = used
|
||||
return result, used
|
||||
files: dict[str, Any] = {}
|
||||
for name in json_names[:50]:
|
||||
lowered = name.lower()
|
||||
if any(part in lowered for part in ("cache/", "__pycache__", "response_", "scores")):
|
||||
continue
|
||||
try:
|
||||
files[name] = load_member(name)
|
||||
used.append(name)
|
||||
except Exception:
|
||||
continue
|
||||
if not files:
|
||||
raise ValueError("JSON-файлы найдены, но настройки прочитать не удалось")
|
||||
return {"_import_archive": file_name, "files": files, "_import_used_files": used}, used
|
||||
|
||||
|
||||
def create_hockey_vmix_settings_router(
|
||||
portable: HockeyVmixPortable,
|
||||
*,
|
||||
admin_dependency: Callable[..., Any],
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
prefix="/api/hockey/vmix",
|
||||
tags=["Hockey vMix settings"],
|
||||
dependencies=[Depends(admin_dependency)],
|
||||
)
|
||||
|
||||
@router.get("/settings")
|
||||
async def get_settings() -> dict[str, Any]:
|
||||
return {
|
||||
"settings": settings_lib.read_settings(),
|
||||
"vmix_json": normalize_hockey_vmix_json(settings_lib.read_vmix_json()),
|
||||
"vmix_functions": settings_lib.read_vmix_functions(),
|
||||
"source_types": [
|
||||
{"value": key, "label": SOURCE_LABELS[key]}
|
||||
for key in SOURCE_LABELS
|
||||
],
|
||||
"meta": settings_lib.settings_meta(),
|
||||
}
|
||||
|
||||
@router.put("/configs")
|
||||
async def save_configs(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
raw = payload.get("vmix_json", payload)
|
||||
if not isinstance(raw, dict):
|
||||
raise HTTPException(status_code=400, detail="vmix_json должен быть объектом")
|
||||
normalized = normalize_hockey_vmix_json(raw)
|
||||
return {"vmix_json": settings_lib.write_vmix_json(normalized), "meta": settings_lib.settings_meta()}
|
||||
|
||||
@router.get("/source-fields")
|
||||
async def source_fields(
|
||||
token: str = Query(..., min_length=4),
|
||||
language: str = Query("ru", pattern="^(ru|en|bilingual)$"),
|
||||
source_type: str = Query("scoreboard"),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return portable.source_fields(token, language, source_type)
|
||||
except (KeyError, ValueError) as exc:
|
||||
raise HTTPException(status_code=404 if isinstance(exc, KeyError) else 400, detail=str(exc)) from exc
|
||||
|
||||
@router.get("/preview/{json_key}")
|
||||
async def preview(
|
||||
json_key: str,
|
||||
token: str = Query(..., min_length=4),
|
||||
language: str = Query("ru", pattern="^(ru|en|bilingual)$"),
|
||||
limit: int | None = Query(None, ge=0, le=1000),
|
||||
) -> JSONResponse:
|
||||
try:
|
||||
return JSONResponse(portable.configured_output(token, language, json_key, limit=limit))
|
||||
except (KeyError, ValueError) as exc:
|
||||
raise HTTPException(status_code=404 if isinstance(exc, KeyError) else 400, detail=str(exc)) from exc
|
||||
|
||||
@router.get("/export")
|
||||
async def export_settings() -> dict[str, Any]:
|
||||
return {
|
||||
"format": "hockey-vmix-settings",
|
||||
"version": 1,
|
||||
"vmix_json": normalize_hockey_vmix_json(settings_lib.read_vmix_json()),
|
||||
}
|
||||
|
||||
@router.post("/import/preview-file")
|
||||
async def import_preview_file(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
file_name = str(payload.get("file_name") or payload.get("name") or "settings.json")
|
||||
content_b64 = payload.get("content_base64") or payload.get("base64")
|
||||
if not content_b64:
|
||||
raise HTTPException(status_code=400, detail="Нужно передать файл в content_base64")
|
||||
try:
|
||||
content = base64.b64decode(str(content_b64), validate=False)
|
||||
if file_name.lower().endswith(".zip"):
|
||||
data, _ = _extract_import_json_from_zip(file_name, content)
|
||||
else:
|
||||
data = json.loads(content.decode("utf-8-sig"))
|
||||
preview_value = settings_lib.preview_settings_import(data, file_name=file_name)
|
||||
preview_value["raw_import"] = data
|
||||
return preview_value
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=f"Не удалось прочитать настройки: {exc}") from exc
|
||||
|
||||
@router.post("/import/apply")
|
||||
async def import_apply(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
preview_value = payload.get("preview") or payload
|
||||
if not isinstance(preview_value, dict):
|
||||
raise HTTPException(status_code=400, detail="Нужно передать preview")
|
||||
try:
|
||||
result = settings_lib.apply_settings_import(
|
||||
preview=preview_value,
|
||||
selected_indexes=payload.get("selected_indexes") if isinstance(payload.get("selected_indexes"), list) else None,
|
||||
import_general=False,
|
||||
import_vmix=True,
|
||||
mode=str(payload.get("mode") or "add_new"),
|
||||
)
|
||||
normalized = normalize_hockey_vmix_json(result.get("vmix_json") or settings_lib.read_vmix_json())
|
||||
saved = settings_lib.write_vmix_json(normalized)
|
||||
return {**result, "vmix_json": saved, "meta": settings_lib.settings_meta()}
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
return router
|
||||
Reference in New Issue
Block a user