BUILD119 — ручной счёт + SQL + составы
This commit is contained in:
@@ -17,7 +17,7 @@ from sqlalchemy import and_, delete, desc, select
|
||||
from .auth_bridge import HockeyUser
|
||||
from .database import HockeyDatabase
|
||||
from .mapping_context import MappingDataService
|
||||
from .models import (MappingSqlDataSource, OperatorSession, UserPreference, VmixAssignment, VmixDevice, VmixMappingProfile, VmixMappingField, VmixPreparedTitle)
|
||||
from .models import (GameControlState, MappingSqlDataSource, OperatorSession, UserPreference, VmixAssignment, VmixDevice, VmixMappingProfile, VmixMappingField, VmixPreparedTitle)
|
||||
|
||||
|
||||
AGENT_PROTOCOL_VERSION = 1
|
||||
@@ -1106,6 +1106,53 @@ class VmixAgentHub:
|
||||
"agent_ack": ack,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _score_int(value: Any, fallback: int = 0) -> int:
|
||||
try:
|
||||
return max(0, int(float(str(value if value is not None else fallback).strip())))
|
||||
except (TypeError, ValueError):
|
||||
return max(0, int(fallback or 0))
|
||||
|
||||
def _score_source_settings(self) -> tuple[str, str]:
|
||||
try:
|
||||
settings = self.mapping_data.settings.public() if self.mapping_data.settings is not None else {}
|
||||
except Exception:
|
||||
settings = {}
|
||||
return (
|
||||
str(settings.get("score_external_home_key") or "").strip(),
|
||||
str(settings.get("score_external_away_key") or "").strip(),
|
||||
)
|
||||
|
||||
def _persist_score_external_snapshot(
|
||||
self,
|
||||
game_external_id: str,
|
||||
*,
|
||||
home: int,
|
||||
away: int,
|
||||
source: str,
|
||||
) -> None:
|
||||
game_external_id = str(game_external_id or "").strip()
|
||||
if not game_external_id:
|
||||
return
|
||||
with self.database.session() as session:
|
||||
row = session.scalar(select(GameControlState).where(GameControlState.game_external_id == game_external_id))
|
||||
if row is None:
|
||||
row = GameControlState(game_external_id=game_external_id)
|
||||
session.add(row)
|
||||
session.flush()
|
||||
try:
|
||||
values = json.loads(str(row.runtime_values_json or "{}"))
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
values = {}
|
||||
if not isinstance(values, dict):
|
||||
values = {}
|
||||
values["score.external_valid"] = "1"
|
||||
values["score.external_home"] = str(max(0, int(home)))
|
||||
values["score.external_away"] = str(max(0, int(away)))
|
||||
values["score.external_source"] = str(source or "game")[:160]
|
||||
row.runtime_values_json = json.dumps(values, ensure_ascii=False, separators=(",", ":"))
|
||||
row.updated_at = _utcnow()
|
||||
|
||||
async def apply_mapping_to_device(
|
||||
self,
|
||||
device_id: str,
|
||||
@@ -1200,6 +1247,12 @@ class VmixAgentHub:
|
||||
if mapping_language:
|
||||
supplied_context["ui_language"] = mapping_language
|
||||
catalog_source_codes = None if source_codes is None else {str(code) for code in source_codes}
|
||||
score_home_key, score_away_key = self._score_source_settings()
|
||||
if catalog_source_codes is not None and "score" in catalog_source_codes:
|
||||
for score_key in (score_home_key, score_away_key):
|
||||
dependency_code = self._mapping_source_code(score_key)
|
||||
if dependency_code and dependency_code not in {"score", "context"}:
|
||||
catalog_source_codes.add(dependency_code)
|
||||
if catalog_source_codes is not None:
|
||||
for field in fields:
|
||||
rule = _mapping_rule_payload(getattr(field, "rule_json", "{}"))
|
||||
@@ -1214,7 +1267,49 @@ class VmixAgentHub:
|
||||
if catalog_source_codes is not None
|
||||
else self.mapping_data.data_catalog(user, supplied_context)
|
||||
)
|
||||
by_key = {str(item.get("key") or ""): item for item in (catalog.get("items") or []) if item.get("key")}
|
||||
catalog_items = [item for item in (catalog.get("items") or []) if isinstance(item, dict) and item.get("key")]
|
||||
by_key = {str(item.get("key") or ""): item for item in catalog_items}
|
||||
# ``score`` is a natural name for an operator-created SQL source. Keep the
|
||||
# built-in LIVE score namespace authoritative even if such a source exists.
|
||||
for item in catalog_items:
|
||||
key = str(item.get("key") or "")
|
||||
if key.startswith("score.") and str(item.get("source_code") or "") == "live_control":
|
||||
by_key[key] = item
|
||||
|
||||
def external_score_item(key: str) -> dict[str, Any] | None:
|
||||
key = str(key or "").strip()
|
||||
if not key:
|
||||
return None
|
||||
candidates = [
|
||||
item for item in catalog_items
|
||||
if str(item.get("key") or "") == key and str(item.get("source_code") or "") != "live_control"
|
||||
]
|
||||
return candidates[-1] if candidates else (by_key.get(key) if not key.startswith("score.") else None)
|
||||
|
||||
score_snapshot = None
|
||||
if source_codes is None or "score" in {str(code) for code in source_codes}:
|
||||
fallback_home = self._score_int((by_key.get("score.external_home") or {}).get("value"), 0)
|
||||
fallback_away = self._score_int((by_key.get("score.external_away") or {}).get("value"), 0)
|
||||
home_item = external_score_item(score_home_key)
|
||||
away_item = external_score_item(score_away_key)
|
||||
resolved_home = self._score_int(home_item.get("value"), fallback_home) if isinstance(home_item, dict) else fallback_home
|
||||
resolved_away = self._score_int(away_item.get("value"), fallback_away) if isinstance(away_item, dict) else fallback_away
|
||||
source_label = "mapping_sql" if (score_home_key and score_away_key and isinstance(home_item, dict) and isinstance(away_item, dict)) else "game"
|
||||
self._persist_score_external_snapshot(match_id, home=resolved_home, away=resolved_away, source=source_label)
|
||||
if "score.external_home" in by_key:
|
||||
by_key["score.external_home"]["value"] = resolved_home
|
||||
if "score.external_away" in by_key:
|
||||
by_key["score.external_away"]["value"] = resolved_away
|
||||
manual = str((by_key.get("score.manual") or {}).get("value") or "0").strip().lower() in {"1", "true", "yes", "on"}
|
||||
if not manual:
|
||||
if "score.home" in by_key:
|
||||
by_key["score.home"]["value"] = resolved_home
|
||||
if "score.away" in by_key:
|
||||
by_key["score.away"]["value"] = resolved_away
|
||||
score_snapshot = {
|
||||
"home": resolved_home, "away": resolved_away, "manual": manual,
|
||||
"source": source_label, "home_key": score_home_key, "away_key": score_away_key,
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"ok": True,
|
||||
@@ -1225,6 +1320,7 @@ class VmixAgentHub:
|
||||
"profile_id": profile_id,
|
||||
"profile_name": profile_name,
|
||||
"profile_version": profile_version,
|
||||
"score_snapshot": score_snapshot,
|
||||
"total": len(fields),
|
||||
"applied": 0,
|
||||
"rules_total": 0,
|
||||
@@ -4382,6 +4478,7 @@ def create_hockey_agent_router(
|
||||
device_id: str,
|
||||
only_changed: bool = False,
|
||||
active_tab: str = "",
|
||||
source_code: str = "",
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
device_id = hub.normalise_device_id(device_id)
|
||||
@@ -4391,9 +4488,12 @@ def create_hockey_agent_router(
|
||||
raise HTTPException(status_code=404, detail="Устройство не прикреплено к вашему аккаунту")
|
||||
if not device.is_active_for_account:
|
||||
raise HTTPException(status_code=409, detail="Для этого Agent выключено получение данных")
|
||||
wanted_source = str(source_code or "").strip()
|
||||
source_codes = {wanted_source} if wanted_source else None
|
||||
result = await hub.apply_mapping_to_device(
|
||||
device_id,
|
||||
reason="runtime_strength_changed" if only_changed else "manual_apply",
|
||||
reason=("runtime_score_changed" if wanted_source == "score" else "runtime_strength_changed") if only_changed else "manual_apply",
|
||||
source_codes=source_codes,
|
||||
only_changed=only_changed,
|
||||
extra_context={"active_tab": str(active_tab or "").strip()} if str(active_tab or "").strip() else None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user