|
1 | 1 | import asyncio |
2 | 2 | import http |
3 | 3 | import json |
| 4 | +import re |
4 | 5 | from typing import Final, cast |
5 | 6 |
|
6 | 7 | import aiohttp |
|
22 | 23 |
|
23 | 24 | LOGIN_ERROR_CHECK: Final = "Erreur de login" |
24 | 25 |
|
| 26 | +# ScreenScraper occasionally returns malformed JSON with unescaped backslashes in |
| 27 | +# text fields (e.g. game synopses), which the strict parser rejects with |
| 28 | +# "Invalid \escape" and discards the whole response. Match any backslash that is |
| 29 | +# not part of a valid JSON escape so we can repair those before parsing. |
| 30 | +_INVALID_ESCAPE_RE: Final = re.compile(r'\\(?!["\\/bfnrt]|u[0-9a-fA-F]{4})') |
| 31 | + |
| 32 | + |
| 33 | +def _loads_lenient(text: str) -> dict: |
| 34 | + """Parse a ScreenScraper JSON payload, repairing invalid escapes on failure. |
| 35 | +
|
| 36 | + A single unescaped backslash would otherwise sink an entire response (and thus |
| 37 | + the match), so on a decode error we double any backslash that isn't a valid |
| 38 | + JSON escape and try once more. |
| 39 | + """ |
| 40 | + try: |
| 41 | + return json.loads(text) |
| 42 | + except json.JSONDecodeError: |
| 43 | + return json.loads(_INVALID_ESCAPE_RE.sub(r"\\\\", text)) |
| 44 | + |
| 45 | + |
25 | 46 | # ScreenScraper enforces a per-account *thread* (concurrency) cap rather than a |
26 | 47 | # request rate. Because a request can take several seconds, spacing out request |
27 | 48 | # starts is not enough, as overlapping requests would exceed the cap and get |
@@ -140,7 +161,7 @@ async def _request(self, url: str, request_timeout: int = 120) -> dict: |
140 | 161 | status_code=status.HTTP_401_UNAUTHORIZED, |
141 | 162 | detail="Invalid ScreenScraper credentials", |
142 | 163 | ) |
143 | | - data = await res.json() |
| 164 | + data = await res.json(loads=_loads_lenient) |
144 | 165 | _update_thread_allowance(data) |
145 | 166 | return data |
146 | 167 | except aiohttp.ServerTimeoutError: |
@@ -213,7 +234,7 @@ async def _request(self, url: str, request_timeout: int = 120) -> dict: |
213 | 234 | status_code=status.HTTP_401_UNAUTHORIZED, |
214 | 235 | detail="Invalid ScreenScraper credentials", |
215 | 236 | ) |
216 | | - data = await res.json() |
| 237 | + data = await res.json(loads=_loads_lenient) |
217 | 238 | _update_thread_allowance(data) |
218 | 239 | return data |
219 | 240 | except (aiohttp.ClientResponseError, aiohttp.ServerTimeoutError) as err: |
|
0 commit comments