diff --git a/backend/app/services/patreon_resolver.py b/backend/app/services/patreon_resolver.py index f4dc3ef..b4e9300 100644 --- a/backend/app/services/patreon_resolver.py +++ b/backend/app/services/patreon_resolver.py @@ -91,48 +91,46 @@ def _sync_lookup(vanity: str, cookies_path: str | None) -> str | None: ) -def _lookup_via_api(vanity: str, cookies_path: str | None) -> str | None: +def _campaigns_api_first(vanity: str, cookies_path: str | None) -> dict | None: + """The first `data` object from Patreon's campaigns API filtered by vanity + (`?filter[vanity]=&fields[campaign]=name`), or None on any failure + (network / non-200 / non-JSON / empty). The single request shape shared by + _lookup_via_api (plucks the campaign id) and resolve_display_name (plucks the + display name).""" jar = _load_cookie_jar(cookies_path) - headers = { - "User-Agent": _USER_AGENT, - "Accept": "application/vnd.api+json", - } - params = { - "filter[vanity]": vanity, - "fields[campaign]": "name", - } try: resp = requests.get( _CAMPAIGNS_URL, - params=params, - headers=headers, + params={"filter[vanity]": vanity, "fields[campaign]": "name"}, + headers={"User-Agent": _USER_AGENT, "Accept": "application/vnd.api+json"}, cookies=jar, timeout=_TIMEOUT_SECONDS, ) except requests.RequestException as exc: log.warning("Patreon campaigns API request failed for vanity=%s: %s", vanity, exc) return None - if resp.status_code != 200: log.warning( "Patreon campaigns API returned HTTP %d for vanity=%s", resp.status_code, vanity, ) return None - try: payload = resp.json() except ValueError as exc: log.warning("Patreon campaigns API returned non-JSON for vanity=%s: %s", vanity, exc) return None + data = payload.get("data") if isinstance(payload, dict) else None + if not isinstance(data, list) or not data or not isinstance(data[0], dict): + return None + return data[0] - if not isinstance(payload, dict): + +def _lookup_via_api(vanity: str, cookies_path: str | None) -> str | None: + first = _campaigns_api_first(vanity, cookies_path) + if first is None: return None - data = payload.get("data") - if not isinstance(data, list) or not data: - return None - first = data[0] if isinstance(data[0], dict) else None - campaign_id = first.get("id") if first else None + campaign_id = first.get("id") if not isinstance(campaign_id, str) or not campaign_id: return None log.info("Resolved Patreon vanity=%s → campaign_id=%s", vanity, campaign_id) @@ -144,24 +142,10 @@ def resolve_display_name(vanity: str, cookies_path: str | None) -> str | None: (`fields[campaign]=name`), used to name the Artist at add-time (#130). None on any failure — the caller falls back to the vanity handle. Sync: call from an executor.""" - jar = _load_cookie_jar(cookies_path) - try: - resp = requests.get( - _CAMPAIGNS_URL, - params={"filter[vanity]": vanity, "fields[campaign]": "name"}, - headers={"User-Agent": _USER_AGENT, "Accept": "application/vnd.api+json"}, - cookies=jar, - timeout=_TIMEOUT_SECONDS, - ) - if resp.status_code != 200: - return None - data = resp.json().get("data") - except (requests.RequestException, ValueError) as exc: - log.warning("Patreon name lookup failed for vanity=%s: %s", vanity, exc) + first = _campaigns_api_first(vanity, cookies_path) + if first is None: return None - if not isinstance(data, list) or not data or not isinstance(data[0], dict): - return None - name = (data[0].get("attributes") or {}).get("name") + name = (first.get("attributes") or {}).get("name") return name.strip() if isinstance(name, str) and name.strip() else None