выделение удалений, и заготовки
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)
|
||||
from .models import (MappingSqlDataSource, OperatorSession, UserPreference, VmixAssignment, VmixDevice, VmixMappingProfile, VmixMappingField, VmixPreparedTitle)
|
||||
|
||||
|
||||
AGENT_PROTOCOL_VERSION = 1
|
||||
@@ -2010,6 +2010,394 @@ class VmixAgentHub:
|
||||
})
|
||||
return {"devices": items}
|
||||
|
||||
@staticmethod
|
||||
def _prepared_inventory_from_row(device: VmixDevice | None) -> dict[str, Any]:
|
||||
if device is None:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(device.project_inventory_json or "{}")
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _prepared_find_input(inventory: dict[str, Any], *, key: str = "", number: str = "", title: str = "") -> dict[str, Any] | None:
|
||||
inputs = inventory.get("inputs") if isinstance(inventory, dict) and isinstance(inventory.get("inputs"), list) else []
|
||||
key = str(key or "").strip()
|
||||
number = str(number or "").strip()
|
||||
title = str(title or "").strip()
|
||||
if key:
|
||||
found = next((item for item in inputs if isinstance(item, dict) and str(item.get("key") or "").strip() == key), None)
|
||||
if found is not None:
|
||||
return found
|
||||
if number:
|
||||
found = next((item for item in inputs if isinstance(item, dict) and str(item.get("number") or "").strip() == number), None)
|
||||
if found is not None:
|
||||
return found
|
||||
if title:
|
||||
matches = [item for item in inputs if isinstance(item, dict) and str(item.get("title") or "").strip() == title]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
return None
|
||||
|
||||
def _prepared_device_for_user(self, session: Any, user: HockeyUser, requested_device_id: str = "") -> VmixDevice:
|
||||
requested = str(requested_device_id or "").strip()
|
||||
if requested:
|
||||
requested = self.normalise_device_id(requested)
|
||||
device = session.scalar(select(VmixDevice).where(and_(VmixDevice.device_uuid == requested, VmixDevice.wfl_user_id == user.id)))
|
||||
if device is None:
|
||||
raise HTTPException(status_code=404, detail="Agent не принадлежит текущему аккаунту")
|
||||
return device
|
||||
rows = list(session.scalars(
|
||||
select(VmixDevice)
|
||||
.where(and_(VmixDevice.wfl_user_id == user.id, VmixDevice.is_active_for_account.is_(True)))
|
||||
.order_by(desc(VmixDevice.last_seen_at))
|
||||
))
|
||||
if not rows:
|
||||
raise HTTPException(status_code=409, detail="Hockey Agent не выбран")
|
||||
live = next((row for row in rows if row.device_uuid in self._live and bool(row.vmix_connected)), None)
|
||||
return live or rows[0]
|
||||
|
||||
@staticmethod
|
||||
def _prepared_title_payload(row: VmixPreparedTitle, inventory: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
try:
|
||||
fields = json.loads(row.field_values_json or "{}")
|
||||
if not isinstance(fields, dict):
|
||||
fields = {}
|
||||
except Exception:
|
||||
fields = {}
|
||||
clone_key = str(row.clone_input_key or "")
|
||||
clone_number = str(row.clone_input_number or "")
|
||||
clone_title = str(row.clone_input_title or "")
|
||||
if inventory:
|
||||
match = VmixAgentHub._prepared_find_input(inventory, key=clone_key, number=clone_number, title=clone_title)
|
||||
if match is not None:
|
||||
clone_key = str(match.get("key") or clone_key)
|
||||
clone_number = str(match.get("number") or clone_number)
|
||||
clone_title = str(match.get("title") or clone_title)
|
||||
return {
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"game_id": row.game_external_id,
|
||||
"device_id": row.device_uuid,
|
||||
"source_kind": row.source_kind,
|
||||
"source_ref": row.source_ref,
|
||||
"source_input": {
|
||||
"key": row.source_input_key,
|
||||
"number": row.source_input_number,
|
||||
"title": row.source_input_title,
|
||||
},
|
||||
"clone_input": {"key": clone_key, "number": clone_number, "title": clone_title},
|
||||
"field_values": fields,
|
||||
"created_at": row.created_at.isoformat() if row.created_at else "",
|
||||
"updated_at": row.updated_at.isoformat() if row.updated_at else "",
|
||||
}
|
||||
|
||||
async def list_prepared_titles(self, user: HockeyUser, *, device_id: str = "", game_id: str = "") -> dict[str, Any]:
|
||||
with self.database.session() as session:
|
||||
device = self._prepared_device_for_user(session, user, device_id)
|
||||
resolved_game = str(game_id or device.current_match_external_id or "").strip()
|
||||
query = select(VmixPreparedTitle).where(VmixPreparedTitle.wfl_user_id == user.id)
|
||||
if resolved_game:
|
||||
query = query.where(VmixPreparedTitle.game_external_id == resolved_game)
|
||||
if device.device_uuid:
|
||||
query = query.where(VmixPreparedTitle.device_uuid == device.device_uuid)
|
||||
rows = list(session.scalars(query.order_by(desc(VmixPreparedTitle.created_at), desc(VmixPreparedTitle.id))))
|
||||
inventory = self._prepared_inventory_from_row(device)
|
||||
# Refresh clone key/title opportunistically after Agent inventory catches up.
|
||||
for row in rows:
|
||||
found = self._prepared_find_input(
|
||||
inventory,
|
||||
key=str(row.clone_input_key or ""),
|
||||
number=str(row.clone_input_number or ""),
|
||||
title=str(row.clone_input_title or ""),
|
||||
)
|
||||
if found is not None:
|
||||
row.clone_input_key = str(found.get("key") or row.clone_input_key or "")[:128]
|
||||
row.clone_input_number = str(found.get("number") or row.clone_input_number or "")[:32]
|
||||
row.clone_input_title = str(found.get("title") or row.clone_input_title or "")[:300]
|
||||
items = [self._prepared_title_payload(row, inventory) for row in rows]
|
||||
return {
|
||||
"device_id": device.device_uuid,
|
||||
"game_id": resolved_game,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
def _prepared_mapping_context(self, session: Any, device: VmixDevice, user: HockeyUser) -> tuple[VmixMappingProfile | None, dict[str, Any], dict[str, Any]]:
|
||||
profile = self._active_mapping_profile_for_device(session, device)
|
||||
if profile is None:
|
||||
return None, {}, {}
|
||||
assignment = session.scalar(
|
||||
select(VmixAssignment)
|
||||
.where(and_(VmixAssignment.device_id == device.id, VmixAssignment.active.is_(True)))
|
||||
.order_by(desc(VmixAssignment.id))
|
||||
)
|
||||
context = {
|
||||
"game_id": str(device.current_match_external_id or ""),
|
||||
"device_id": device.device_uuid,
|
||||
"assignment_id": str(device.current_assignment_key or ""),
|
||||
"tournament_id": str(assignment.tournament_external_id or "") if assignment is not None else "",
|
||||
}
|
||||
preference = session.get(UserPreference, user.id)
|
||||
if preference is not None:
|
||||
language = str(preference.vmix_language or preference.display_language or "").strip().lower()
|
||||
if language in {"ru", "en"}:
|
||||
context["ui_language"] = language
|
||||
inventory = self._prepared_inventory_from_row(device)
|
||||
return profile, context, inventory
|
||||
|
||||
async def prepared_mapping_sources(self, user: HockeyUser, *, device_id: str = "", panel_id: str = "") -> dict[str, Any]:
|
||||
panel_id = re.sub(r"[^A-Za-z0-9_]+", "_", str(panel_id or "").strip()).strip("_")[:64]
|
||||
with self.database.session() as session:
|
||||
device = self._prepared_device_for_user(session, user, device_id)
|
||||
profile, _context, inventory = self._prepared_mapping_context(session, device, user)
|
||||
if profile is None:
|
||||
return {"device_id": device.device_uuid, "profile_id": 0, "items": []}
|
||||
fields = list(session.scalars(
|
||||
select(VmixMappingField)
|
||||
.where(and_(VmixMappingField.profile_id == profile.id, VmixMappingField.enabled.is_(True)))
|
||||
.order_by(VmixMappingField.sort_order, VmixMappingField.id)
|
||||
))
|
||||
if panel_id:
|
||||
prefix = f"player_select.{panel_id}."
|
||||
fields = [field for field in fields if str(field.data_key or "").startswith(prefix)]
|
||||
grouped: dict[str, dict[str, Any]] = {}
|
||||
for field in fields:
|
||||
identity = str(field.vmix_input_key or field.vmix_input_title or field.vmix_input_number or "").strip()
|
||||
if not identity:
|
||||
continue
|
||||
item = grouped.setdefault(identity, {
|
||||
"key": str(field.vmix_input_key or ""),
|
||||
"number": str(field.vmix_input_number or ""),
|
||||
"title": str(field.vmix_input_title or ""),
|
||||
"mapped_fields": 0,
|
||||
})
|
||||
item["mapped_fields"] += 1
|
||||
for item in grouped.values():
|
||||
found = self._prepared_find_input(inventory, key=item["key"], number=item["number"], title=item["title"])
|
||||
if found is not None:
|
||||
item.update({
|
||||
"key": str(found.get("key") or item["key"]),
|
||||
"number": str(found.get("number") or item["number"]),
|
||||
"title": str(found.get("title") or item["title"]),
|
||||
"type": str(found.get("type") or ""),
|
||||
})
|
||||
items = sorted(grouped.values(), key=lambda value: (int(value.get("number") or 999999) if str(value.get("number") or "").isdigit() else 999999, str(value.get("title") or "").casefold()))
|
||||
return {"device_id": device.device_uuid, "profile_id": profile.id, "items": items}
|
||||
|
||||
async def prepared_mapping_snapshot(
|
||||
self,
|
||||
user: HockeyUser,
|
||||
*,
|
||||
device_id: str = "",
|
||||
input_key: str = "",
|
||||
input_number: str = "",
|
||||
input_title: str = "",
|
||||
panel_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
panel_id = re.sub(r"[^A-Za-z0-9_]+", "_", str(panel_id or "").strip()).strip("_")[:64]
|
||||
with self.database.session() as session:
|
||||
device = self._prepared_device_for_user(session, user, device_id)
|
||||
profile, context, inventory = self._prepared_mapping_context(session, device, user)
|
||||
if profile is None:
|
||||
return {"device_id": device.device_uuid, "profile_id": 0, "fields": []}
|
||||
source = self._prepared_find_input(inventory, key=input_key, number=input_number, title=input_title)
|
||||
source_key = str(source.get("key") or input_key or "") if source else str(input_key or "")
|
||||
source_number = str(source.get("number") or input_number or "") if source else str(input_number or "")
|
||||
source_title = str(source.get("title") or input_title or "") if source else str(input_title or "")
|
||||
fields = list(session.scalars(
|
||||
select(VmixMappingField)
|
||||
.where(and_(VmixMappingField.profile_id == profile.id, VmixMappingField.enabled.is_(True)))
|
||||
.order_by(VmixMappingField.sort_order, VmixMappingField.id)
|
||||
))
|
||||
if panel_id:
|
||||
prefix = f"player_select.{panel_id}."
|
||||
fields = [field for field in fields if str(field.data_key or "").startswith(prefix)]
|
||||
def target_matches(field: VmixMappingField) -> bool:
|
||||
if source_key and str(field.vmix_input_key or "") == source_key:
|
||||
return True
|
||||
if source_title and str(field.vmix_input_title or "") == source_title:
|
||||
return True
|
||||
if source_number and str(field.vmix_input_number or "") == source_number:
|
||||
return True
|
||||
return False
|
||||
fields = [field for field in fields if target_matches(field)]
|
||||
catalog = self.mapping_data.data_catalog(user, context)
|
||||
by_key = {str(item.get("key") or ""): item for item in (catalog.get("items") or []) if item.get("key")}
|
||||
result_fields = []
|
||||
for field in fields:
|
||||
item = by_key.get(str(field.data_key or ""))
|
||||
if item is None:
|
||||
continue
|
||||
result_fields.append({
|
||||
"name": str(field.vmix_field or ""),
|
||||
"type": str(field.field_type or "text"),
|
||||
"data_key": str(field.data_key or ""),
|
||||
"value": "" if item.get("value") is None else str(item.get("value")),
|
||||
})
|
||||
return {
|
||||
"device_id": device.device_uuid,
|
||||
"game_id": str(device.current_match_external_id or ""),
|
||||
"profile_id": profile.id,
|
||||
"input": {"key": source_key, "number": source_number, "title": source_title},
|
||||
"fields": result_fields,
|
||||
}
|
||||
|
||||
async def create_prepared_title(self, user: HockeyUser, payload: Any) -> dict[str, Any]:
|
||||
requested_device = str(getattr(payload, "device_id", "") or "").strip()
|
||||
session_token = str(getattr(payload, "session_token", "") or "").strip()
|
||||
with self.database.session() as session:
|
||||
device = self._prepared_device_for_user(session, user, requested_device)
|
||||
inventory = self._prepared_inventory_from_row(device)
|
||||
source = self._prepared_find_input(
|
||||
inventory,
|
||||
key=str(getattr(payload, "source_input_key", "") or ""),
|
||||
number=str(getattr(payload, "source_input_number", "") or ""),
|
||||
title=str(getattr(payload, "source_input_title", "") or ""),
|
||||
)
|
||||
if source is None:
|
||||
raise HTTPException(status_code=404, detail="Исходный vMix Input не найден в текущем проекте")
|
||||
source_key = str(source.get("key") or "")
|
||||
source_number = str(source.get("number") or "")
|
||||
source_title = str(source.get("title") or "")
|
||||
source_ref = source_key or source_number or source_title
|
||||
before_inputs = [item for item in (inventory.get("inputs") or []) if isinstance(item, dict)]
|
||||
before_keys = {str(item.get("key") or "") for item in before_inputs if str(item.get("key") or "")}
|
||||
before_numbers = {str(item.get("number") or "") for item in before_inputs if str(item.get("number") or "")}
|
||||
numeric_numbers = [int(value) for value in before_numbers if value.isdigit()]
|
||||
expected_number = str((max(numeric_numbers) if numeric_numbers else len(before_inputs)) + 1)
|
||||
|
||||
create_result = await self.run_vmix_sequence_for_user(
|
||||
user,
|
||||
[{"Function": "CreateVirtualInput", "Input": source_ref}],
|
||||
device_id=device.device_uuid,
|
||||
session_token=session_token,
|
||||
timeout=6.0,
|
||||
)
|
||||
target_device_id = str(create_result.get("device_id") or device.device_uuid)
|
||||
match_id = str(create_result.get("match_id") or "")
|
||||
|
||||
clone: dict[str, Any] | None = None
|
||||
for _ in range(12):
|
||||
await asyncio.sleep(0.18)
|
||||
with self.database.session() as session:
|
||||
refreshed = session.scalar(select(VmixDevice).where(VmixDevice.device_uuid == target_device_id))
|
||||
current_inventory = self._prepared_inventory_from_row(refreshed)
|
||||
current_inputs = [item for item in (current_inventory.get("inputs") or []) if isinstance(item, dict)]
|
||||
candidates = [item for item in current_inputs if (str(item.get("key") or "") and str(item.get("key") or "") not in before_keys) or (str(item.get("number") or "") and str(item.get("number") or "") not in before_numbers)]
|
||||
if candidates:
|
||||
clone = sorted(candidates, key=lambda item: int(item.get("number") or 0) if str(item.get("number") or "").isdigit() else 0)[-1]
|
||||
break
|
||||
clone_key = str(clone.get("key") or "") if clone else ""
|
||||
clone_number = str(clone.get("number") or expected_number) if clone else expected_number
|
||||
clone_title = str(clone.get("title") or source_title) if clone else source_title
|
||||
clone_ref = clone_key or clone_number or clone_title
|
||||
|
||||
raw_values = getattr(payload, "field_values", {})
|
||||
raw_values = raw_values if isinstance(raw_values, dict) else {}
|
||||
source_fields = source.get("fields") if isinstance(source.get("fields"), list) else []
|
||||
allowed = {str(field.get("name") or ""): field for field in source_fields if isinstance(field, dict) and str(field.get("name") or "")}
|
||||
commands: list[dict[str, Any]] = []
|
||||
saved_fields: dict[str, dict[str, str]] = {}
|
||||
for name, field in list(allowed.items())[:240]:
|
||||
if name not in raw_values:
|
||||
continue
|
||||
raw_entry = raw_values.get(name)
|
||||
if isinstance(raw_entry, dict):
|
||||
value = "" if raw_entry.get("value") is None else str(raw_entry.get("value"))
|
||||
requested_type = str(raw_entry.get("type") or "")
|
||||
else:
|
||||
value = "" if raw_entry is None else str(raw_entry)
|
||||
requested_type = ""
|
||||
field_type = str(field.get("type") or requested_type or "text").lower()
|
||||
if field_type not in {"text", "image", "source", "color", "colour"}:
|
||||
field_type = requested_type.lower() if requested_type.lower() in {"text", "image", "source", "color", "colour"} else "text"
|
||||
saved_fields[name] = {"value": value[:8000], "type": field_type}
|
||||
# Empty text is meaningful (clear inherited content). Empty image/color is
|
||||
# treated as "leave inherited value" because vMix rejects some empty sources.
|
||||
if not value and field_type in {"image", "source", "color", "colour"}:
|
||||
continue
|
||||
commands.append({
|
||||
"Function": _mapping_value_function(field_type),
|
||||
"Input": clone_ref,
|
||||
"SelectedName": name,
|
||||
"Value": value,
|
||||
})
|
||||
set_result = None
|
||||
if commands:
|
||||
set_result = await self.run_vmix_sequence_for_user(
|
||||
user,
|
||||
commands,
|
||||
device_id=target_device_id,
|
||||
session_token=session_token,
|
||||
timeout=max(4.0, min(12.0, 3.0 + len(commands) * 0.04)),
|
||||
)
|
||||
|
||||
title_name = str(getattr(payload, "name", "") or "").strip()[:200] or f"{source_title or 'Title'} · заготовка"
|
||||
source_kind = re.sub(r"[^A-Za-z0-9_-]+", "_", str(getattr(payload, "source_kind", "manual") or "manual").strip())[:32] or "manual"
|
||||
source_ref_name = re.sub(r"[^A-Za-z0-9_.:-]+", "_", str(getattr(payload, "source_ref", "") or "").strip())[:128]
|
||||
with self.database.session() as session:
|
||||
row = VmixPreparedTitle(
|
||||
wfl_user_id=user.id,
|
||||
game_external_id=match_id,
|
||||
device_uuid=target_device_id,
|
||||
name=title_name,
|
||||
source_kind=source_kind,
|
||||
source_ref=source_ref_name,
|
||||
source_input_key=source_key,
|
||||
source_input_number=source_number,
|
||||
source_input_title=source_title,
|
||||
clone_input_key=clone_key,
|
||||
clone_input_number=clone_number,
|
||||
clone_input_title=clone_title,
|
||||
field_values_json=json.dumps(saved_fields, ensure_ascii=False, separators=(",", ":")),
|
||||
created_by=user.id,
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
session.add(row)
|
||||
session.flush()
|
||||
result = self._prepared_title_payload(row)
|
||||
result["ok"] = True
|
||||
result["create_result"] = create_result
|
||||
result["set_result"] = set_result or {"ok": True, "applied": 0}
|
||||
return result
|
||||
|
||||
async def preview_prepared_title(self, prepared_id: int, user: HockeyUser, payload: Any) -> dict[str, Any]:
|
||||
with self.database.session() as session:
|
||||
row = session.get(VmixPreparedTitle, int(prepared_id))
|
||||
if row is None or row.wfl_user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Заготовка не найдена")
|
||||
requested_device = str(getattr(payload, "device_id", "") or row.device_uuid or "").strip()
|
||||
device = self._prepared_device_for_user(session, user, requested_device)
|
||||
inventory = self._prepared_inventory_from_row(device)
|
||||
found = self._prepared_find_input(
|
||||
inventory,
|
||||
key=str(row.clone_input_key or ""),
|
||||
number=str(row.clone_input_number or ""),
|
||||
title=str(row.clone_input_title or ""),
|
||||
)
|
||||
input_ref = str(found.get("key") or found.get("number") or "") if found is not None else str(row.clone_input_key or row.clone_input_number or row.clone_input_title or "")
|
||||
if not input_ref:
|
||||
raise HTTPException(status_code=409, detail="Клонированный Input больше не найден в vMix")
|
||||
return await self.run_vmix_sequence_for_user(
|
||||
user,
|
||||
[{"Function": "PreviewInput", "Input": input_ref}],
|
||||
device_id=device.device_uuid,
|
||||
session_token=str(getattr(payload, "session_token", "") or ""),
|
||||
timeout=4.0,
|
||||
)
|
||||
|
||||
async def delete_prepared_title(self, prepared_id: int, user: HockeyUser) -> dict[str, Any]:
|
||||
with self.database.session() as session:
|
||||
row = session.get(VmixPreparedTitle, int(prepared_id))
|
||||
if row is None or row.wfl_user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Заготовка не найдена")
|
||||
# Deliberately do not RemoveInput in vMix: deleting a web list entry must
|
||||
# never unexpectedly destroy a studio graphic that may already be used.
|
||||
session.delete(row)
|
||||
return {"ok": True, "id": int(prepared_id), "vmix_input_removed": False}
|
||||
|
||||
async def latest_vmix_inventory(self) -> dict[str, Any]:
|
||||
"""Return the most recent connected vMix project inventory for editor helpers.
|
||||
|
||||
@@ -3183,6 +3571,23 @@ class MappingSqlPreviewPayload(BaseModel):
|
||||
context: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class PreparedTitleCreatePayload(BaseModel):
|
||||
name: str = Field(default="", max_length=200)
|
||||
device_id: str = Field(default="", max_length=128)
|
||||
session_token: str = Field(default="", max_length=128)
|
||||
source_input_key: str = Field(default="", max_length=128)
|
||||
source_input_number: str = Field(default="", max_length=32)
|
||||
source_input_title: str = Field(default="", max_length=300)
|
||||
source_kind: str = Field(default="manual", max_length=32)
|
||||
source_ref: str = Field(default="", max_length=128)
|
||||
field_values: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class PreparedTitlePreviewPayload(BaseModel):
|
||||
device_id: str = Field(default="", max_length=128)
|
||||
session_token: str = Field(default="", max_length=128)
|
||||
|
||||
|
||||
class AgentHelloPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
type: str = "hello"
|
||||
@@ -3215,6 +3620,57 @@ def create_hockey_agent_router(
|
||||
) -> dict[str, Any]:
|
||||
return await hub.vmix_inventory_for_user(user, device_id=device_id)
|
||||
|
||||
@router.get("/api/hockey/prepared-titles")
|
||||
async def prepared_titles_list(
|
||||
device_id: str = Query("", max_length=128),
|
||||
game_id: str = Query("", max_length=64),
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
return await hub.list_prepared_titles(user, device_id=device_id, game_id=game_id)
|
||||
|
||||
@router.get("/api/hockey/prepared-titles/mapping-sources")
|
||||
async def prepared_titles_mapping_sources(
|
||||
device_id: str = Query("", max_length=128),
|
||||
panel_id: str = Query("", max_length=64),
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
return await hub.prepared_mapping_sources(user, device_id=device_id, panel_id=panel_id)
|
||||
|
||||
@router.get("/api/hockey/prepared-titles/mapping-snapshot")
|
||||
async def prepared_titles_mapping_snapshot(
|
||||
device_id: str = Query("", max_length=128),
|
||||
input_key: str = Query("", max_length=128),
|
||||
input_number: str = Query("", max_length=32),
|
||||
input_title: str = Query("", max_length=300),
|
||||
panel_id: str = Query("", max_length=64),
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
return await hub.prepared_mapping_snapshot(
|
||||
user, device_id=device_id, input_key=input_key, input_number=input_number, input_title=input_title, panel_id=panel_id
|
||||
)
|
||||
|
||||
@router.post("/api/hockey/prepared-titles")
|
||||
async def prepared_title_create(
|
||||
payload: PreparedTitleCreatePayload,
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
return await hub.create_prepared_title(user, payload)
|
||||
|
||||
@router.post("/api/hockey/prepared-titles/{prepared_id}/preview")
|
||||
async def prepared_title_preview(
|
||||
prepared_id: int,
|
||||
payload: PreparedTitlePreviewPayload,
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
return await hub.preview_prepared_title(prepared_id, user, payload)
|
||||
|
||||
@router.delete("/api/hockey/prepared-titles/{prepared_id}")
|
||||
async def prepared_title_delete(
|
||||
prepared_id: int,
|
||||
user: HockeyUser = Depends(auth_dependency),
|
||||
) -> dict[str, Any]:
|
||||
return await hub.delete_prepared_title(prepared_id, user)
|
||||
|
||||
@router.post("/api/hockey/agents/devices/{device_id}/pair")
|
||||
async def pair_device(
|
||||
device_id: str,
|
||||
|
||||
Reference in New Issue
Block a user