mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 17:33:48 +01:00
_hltb_search sheds name matching (_hltb_matching) and the search request itself (_hltb_search_api); _hltb_detail sheds page parsing (_hltb_page_parse) and completionist/DLC extraction (_hltb_comp_extract). Every extraction is a leaf set -- functions that call nothing else in their module -- so each import stays one-way and no cycle is possible. Choosing boundaries any other way is what made the earlier scanning/_stats attempt fail, and those two are deliberately left for a later pass. 1347 tests, 100.00% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016A1EYiv7e7HGhET84oa9M3
62 lines
1.7 KiB
Python
62 lines
1.7 KiB
Python
"""Parsing a HowLongToBeat game page.
|
|
|
|
Leaf helpers: they call nothing else in the module, so extracting
|
|
them introduces no cycle. Split to keep both files under the 250-line cap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_NEXT_DATA_RE = re.compile(
|
|
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
|
|
)
|
|
|
|
|
|
def _parse_game_page(html: str) -> dict[str, Any] | None:
|
|
"""Extract game data dict from a HLTB game page's __NEXT_DATA__."""
|
|
match = _NEXT_DATA_RE.search(html)
|
|
if not match:
|
|
return None
|
|
try:
|
|
data = json.loads(match.group(1))
|
|
result: dict[str, Any] = data["props"]["pageProps"]["game"]["data"]
|
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
return None
|
|
return result
|
|
|
|
|
|
def _as_positive_int(value: object) -> int:
|
|
"""Convert HLTB numeric JSON values to a positive int, or 0 when invalid."""
|
|
if isinstance(value, int):
|
|
return max(0, value)
|
|
if isinstance(value, float):
|
|
int_value = int(value)
|
|
return max(0, int_value)
|
|
if isinstance(value, str):
|
|
try:
|
|
int_value = int(value)
|
|
return max(0, int_value)
|
|
except ValueError:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def _apply_dlc_leisure_overrides(
|
|
base_hours: float,
|
|
dlc_rels: list[tuple[int, float]],
|
|
dlc_hours_by_id: dict[int, float],
|
|
) -> float:
|
|
"""Replace fallback DLC hours with detailed leisure hours when available."""
|
|
adjusted = base_hours
|
|
for dlc_id, fallback_hours in dlc_rels:
|
|
dlc_leisure = dlc_hours_by_id.get(dlc_id, -1.0)
|
|
if dlc_leisure > 0:
|
|
adjusted += dlc_leisure - fallback_hours
|
|
return round(adjusted, 2)
|