try to match to steam game

This commit is contained in:
2026-10-16 05:28:15 +02:00
parent 89fb745e74
commit ad7726d978
5 changed files with 271 additions and 30 deletions
+85 -1
View File
@@ -1,17 +1,101 @@
import logging
import re
import unicodedata
from ..models import Game, PlanningSlot
import httpx
from django.utils.text import slugify
from ..models import EventGame, Game, PlanningSlot
from .steam_store import search_store
logger = logging.getLogger(__name__)
# "Cosmoteer: Starship Architect & Commander" is still "Cosmoteer"
SUBTITLE_SEPARATOR = re.compile(r"\s*(?::|\s-\s|\s–\s|\s—\s)")
def normalize_name(name: str) -> str:
"""'Towerfall: Ascension' and 'TowerFall Ascension' are the same game:
no accents, no case, letters and digits only."""
# Before NFKD, which turns ™ into "TM"
name = re.sub(r"[™®©]", "", name)
name = unicodedata.normalize("NFKD", name)
name = "".join(c for c in name if not unicodedata.combining(c))
return re.sub(r"[^0-9a-z]", "", name.casefold())
def _steam_match(name: str) -> dict | None:
"""The Steam store app named name (or name + a subtitle), if there's one.
Strict on purpose: "Jass" or "Crochet" must not end up as some random game."""
wanted = normalize_name(name)
if not wanted:
return None
# Steam's search chokes on punctuation ("Puyo Puyo — Tetris" finds nothing)
term = " ".join(re.sub(r"[^\w\s]", " ", name).split())
for item in search_store(term):
steam_name = item.get("name") or ""
if wanted in (
normalize_name(steam_name),
normalize_name(SUBTITLE_SEPARATOR.split(steam_name, maxsplit=1)[0]),
):
return item
return None
def _unique_path(name: str) -> str:
base = slugify(name)[:90] or "game"
path, n = base, 2
while Game.objects.filter(path=path).exists():
path, n = f"{base}-{n}", n + 1
return path
def create_missing_steam_games(event, names: list[str]) -> list[Game]:
"""For each name without a Game, look it up on the Steam store and create
the Game + its EventGame when found. Returns the created games."""
known = {normalize_name(game.name) for game in Game.objects.all()}
created = []
for name in dict.fromkeys(names): # unique, keeps order
if normalize_name(name) in known:
continue
try:
item = _steam_match(name)
except (httpx.HTTPError, ValueError) as exc:
logger.warning("Steam store search failed for %r: %s", name, exc)
continue
if item is None:
continue
if Game.objects.filter(steam_app_id=item["id"]).exists():
# Already there under another name, the planning won't match it
logger.warning(
"%r is Steam app %s, already a Game under another name",
name,
item["id"],
)
continue
# Named like in the planning, so the slot matches it
game = Game.objects.create(
name=name, steam_app_id=item["id"], path=_unique_path(name)
)
EventGame.objects.get_or_create(event=event, game=game)
known.add(normalize_name(name))
created.append(game)
return created
def slot_game_names(entries: list[dict]) -> list[str]:
"""Names to look for in a planning: titles of single slots, game names of
multi-anims (their title, like "Fils rouges", isn't a game)."""
names = []
for entry in entries:
if not entry.get("is_multi_anims"):
names.append(entry["title"])
names += [a["name"] for a in entry.get("aandts") or []]
return names
def _game_out(name: str, game: Game | None) -> dict:
if game is None:
return {"name": name}
@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
# Public store endpoint, no API key needed. Rate limited to ~200 calls / 5 min,
# which is plenty for a handful of games every few hours.
APP_DETAILS_URL = "https://store.steampowered.com/api/appdetails"
STORE_SEARCH_URL = "https://store.steampowered.com/api/storesearch/"
class SteamStoreError(Exception):
@@ -37,6 +38,17 @@ def fetch_app_details(steam_app_id: int) -> dict:
return entry["data"]
def search_store(term: str) -> list[dict]:
"""Store search results for term, as [{"id": app id, "name": ...}, ...]."""
response = httpx.get(
STORE_SEARCH_URL,
params={"term": term, "cc": settings.STEAM_STORE_COUNTRY, "l": "english"},
timeout=10,
)
response.raise_for_status()
return (response.json() or {}).get("items") or []
def refresh_steam_game(game: Game) -> None:
"""Pull the current store info for a Steam game into its steam_* fields."""
data = fetch_app_details(game.steam_app_id)