From 72d86fd4e5c0db474d5e43721ca11a42efdbeb19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 04:47:00 +0000 Subject: [PATCH] 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 --- gaming_deals_bot/itad.py | 90 +++++++++++++++++++++++------------ gaming_deals_bot/preflight.py | 13 +++-- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/gaming_deals_bot/itad.py b/gaming_deals_bot/itad.py index 522f5e6..09f50a8 100644 --- a/gaming_deals_bot/itad.py +++ b/gaming_deals_bot/itad.py @@ -7,6 +7,29 @@ logger = logging.getLogger(__name__) 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( client: httpx.AsyncClient, api_key: str, @@ -19,18 +42,22 @@ async def check_historical_lows( if not api_key or not steam_app_ids: return {} - # Build the list of ITAD game IDs from Steam app IDs - # ITAD uses the format "app/{steam_app_id}" for Steam games - game_ids = [f"app/{sid}" for sid in steam_app_ids] + # Look up ITAD game UUIDs for each Steam app ID + itad_id_to_steam: dict[str, str] = {} + 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: - resp = await client.get( + resp = await client.post( f"{BASE_URL}/games/overview/v2", - params={"key": api_key, "shops[]": "steam"}, - headers={"Content-Type": "application/json"}, + params={"key": api_key, "shops": [61]}, + 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() data = resp.json() except (httpx.HTTPError, ValueError) as exc: @@ -38,19 +65,17 @@ async def check_historical_lows( return {} results: dict[str, bool] = {} - # Parse the response — structure depends on the ITAD API version - # The overview endpoint returns price overview with historical low data - if isinstance(data, dict): - for steam_id in steam_app_ids: - game_key = f"app/{steam_id}" - game_data = data.get(game_key) or data.get(steam_id, {}) - if isinstance(game_data, dict): - lowest = game_data.get("lowest", {}) - current = game_data.get("current", {}) - if lowest and current: - lowest_price = lowest.get("price", float("inf")) - current_price = current.get("price", float("inf")) - results[steam_id] = current_price <= lowest_price + for price_entry in data.get("prices", []): + itad_id = price_entry.get("id") + steam_id = itad_id_to_steam.get(itad_id) + if not steam_id: + continue + lowest = price_entry.get("lowest") + current = price_entry.get("current") + if lowest and current: + lowest_price = lowest.get("price", {}).get("amount", float("inf")) + current_price = current.get("price", {}).get("amount", float("inf")) + results[steam_id] = current_price <= lowest_price logger.info( "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: return False + itad_id = await _lookup_game_id(client, api_key, steam_app_id) + if not itad_id: + return False + try: - resp = await client.get( + resp = await client.post( f"{BASE_URL}/games/overview/v2", - params={ - "key": api_key, - "apps[]": f"app/{steam_app_id}", - }, + params={"key": api_key}, + json=[itad_id], ) resp.raise_for_status() 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) return False - # Try to extract historical low info - if isinstance(data, dict) and "prices" in data: - for price_info in data["prices"]: - if price_info.get("isLowest"): + for price_entry in data.get("prices", []): + lowest = price_entry.get("lowest") + current = price_entry.get("current") + 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 False diff --git a/gaming_deals_bot/preflight.py b/gaming_deals_bot/preflight.py index e86b6c9..88a4dd1 100644 --- a/gaming_deals_bot/preflight.py +++ b/gaming_deals_bot/preflight.py @@ -170,14 +170,17 @@ async def _check_itad(http: httpx.AsyncClient, api_key: str) -> bool: return _skip("Skipped", "no ITAD_API_KEY configured (optional)") 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( - f"{ITAD_URL}/games/overview/v2", - params={"key": api_key, "apps[]": "app/220"}, # Half-Life 2 + f"{ITAD_URL}/games/lookup/v1", + 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)") 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: return _fail("API reachable", str(exc))