Fix IsThereAnyDeal API: use POST for overview endpoint and lookup game UUIDs
The ITAD v2 API requires POST (not GET) for /games/overview/v2, and expects ITAD game UUIDs in the JSON body rather than Steam app IDs. Added a lookup step via GET /games/lookup/v1 to convert Steam app IDs to ITAD UUIDs before calling the overview endpoint. Updated preflight check to use the lookup endpoint for API key validation. https://claude.ai/code/session_01B7YPGrE3NatkwadCXVjv2j
This commit is contained in:
@@ -7,6 +7,29 @@ logger = logging.getLogger(__name__)
|
|||||||
BASE_URL = "https://api.isthereanydeal.com"
|
BASE_URL = "https://api.isthereanydeal.com"
|
||||||
|
|
||||||
|
|
||||||
|
async def _lookup_game_id(
|
||||||
|
client: httpx.AsyncClient,
|
||||||
|
api_key: str,
|
||||||
|
steam_app_id: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""Look up the ITAD game UUID for a Steam app ID.
|
||||||
|
|
||||||
|
Returns the ITAD game UUID string, or None if not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = await client.get(
|
||||||
|
f"{BASE_URL}/games/lookup/v1",
|
||||||
|
params={"key": api_key, "appid": steam_app_id},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("found") and data.get("game"):
|
||||||
|
return data["game"]["id"]
|
||||||
|
except (httpx.HTTPError, ValueError, KeyError) as exc:
|
||||||
|
logger.error("ITAD lookup error for app %s: %s", steam_app_id, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def check_historical_lows(
|
async def check_historical_lows(
|
||||||
client: httpx.AsyncClient,
|
client: httpx.AsyncClient,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
@@ -19,18 +42,22 @@ async def check_historical_lows(
|
|||||||
if not api_key or not steam_app_ids:
|
if not api_key or not steam_app_ids:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Build the list of ITAD game IDs from Steam app IDs
|
# Look up ITAD game UUIDs for each Steam app ID
|
||||||
# ITAD uses the format "app/{steam_app_id}" for Steam games
|
itad_id_to_steam: dict[str, str] = {}
|
||||||
game_ids = [f"app/{sid}" for sid in steam_app_ids]
|
for sid in steam_app_ids:
|
||||||
|
itad_id = await _lookup_game_id(client, api_key, sid)
|
||||||
|
if itad_id:
|
||||||
|
itad_id_to_steam[itad_id] = sid
|
||||||
|
|
||||||
|
if not itad_id_to_steam:
|
||||||
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await client.get(
|
resp = await client.post(
|
||||||
f"{BASE_URL}/games/overview/v2",
|
f"{BASE_URL}/games/overview/v2",
|
||||||
params={"key": api_key, "shops[]": "steam"},
|
params={"key": api_key, "shops": [61]},
|
||||||
headers={"Content-Type": "application/json"},
|
json=list(itad_id_to_steam.keys()),
|
||||||
)
|
)
|
||||||
# The v2 endpoint may use POST with body — fall back to per-game lookup
|
|
||||||
# if batch doesn't work. For now, use the games/info endpoint pattern.
|
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except (httpx.HTTPError, ValueError) as exc:
|
except (httpx.HTTPError, ValueError) as exc:
|
||||||
@@ -38,19 +65,17 @@ async def check_historical_lows(
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
results: dict[str, bool] = {}
|
results: dict[str, bool] = {}
|
||||||
# Parse the response — structure depends on the ITAD API version
|
for price_entry in data.get("prices", []):
|
||||||
# The overview endpoint returns price overview with historical low data
|
itad_id = price_entry.get("id")
|
||||||
if isinstance(data, dict):
|
steam_id = itad_id_to_steam.get(itad_id)
|
||||||
for steam_id in steam_app_ids:
|
if not steam_id:
|
||||||
game_key = f"app/{steam_id}"
|
continue
|
||||||
game_data = data.get(game_key) or data.get(steam_id, {})
|
lowest = price_entry.get("lowest")
|
||||||
if isinstance(game_data, dict):
|
current = price_entry.get("current")
|
||||||
lowest = game_data.get("lowest", {})
|
if lowest and current:
|
||||||
current = game_data.get("current", {})
|
lowest_price = lowest.get("price", {}).get("amount", float("inf"))
|
||||||
if lowest and current:
|
current_price = current.get("price", {}).get("amount", float("inf"))
|
||||||
lowest_price = lowest.get("price", float("inf"))
|
results[steam_id] = current_price <= lowest_price
|
||||||
current_price = current.get("price", float("inf"))
|
|
||||||
results[steam_id] = current_price <= lowest_price
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"ITAD: checked %d games, %d are at historical low",
|
"ITAD: checked %d games, %d are at historical low",
|
||||||
@@ -69,13 +94,15 @@ async def check_single_historical_low(
|
|||||||
if not api_key or not steam_app_id:
|
if not api_key or not steam_app_id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
itad_id = await _lookup_game_id(client, api_key, steam_app_id)
|
||||||
|
if not itad_id:
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await client.get(
|
resp = await client.post(
|
||||||
f"{BASE_URL}/games/overview/v2",
|
f"{BASE_URL}/games/overview/v2",
|
||||||
params={
|
params={"key": api_key},
|
||||||
"key": api_key,
|
json=[itad_id],
|
||||||
"apps[]": f"app/{steam_app_id}",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
@@ -83,10 +110,13 @@ async def check_single_historical_low(
|
|||||||
logger.error("ITAD API error for app %s: %s", steam_app_id, exc)
|
logger.error("ITAD API error for app %s: %s", steam_app_id, exc)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Try to extract historical low info
|
for price_entry in data.get("prices", []):
|
||||||
if isinstance(data, dict) and "prices" in data:
|
lowest = price_entry.get("lowest")
|
||||||
for price_info in data["prices"]:
|
current = price_entry.get("current")
|
||||||
if price_info.get("isLowest"):
|
if lowest and current:
|
||||||
|
lowest_price = lowest.get("price", {}).get("amount", float("inf"))
|
||||||
|
current_price = current.get("price", {}).get("amount", float("inf"))
|
||||||
|
if current_price <= lowest_price:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -170,14 +170,17 @@ async def _check_itad(http: httpx.AsyncClient, api_key: str) -> bool:
|
|||||||
return _skip("Skipped", "no ITAD_API_KEY configured (optional)")
|
return _skip("Skipped", "no ITAD_API_KEY configured (optional)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use a lightweight endpoint to validate the key
|
# Use the lookup endpoint (GET) to validate the key with a known game
|
||||||
resp = await http.get(
|
resp = await http.get(
|
||||||
f"{ITAD_URL}/games/overview/v2",
|
f"{ITAD_URL}/games/lookup/v1",
|
||||||
params={"key": api_key, "apps[]": "app/220"}, # Half-Life 2
|
params={"key": api_key, "appid": 220}, # Half-Life 2
|
||||||
)
|
)
|
||||||
if resp.status_code == 401 or resp.status_code == 403:
|
if resp.status_code in (401, 403):
|
||||||
return _fail("API key", "rejected by ITAD (401/403)")
|
return _fail("API key", "rejected by ITAD (401/403)")
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return _pass("API key valid", "ITAD responded successfully")
|
data = resp.json()
|
||||||
|
if data.get("found"):
|
||||||
|
return _pass("API reachable", "ITAD responded successfully")
|
||||||
|
return _pass("API reachable", "ITAD key valid (game not found)")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _fail("API reachable", str(exc))
|
return _fail("API reachable", str(exc))
|
||||||
|
|||||||
Reference in New Issue
Block a user