sni-cli 1.2.0__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sni_cli-1.2.0 → sni_cli-1.2.2}/PKG-INFO +5 -5
- {sni_cli-1.2.0 → sni_cli-1.2.2}/README.md +4 -4
- {sni_cli-1.2.0 → sni_cli-1.2.2}/pyproject.toml +1 -1
- sni_cli-1.2.2/sni/__init__.py +1 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/cli.py +20 -13
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/exceptions.py +2 -1
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/allanime.py +128 -93
- sni_cli-1.2.0/sni/__init__.py +0 -1
- {sni_cli-1.2.0 → sni_cli-1.2.2}/.gitignore +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/LICENSE +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/install.ps1 +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/install.sh +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/__main__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/config.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/logger.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/player.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/__init__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/base.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/cache.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/extractors/__init__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/extractors/megacloud.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/extractors/vixcloud.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/providers/registry.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/__init__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/app.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/bridge.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/screens/__init__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/screens/help.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/screens/history.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/screens/home.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/screens/player.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/widgets/__init__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/widgets/ascii_art.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/tui/widgets/info_box.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/ui.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/watch_history.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/sni/wizard.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/__init__.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/conftest.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/test_allanime_captcha.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/test_cache.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/test_config.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/test_exceptions.py +0 -0
- {sni_cli-1.2.0 → sni_cli-1.2.2}/tests/test_providers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sni-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Stream Ninja Interface — terminal-based anime streaming CLI + TUI
|
|
5
5
|
Project-URL: Homepage, https://github.com/sundeepyt2/SNI
|
|
6
6
|
Project-URL: Source, https://github.com/sundeepyt2/SNI
|
|
@@ -356,9 +356,9 @@ sni tui
|
|
|
356
356
|
|
|
357
357
|
## AllAnime captcha fix
|
|
358
358
|
|
|
359
|
-
**
|
|
359
|
+
**SNI v1.2.1+ auto-fixes captcha for you.** When `api.allanime.day` captcha-walls your IP, SNI automatically retries the request through a free public CORS proxy (`proxy.cors.sh`). This happens silently, with zero setup. Most users can just run `sni play "one piece"` and it works — no cookies, no CF Worker, no config changes.
|
|
360
360
|
|
|
361
|
-
If you
|
|
361
|
+
If you STILL see `NEED_CAPTCHA` (rare — means all proxies were also captcha-walled), try these in order. Run `sni config --cookie-info` to see all options in a single panel.
|
|
362
362
|
|
|
363
363
|
### Option 1 — Browser cookies (if your IP isn't permanently flagged)
|
|
364
364
|
|
|
@@ -380,9 +380,9 @@ echo 'cf_clearance=...;' > ~/.config/sni/allanime_cookies.txt
|
|
|
380
380
|
sni play "one piece" --cookie 'cf_clearance=...;'
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
### Option 2 — Cloudflare Worker (
|
|
383
|
+
### Option 2 — Cloudflare Worker (last resort for VPN/shared IPs that are permanently captcha-walled AND cookies + public proxies don't work)
|
|
384
384
|
|
|
385
|
-
This is the most powerful bypass — it proxies AllAnime API requests through Cloudflare's own IPs, which AllAnime rarely challenges. Only set this up if Option 1 fails.
|
|
385
|
+
This is the most powerful bypass — it proxies AllAnime API requests through Cloudflare's own IPs, which AllAnime rarely challenges. Only set this up if Option 1 fails AND SNI's built-in proxy fallback doesn't work for you.
|
|
386
386
|
|
|
387
387
|
**Deploy via Cloudflare** (free, requires a Cloudflare account):
|
|
388
388
|
|
|
@@ -303,9 +303,9 @@ sni tui
|
|
|
303
303
|
|
|
304
304
|
## AllAnime captcha fix
|
|
305
305
|
|
|
306
|
-
**
|
|
306
|
+
**SNI v1.2.1+ auto-fixes captcha for you.** When `api.allanime.day` captcha-walls your IP, SNI automatically retries the request through a free public CORS proxy (`proxy.cors.sh`). This happens silently, with zero setup. Most users can just run `sni play "one piece"` and it works — no cookies, no CF Worker, no config changes.
|
|
307
307
|
|
|
308
|
-
If you
|
|
308
|
+
If you STILL see `NEED_CAPTCHA` (rare — means all proxies were also captcha-walled), try these in order. Run `sni config --cookie-info` to see all options in a single panel.
|
|
309
309
|
|
|
310
310
|
### Option 1 — Browser cookies (if your IP isn't permanently flagged)
|
|
311
311
|
|
|
@@ -327,9 +327,9 @@ echo 'cf_clearance=...;' > ~/.config/sni/allanime_cookies.txt
|
|
|
327
327
|
sni play "one piece" --cookie 'cf_clearance=...;'
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
### Option 2 — Cloudflare Worker (
|
|
330
|
+
### Option 2 — Cloudflare Worker (last resort for VPN/shared IPs that are permanently captcha-walled AND cookies + public proxies don't work)
|
|
331
331
|
|
|
332
|
-
This is the most powerful bypass — it proxies AllAnime API requests through Cloudflare's own IPs, which AllAnime rarely challenges. Only set this up if Option 1 fails.
|
|
332
|
+
This is the most powerful bypass — it proxies AllAnime API requests through Cloudflare's own IPs, which AllAnime rarely challenges. Only set this up if Option 1 fails AND SNI's built-in proxy fallback doesn't work for you.
|
|
333
333
|
|
|
334
334
|
**Deploy via Cloudflare** (free, requires a Cloudflare account):
|
|
335
335
|
|
|
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
|
|
|
9
9
|
# just `sni` (defined below in [project.scripts]), so users run `sni play`,
|
|
10
10
|
# `sni tui`, etc. — only `pip install sni-cli` differs from the bare name.
|
|
11
11
|
name = "sni-cli"
|
|
12
|
-
version = "1.2.
|
|
12
|
+
version = "1.2.2"
|
|
13
13
|
description = "Stream Ninja Interface — terminal-based anime streaming CLI + TUI"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.2"
|
|
@@ -142,6 +142,8 @@ async def _search(
|
|
|
142
142
|
results.append((provider_name, hits))
|
|
143
143
|
except CaptchaRequiredError:
|
|
144
144
|
raise # let the caller print the helpful panel
|
|
145
|
+
except ProviderError:
|
|
146
|
+
raise # let the caller print the error
|
|
145
147
|
except Exception as e:
|
|
146
148
|
logger.exception(f"Search failed for '{query}' with provider '{provider_name}'")
|
|
147
149
|
typer.echo(f"Search failed: {type(e).__name__}: {e}")
|
|
@@ -192,6 +194,9 @@ async def _play(
|
|
|
192
194
|
except CaptchaRequiredError as e:
|
|
193
195
|
_print_captcha_help(e)
|
|
194
196
|
return
|
|
197
|
+
except ProviderError as e:
|
|
198
|
+
typer.echo(f"Error: {e}")
|
|
199
|
+
return
|
|
195
200
|
if not results:
|
|
196
201
|
typer.echo("No results found.")
|
|
197
202
|
return
|
|
@@ -257,6 +262,9 @@ async def _watch(
|
|
|
257
262
|
except CaptchaRequiredError as e:
|
|
258
263
|
_print_captcha_help(e)
|
|
259
264
|
return
|
|
265
|
+
except ProviderError as e:
|
|
266
|
+
typer.echo(f"Error: {e}")
|
|
267
|
+
return
|
|
260
268
|
if not results:
|
|
261
269
|
typer.echo("No results found.")
|
|
262
270
|
return
|
|
@@ -305,6 +313,9 @@ async def _search_cmd(
|
|
|
305
313
|
except CaptchaRequiredError as e:
|
|
306
314
|
_print_captcha_help(e)
|
|
307
315
|
raise typer.Exit(code=1)
|
|
316
|
+
except ProviderError as e:
|
|
317
|
+
typer.echo(f"Error: {e}")
|
|
318
|
+
raise typer.Exit(code=1)
|
|
308
319
|
|
|
309
320
|
if not results:
|
|
310
321
|
typer.echo("No results found.")
|
|
@@ -502,12 +513,12 @@ def config(
|
|
|
502
513
|
if cookie_info:
|
|
503
514
|
from sni.config import DEFAULT_COOKIES_PATH
|
|
504
515
|
console.print(Panel(
|
|
505
|
-
"[bold green]
|
|
506
|
-
"
|
|
507
|
-
"
|
|
508
|
-
"
|
|
509
|
-
"
|
|
510
|
-
"[bold]If you
|
|
516
|
+
"[bold green]SNI now auto-fixes captcha — you probably don't need this.[/bold green]\n"
|
|
517
|
+
"When api.allanime.day captcha-walls your IP, SNI automatically\n"
|
|
518
|
+
"retries the request through a free public CORS proxy\n"
|
|
519
|
+
"(proxy.cors.sh). This happens silently, with zero setup. Just\n"
|
|
520
|
+
"run `sni play \"one piece\"` — if it works, you're done.\n\n"
|
|
521
|
+
"[bold]If you STILL hit a NEED_CAPTCHA error (all proxies failed),[/bold]\n"
|
|
511
522
|
"[bold]try these in order:[/bold]\n\n"
|
|
512
523
|
"[bold cyan]Option 1 - Browser cookies:[/bold cyan]\n"
|
|
513
524
|
" Get cookies from a working allanime mirror (NOT allanime.day which\n"
|
|
@@ -518,8 +529,8 @@ def config(
|
|
|
518
529
|
" sni config --update allanime_cookies='cf_clearance=...;'\n"
|
|
519
530
|
" Or write to file (easier to refresh):\n"
|
|
520
531
|
f" echo 'cf_clearance=...;' > {DEFAULT_COOKIES_PATH}\n\n"
|
|
521
|
-
"[bold yellow]Option 2 - Cloudflare Worker
|
|
522
|
-
"[bold yellow]
|
|
532
|
+
"[bold yellow]Option 2 - Cloudflare Worker[/bold yellow]\n"
|
|
533
|
+
"[bold yellow](last resort for VPN/shared IPs):[/bold yellow]\n"
|
|
523
534
|
" Deploy the XAN CF Worker (free, requires Cloudflare account):\n"
|
|
524
535
|
" 1. https://dash.cloudflare.com -> Workers & Pages -> Create\n"
|
|
525
536
|
" 2. Paste XAN/cf-worker/worker.js from the XAN repo\n"
|
|
@@ -528,11 +539,7 @@ def config(
|
|
|
528
539
|
" Can't create a Cloudflare account? Deploy the same worker.js to:\n"
|
|
529
540
|
" - Deno Deploy (https://dash.deno.com) - free, no card required\n"
|
|
530
541
|
" - Vercel Edge Functions (https://vercel.com) - free tier\n"
|
|
531
|
-
" - Netlify Functions (https://netlify.com) - free tier\n
|
|
532
|
-
"[dim]Note: hianime and animepahe providers were removed in v1.2.0[/dim]\n"
|
|
533
|
-
"[dim](both were dead — hianime.to domain gone, animepahe API deprecated).[/dim]\n"
|
|
534
|
-
"[dim]AllAnime is now the only provider; the mirror fallback handles[/dim]\n"
|
|
535
|
-
"[dim]most captcha cases automatically.[/dim]",
|
|
542
|
+
" - Netlify Functions (https://netlify.com) - free tier\n",
|
|
536
543
|
title="AllAnime captcha bypass",
|
|
537
544
|
border_style="cyan",
|
|
538
545
|
))
|
|
@@ -22,7 +22,8 @@ class CaptchaRequiredError(ProviderError):
|
|
|
22
22
|
|
|
23
23
|
def __init__(self, message: str = "Provider requires captcha.", hint: str = ""):
|
|
24
24
|
self.hint = hint or (
|
|
25
|
-
"All AllAnime
|
|
25
|
+
"All AllAnime endpoints + public proxy fallbacks were captcha-walled.\n"
|
|
26
|
+
"Two options:\n"
|
|
26
27
|
" 1. Browser cookies from a working mirror (NOT allanime.day which\n"
|
|
27
28
|
" is broken with a redirect loop). Use allmanga.to or\n"
|
|
28
29
|
" allanime.uns.bio, then:\n"
|
|
@@ -58,8 +58,8 @@ USER_AGENT = (
|
|
|
58
58
|
REFERER = "https://youtu-chan.com"
|
|
59
59
|
ORIGIN = "https://youtu-chan.com"
|
|
60
60
|
|
|
61
|
-
REQUEST_TIMEOUT =
|
|
62
|
-
CLOCK_TIMEOUT =
|
|
61
|
+
REQUEST_TIMEOUT = 30.0 # increased from 15s — proxy connections can be slow
|
|
62
|
+
CLOCK_TIMEOUT = 30.0
|
|
63
63
|
|
|
64
64
|
# Regex used by the embed scraper to pull HLS / MP4 URLs out of HTML pages.
|
|
65
65
|
# Mirrors XAN's scrapeEmbedPage regexes.
|
|
@@ -146,33 +146,66 @@ def _is_captcha_response(resp: httpx.Response) -> bool:
|
|
|
146
146
|
return "just a moment" in body_start or "cf-mitigated" in resp.headers
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def _wrap_fallback_error(last_error: Optional[Exception], endpoint: str) -> Exception:
|
|
150
|
+
"""Wrap a network/captcha error in a user-friendly ProviderError.
|
|
151
|
+
|
|
152
|
+
If the last error was already a CaptchaRequiredError, return it as-is
|
|
153
|
+
(it already has a helpful hint). If it was a network error (ConnectTimeout,
|
|
154
|
+
ReadTimeout, etc.), wrap it in a ProviderError with a clear message
|
|
155
|
+
explaining that SNI couldn't reach AllAnime and suggesting fixes.
|
|
156
|
+
"""
|
|
157
|
+
if last_error is None:
|
|
158
|
+
return CaptchaRequiredError(f"AllAnime {endpoint} failed with no specific error")
|
|
159
|
+
|
|
160
|
+
if isinstance(last_error, CaptchaRequiredError):
|
|
161
|
+
return last_error
|
|
162
|
+
|
|
163
|
+
# Network errors (ConnectTimeout, ReadTimeout, ConnectError, etc.)
|
|
164
|
+
if isinstance(last_error, (httpx.TimeoutException, httpx.HTTPError)):
|
|
165
|
+
err_name = type(last_error).__name__
|
|
166
|
+
return ProviderError(
|
|
167
|
+
f"SNI could not connect to AllAnime ({err_name} on {endpoint}).\n\n"
|
|
168
|
+
f"This is a NETWORK issue, not a captcha. SNI tried direct + proxy.cors.sh "
|
|
169
|
+
f"but couldn't reach any of them.\n\n"
|
|
170
|
+
f"Possible causes:\n"
|
|
171
|
+
f" 1. Your internet is down or unstable\n"
|
|
172
|
+
f" 2. Your ISP/firewall blocks api.allanime.day or proxy.cors.sh\n"
|
|
173
|
+
f" 3. Your DNS can't resolve these hosts\n"
|
|
174
|
+
f" 4. You're on a restrictive network (school/work/country firewall)\n\n"
|
|
175
|
+
f"Try:\n"
|
|
176
|
+
f" - Check your internet: curl -I https://api.allanime.day\n"
|
|
177
|
+
f" - Try a different network (mobile hotspot, VPN)\n"
|
|
178
|
+
f" - If on a VPN, try WITHOUT it (some VPNs are blocked)\n"
|
|
179
|
+
f" - Check DNS: nslookup api.allanime.day\n"
|
|
180
|
+
f" - Try again in a few minutes (temporary outage)"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Any other exception — wrap it
|
|
184
|
+
return ProviderError(f"AllAnime {endpoint} failed: {type(last_error).__name__}: {last_error}")
|
|
185
|
+
|
|
186
|
+
|
|
149
187
|
class AllAnimeProvider(Provider):
|
|
150
188
|
name = "allanime"
|
|
151
189
|
domain = "allanime.day"
|
|
152
190
|
supports_sub = True
|
|
153
191
|
supports_dub = True
|
|
154
192
|
|
|
155
|
-
# AllAnime API
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# they require zero setup from the user.
|
|
159
|
-
#
|
|
160
|
-
# Format: (api_graphql_url, api_url, allanime_base_url)
|
|
161
|
-
API_MIRRORS = [
|
|
162
|
-
# Primary — most reliable when not captcha-walled
|
|
163
|
-
("https://api.allanime.day/api/graphql",
|
|
164
|
-
"https://api.allanime.day/api",
|
|
165
|
-
"https://allanime.day"),
|
|
166
|
-
# Mirror 1 — allmanga.to (sister site, shared backend)
|
|
167
|
-
("https://api.allmanga.to/api/graphql",
|
|
168
|
-
"https://api.allmanga.to/api",
|
|
169
|
-
"https://allmanga.to"),
|
|
170
|
-
]
|
|
171
|
-
|
|
193
|
+
# AllAnime API endpoints (XAN pattern).
|
|
194
|
+
GRAPHQL_URL = "https://api.allanime.day/api/graphql"
|
|
195
|
+
API_URL = "https://api.allanime.day/api"
|
|
172
196
|
ALLANIME_BASE = "https://allanime.day"
|
|
173
197
|
REFERER = REFERER
|
|
174
198
|
ORIGIN = ORIGIN
|
|
175
199
|
|
|
200
|
+
# Free public CORS proxies that support POST + JSON bodies. SNI tries
|
|
201
|
+
# each one in order when the direct request to api.allanime.day is
|
|
202
|
+
# captcha-walled. As of 2026, most public CORS proxies have shut down
|
|
203
|
+
# or block POST. proxy.cors.sh is the only reliable one left.
|
|
204
|
+
# Format: a format string with a single {url} placeholder.
|
|
205
|
+
PUBLIC_PROXIES = [
|
|
206
|
+
"https://proxy.cors.sh/{url}",
|
|
207
|
+
]
|
|
208
|
+
|
|
176
209
|
def __init__(self, cookies: str = "", cf_worker_url: str = ""):
|
|
177
210
|
self.cookies_str = cookies
|
|
178
211
|
self._cookies: Dict[str, str] = {}
|
|
@@ -183,12 +216,10 @@ class AllAnimeProvider(Provider):
|
|
|
183
216
|
k, v = part.split("=", 1)
|
|
184
217
|
self._cookies[k.strip()] = v.strip()
|
|
185
218
|
self.cf_worker_url = (cf_worker_url or "").rstrip("/")
|
|
186
|
-
# Tracks which
|
|
187
|
-
# calls instead of trying the primary every time.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def _mirror(self, idx: int) -> tuple:
|
|
191
|
-
return self.API_MIRRORS[idx]
|
|
219
|
+
# Tracks which proxy actually worked, so we can reuse it on subsequent
|
|
220
|
+
# calls instead of trying the dead primary every time.
|
|
221
|
+
# None = direct (no proxy), 0 = first public proxy, etc.
|
|
222
|
+
self._working_proxy_idx: Optional[int] = None
|
|
192
223
|
|
|
193
224
|
def _base_headers(self, *, json_body: bool = False) -> Dict[str, str]:
|
|
194
225
|
h = {
|
|
@@ -201,27 +232,39 @@ class AllAnimeProvider(Provider):
|
|
|
201
232
|
h["Content-Type"] = "application/json"
|
|
202
233
|
return h
|
|
203
234
|
|
|
235
|
+
def _proxy_url(self, idx: int, target: str) -> str:
|
|
236
|
+
"""Wrap an upstream URL with the i-th public proxy from PUBLIC_PROXIES."""
|
|
237
|
+
return self.PUBLIC_PROXIES[idx].format(url=target)
|
|
238
|
+
|
|
204
239
|
async def _post_graphql(self, query: str, variables: dict) -> dict:
|
|
205
240
|
"""POST a regular GraphQL query to /api/graphql.
|
|
206
241
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
Fallback order:
|
|
243
|
+
1. Direct request to api.allanime.day (fastest when not captcha-walled)
|
|
244
|
+
2. Each public CORS proxy in PUBLIC_PROXIES (no user config needed)
|
|
245
|
+
3. CF Worker (only if user configured allanime_cf_worker_url — but
|
|
246
|
+
CF Workers are GET-only, so this raises a clear error instead)
|
|
247
|
+
|
|
248
|
+
CaptchaRequiredError is raised only when ALL fallbacks fail.
|
|
211
249
|
"""
|
|
212
250
|
payload = {"query": query, "variables": variables}
|
|
213
251
|
headers = self._base_headers(json_body=True)
|
|
214
252
|
|
|
215
|
-
# Build the list of
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
# Build the list of fallbacks to try.
|
|
254
|
+
# Each entry: ("direct"|"proxy"|"worker", url)
|
|
255
|
+
fallbacks: List[tuple] = [("direct", self.GRAPHQL_URL)]
|
|
256
|
+
for i in range(len(self.PUBLIC_PROXIES)):
|
|
257
|
+
fallbacks.append(("proxy", self._proxy_url(i, self.GRAPHQL_URL)))
|
|
258
|
+
|
|
259
|
+
# If a proxy previously worked, try it first (skip the dead direct)
|
|
260
|
+
if self._working_proxy_idx is not None:
|
|
261
|
+
# Move the working proxy to the front
|
|
262
|
+
working = fallbacks[self._working_proxy_idx + 1] # +1 because [0] is direct
|
|
263
|
+
fallbacks.remove(working)
|
|
264
|
+
fallbacks.insert(0, working)
|
|
221
265
|
|
|
222
266
|
last_error: Optional[Exception] = None
|
|
223
|
-
for
|
|
224
|
-
graphql_url, _api_url, _base_url = self._mirror(idx)
|
|
267
|
+
for kind, url in fallbacks:
|
|
225
268
|
async with httpx.AsyncClient(
|
|
226
269
|
headers=headers,
|
|
227
270
|
cookies=self._cookies,
|
|
@@ -229,17 +272,14 @@ class AllAnimeProvider(Provider):
|
|
|
229
272
|
timeout=REQUEST_TIMEOUT,
|
|
230
273
|
) as client:
|
|
231
274
|
try:
|
|
232
|
-
resp = await client.post(
|
|
275
|
+
resp = await client.post(url, json=payload)
|
|
233
276
|
except (httpx.HTTPError, httpx.TimeoutException) as e:
|
|
234
|
-
# Network error — try next mirror
|
|
235
277
|
last_error = e
|
|
236
278
|
continue
|
|
237
279
|
|
|
238
280
|
if _is_captcha_response(resp):
|
|
239
|
-
# This mirror is captcha-walled — try the next one
|
|
240
281
|
last_error = CaptchaRequiredError(
|
|
241
|
-
f"AllAnime
|
|
242
|
-
f"HTTP {resp.status_code}",
|
|
282
|
+
f"AllAnime {kind} request returned HTTP {resp.status_code}",
|
|
243
283
|
)
|
|
244
284
|
continue
|
|
245
285
|
|
|
@@ -247,7 +287,7 @@ class AllAnimeProvider(Provider):
|
|
|
247
287
|
raw = resp.json()
|
|
248
288
|
except (ValueError, json.JSONDecodeError):
|
|
249
289
|
last_error = ProviderError(
|
|
250
|
-
f"
|
|
290
|
+
f"{kind} returned non-JSON: {resp.text[:200]}",
|
|
251
291
|
)
|
|
252
292
|
continue
|
|
253
293
|
|
|
@@ -257,38 +297,53 @@ class AllAnimeProvider(Provider):
|
|
|
257
297
|
last_error = e
|
|
258
298
|
continue
|
|
259
299
|
|
|
260
|
-
# Success — remember
|
|
261
|
-
|
|
300
|
+
# Success — remember which fallback worked
|
|
301
|
+
if kind == "direct":
|
|
302
|
+
self._working_proxy_idx = None
|
|
303
|
+
elif kind == "proxy":
|
|
304
|
+
self._working_proxy_idx = self.PUBLIC_PROXIES.index(
|
|
305
|
+
next(p for p in self.PUBLIC_PROXIES if url.startswith(p.split("{")[0]))
|
|
306
|
+
)
|
|
262
307
|
return raw
|
|
263
308
|
|
|
264
|
-
# All
|
|
265
|
-
raise last_error
|
|
266
|
-
"All AllAnime API mirrors failed for /api/graphql",
|
|
267
|
-
)
|
|
309
|
+
# All fallbacks failed — wrap the error in a user-friendly message
|
|
310
|
+
raise _wrap_fallback_error(last_error, "/api/graphql")
|
|
268
311
|
|
|
269
312
|
async def _get_persisted(self, variables: dict) -> dict:
|
|
270
313
|
"""GET /api?variables=...&extensions=persistedQuery...
|
|
271
314
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
315
|
+
Fallback order:
|
|
316
|
+
1. Direct request to api.allanime.day
|
|
317
|
+
2. Each public CORS proxy in PUBLIC_PROXIES
|
|
318
|
+
3. CF Worker (only if user configured allanime_cf_worker_url)
|
|
319
|
+
|
|
320
|
+
CaptchaRequiredError is raised only when ALL fallbacks fail.
|
|
275
321
|
"""
|
|
276
322
|
extensions = {"persistedQuery": {"version": 1, "sha256Hash": EPISODE_QUERY_HASH}}
|
|
277
323
|
params = {
|
|
278
324
|
"variables": json.dumps(variables),
|
|
279
325
|
"extensions": json.dumps(extensions),
|
|
280
326
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
327
|
+
direct_url = f"{self.API_URL}?{urlencode(params)}"
|
|
328
|
+
|
|
329
|
+
# Build the list of fallbacks to try
|
|
330
|
+
fallbacks: List[tuple] = [("direct", direct_url)]
|
|
331
|
+
for i in range(len(self.PUBLIC_PROXIES)):
|
|
332
|
+
fallbacks.append(("proxy", self._proxy_url(i, direct_url)))
|
|
333
|
+
if self.cf_worker_url:
|
|
334
|
+
fallbacks.append(("worker", _build_cf_worker_url(
|
|
335
|
+
self.cf_worker_url, direct_url,
|
|
336
|
+
extra_headers={"Referer": self.REFERER, "Origin": self.ORIGIN},
|
|
337
|
+
)))
|
|
338
|
+
|
|
339
|
+
# If a proxy previously worked, try it first
|
|
340
|
+
if self._working_proxy_idx is not None:
|
|
341
|
+
working = fallbacks[self._working_proxy_idx + 1]
|
|
342
|
+
fallbacks.remove(working)
|
|
343
|
+
fallbacks.insert(0, working)
|
|
286
344
|
|
|
287
345
|
last_error: Optional[Exception] = None
|
|
288
|
-
for
|
|
289
|
-
_gql_url, api_url, _base_url = self._mirror(idx)
|
|
290
|
-
direct_url = f"{api_url}?{urlencode(params)}"
|
|
291
|
-
|
|
346
|
+
for kind, url in fallbacks:
|
|
292
347
|
async with httpx.AsyncClient(
|
|
293
348
|
headers=self._base_headers(),
|
|
294
349
|
cookies=self._cookies,
|
|
@@ -296,33 +351,14 @@ class AllAnimeProvider(Provider):
|
|
|
296
351
|
timeout=REQUEST_TIMEOUT,
|
|
297
352
|
) as client:
|
|
298
353
|
try:
|
|
299
|
-
resp = await client.get(
|
|
354
|
+
resp = await client.get(url)
|
|
300
355
|
except (httpx.HTTPError, httpx.TimeoutException) as e:
|
|
301
356
|
last_error = e
|
|
302
357
|
continue
|
|
303
358
|
|
|
304
359
|
if _is_captcha_response(resp):
|
|
305
|
-
# Try CF Worker fallback for this mirror (only the
|
|
306
|
-
# persisted-query GET can go through a CF Worker — the
|
|
307
|
-
# GraphQL POST can't because Workers are GET-only).
|
|
308
|
-
if self.cf_worker_url:
|
|
309
|
-
wrapped = _build_cf_worker_url(
|
|
310
|
-
self.cf_worker_url, direct_url,
|
|
311
|
-
extra_headers={"Referer": self.REFERER, "Origin": self.ORIGIN},
|
|
312
|
-
)
|
|
313
|
-
try:
|
|
314
|
-
cf_resp = await client.get(wrapped)
|
|
315
|
-
cf_ct = cf_resp.headers.get("content-type", "").lower()
|
|
316
|
-
if cf_resp.status_code < 400 and "json" in cf_ct:
|
|
317
|
-
raw = cf_resp.json()
|
|
318
|
-
self._check_graphql_errors(raw)
|
|
319
|
-
self._working_mirror_idx = idx
|
|
320
|
-
return raw
|
|
321
|
-
except (httpx.HTTPError, json.JSONDecodeError):
|
|
322
|
-
pass
|
|
323
360
|
last_error = CaptchaRequiredError(
|
|
324
|
-
f"AllAnime
|
|
325
|
-
f"HTTP {resp.status_code}",
|
|
361
|
+
f"AllAnime {kind} request returned HTTP {resp.status_code}",
|
|
326
362
|
)
|
|
327
363
|
continue
|
|
328
364
|
|
|
@@ -330,7 +366,7 @@ class AllAnimeProvider(Provider):
|
|
|
330
366
|
raw = resp.json()
|
|
331
367
|
except (ValueError, json.JSONDecodeError):
|
|
332
368
|
last_error = ProviderError(
|
|
333
|
-
f"
|
|
369
|
+
f"{kind} returned non-JSON: {resp.text[:200]}",
|
|
334
370
|
)
|
|
335
371
|
continue
|
|
336
372
|
|
|
@@ -340,12 +376,15 @@ class AllAnimeProvider(Provider):
|
|
|
340
376
|
last_error = e
|
|
341
377
|
continue
|
|
342
378
|
|
|
343
|
-
|
|
379
|
+
if kind == "direct":
|
|
380
|
+
self._working_proxy_idx = None
|
|
381
|
+
elif kind == "proxy":
|
|
382
|
+
self._working_proxy_idx = self.PUBLIC_PROXIES.index(
|
|
383
|
+
next(p for p in self.PUBLIC_PROXIES if url.startswith(p.split("{")[0]))
|
|
384
|
+
)
|
|
344
385
|
return raw
|
|
345
386
|
|
|
346
|
-
raise last_error
|
|
347
|
-
"All AllAnime API mirrors failed for /api persisted query",
|
|
348
|
-
)
|
|
387
|
+
raise _wrap_fallback_error(last_error, "/api persisted query")
|
|
349
388
|
|
|
350
389
|
def _check_graphql_errors(self, raw: dict) -> None:
|
|
351
390
|
"""Inspect a parsed GraphQL response for errors, raising the right
|
|
@@ -593,14 +632,10 @@ class AllAnimeProvider(Provider):
|
|
|
593
632
|
(HLS or MP4) with resolution labels. We pick the highest-priority one.
|
|
594
633
|
"""
|
|
595
634
|
full_path = path.replace("/clock", "/clock.json")
|
|
596
|
-
#
|
|
597
|
-
if self._working_mirror_idx is not None:
|
|
598
|
-
_g, _a, base_url = self._mirror(self._working_mirror_idx)
|
|
599
|
-
else:
|
|
600
|
-
base_url = self.ALLANIME_BASE
|
|
635
|
+
# clock.json paths are relative to allanime.day itself (not the API)
|
|
601
636
|
full_url = (
|
|
602
637
|
full_path if full_path.startswith("http")
|
|
603
|
-
else f"{
|
|
638
|
+
else f"{self.ALLANIME_BASE}{full_path}"
|
|
604
639
|
)
|
|
605
640
|
|
|
606
641
|
async with httpx.AsyncClient(
|
sni_cli-1.2.0/sni/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|