Fix
This commit is contained in:
164
backend/app/normalizers.py
Normal file
164
backend/app/normalizers.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Normalization helpers for provisional server collection flows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Mapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .a2s_client import A2SServerInfo
|
||||
|
||||
|
||||
MAP_NAME_ALIASES = {
|
||||
"stmarie": "St. Marie Du Mont",
|
||||
"stmariedumont": "St. Marie Du Mont",
|
||||
"saintemariedumont": "St. Marie Du Mont",
|
||||
"saintemariedumontwarfare": "St. Marie Du Mont",
|
||||
"saintemariedumontoffensiveus": "St. Marie Du Mont",
|
||||
"saintemariedumontoffensiveger": "St. Marie Du Mont",
|
||||
"saintemariedumontnight": "St. Marie Du Mont",
|
||||
"saintemariedumontovercast": "St. Marie Du Mont",
|
||||
"sainte-mariedumont": "St. Marie Du Mont",
|
||||
"sainte-marie-du-mont": "St. Marie Du Mont",
|
||||
"stmereeglise": "St. Mere Eglise",
|
||||
"stmereeglisewarfare": "St. Mere Eglise",
|
||||
"stmereegliseoffensiveus": "St. Mere Eglise",
|
||||
"stmereegliseoffensiveger": "St. Mere Eglise",
|
||||
"saintemereeglise": "St. Mere Eglise",
|
||||
"sainte-mere-eglise": "St. Mere Eglise",
|
||||
"purpleheartlane": "Purple Heart Lane",
|
||||
"utahbeach": "Utah Beach",
|
||||
"omahabeach": "Omaha Beach",
|
||||
"hurtgenforest": "Hurtgen Forest",
|
||||
"hill400": "Hill 400",
|
||||
"foy": "Foy",
|
||||
"kursk": "Kursk",
|
||||
"kharkov": "Kharkov",
|
||||
"kharkiv": "Kharkiv",
|
||||
"stalingrad": "Stalingrad",
|
||||
"remagen": "Remagen",
|
||||
"driel": "Driel",
|
||||
"elalamein": "El Alamein",
|
||||
"mortain": "Mortain",
|
||||
"carentan": "Carentan",
|
||||
"devn": "Elsenborn Ridge",
|
||||
"elsenbornridge": "Elsenborn Ridge",
|
||||
"elsenborn": "Elsenborn Ridge",
|
||||
"smolensk": "Smolensk",
|
||||
"smolenskwarfare": "Smolensk",
|
||||
"smolenskoffensiverus": "Smolensk",
|
||||
"smolenskoffensiveger": "Smolensk",
|
||||
"developertestmap": "Smolensk",
|
||||
"devq": "Smolensk",
|
||||
}
|
||||
|
||||
|
||||
def normalize_server_record(
|
||||
raw_record: Mapping[str, object],
|
||||
*,
|
||||
source_name: str,
|
||||
) -> dict[str, object]:
|
||||
"""Normalize a raw server record into the collector's internal shape."""
|
||||
external_server_id = _string_or_none(raw_record.get("external_server_id"))
|
||||
return {
|
||||
"external_server_id": external_server_id,
|
||||
"server_name": _string_or_default(raw_record.get("server_name"), "Unknown server"),
|
||||
"status": _normalize_status(raw_record.get("status")),
|
||||
"players": _coerce_int(raw_record.get("players")),
|
||||
"max_players": _coerce_int(raw_record.get("max_players")),
|
||||
"current_map": normalize_map_name(raw_record.get("current_map")),
|
||||
"region": _string_or_none(raw_record.get("region")),
|
||||
"source_name": source_name,
|
||||
"snapshot_origin": "controlled-fallback",
|
||||
"source_ref": external_server_id or source_name,
|
||||
}
|
||||
|
||||
|
||||
def normalize_a2s_server_info(
|
||||
server_info: "A2SServerInfo",
|
||||
*,
|
||||
source_name: str,
|
||||
external_server_id: str | None = None,
|
||||
region: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Normalize a probed A2S payload into the collector's internal shape."""
|
||||
resolved_external_id = external_server_id or (
|
||||
f"a2s:{server_info.host}:{server_info.query_port}"
|
||||
)
|
||||
return {
|
||||
"external_server_id": resolved_external_id,
|
||||
"server_name": server_info.server_name or "Unknown server",
|
||||
"status": "online",
|
||||
"players": server_info.players,
|
||||
"max_players": server_info.max_players,
|
||||
"current_map": normalize_map_name(server_info.map_name),
|
||||
"region": region,
|
||||
"source_name": source_name,
|
||||
"snapshot_origin": "real-a2s",
|
||||
"source_ref": f"a2s://{server_info.host}:{server_info.query_port}",
|
||||
}
|
||||
|
||||
|
||||
def normalize_map_name(value: object) -> str | None:
|
||||
"""Normalize internal or abbreviated HLL map labels into a stable display name."""
|
||||
normalized = _string_or_none(value)
|
||||
if normalized is None:
|
||||
return None
|
||||
|
||||
alias_key = "".join(character.lower() for character in normalized if character.isalnum())
|
||||
alias_match = MAP_NAME_ALIASES.get(alias_key)
|
||||
if alias_match:
|
||||
return alias_match
|
||||
|
||||
for candidate_key, candidate_label in MAP_NAME_ALIASES.items():
|
||||
if alias_key.startswith(candidate_key):
|
||||
return candidate_label
|
||||
|
||||
prettified = _prettify_map_name(normalized)
|
||||
return prettified or normalized
|
||||
|
||||
|
||||
def _normalize_status(value: object) -> str:
|
||||
if not isinstance(value, str):
|
||||
return "unknown"
|
||||
|
||||
normalized = value.strip().lower()
|
||||
if normalized in {"online", "offline", "unknown"}:
|
||||
return normalized
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _coerce_int(value: object) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _string_or_none(value: object) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
def _string_or_default(value: object, default: str) -> str:
|
||||
normalized = _string_or_none(value)
|
||||
return normalized or default
|
||||
|
||||
|
||||
def _prettify_map_name(value: str) -> str:
|
||||
text = value.replace("_", " ").replace("-", " ").strip()
|
||||
compact_text = " ".join(text.split())
|
||||
if not compact_text:
|
||||
return value
|
||||
|
||||
return " ".join(
|
||||
word.upper() if word.isdigit() else word.capitalize()
|
||||
for word in compact_text.split(" ")
|
||||
)
|
||||
Reference in New Issue
Block a user