sni-cli 1.2.1__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.1 → sni_cli-1.2.2}/PKG-INFO +1 -1
- {sni_cli-1.2.1 → sni_cli-1.2.2}/pyproject.toml +1 -1
- sni_cli-1.2.2/sni/__init__.py +1 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/cli.py +11 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/allanime.py +46 -13
- sni_cli-1.2.1/sni/__init__.py +0 -1
- {sni_cli-1.2.1 → sni_cli-1.2.2}/.gitignore +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/LICENSE +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/README.md +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/install.ps1 +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/install.sh +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/__main__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/config.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/exceptions.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/logger.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/player.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/__init__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/base.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/cache.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/extractors/__init__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/extractors/megacloud.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/extractors/vixcloud.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/registry.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/__init__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/app.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/bridge.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/__init__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/help.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/history.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/home.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/player.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/widgets/__init__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/widgets/ascii_art.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/widgets/info_box.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/ui.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/watch_history.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/wizard.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/__init__.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/conftest.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_allanime_captcha.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_cache.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_config.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_exceptions.py +0 -0
- {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_providers.py +0 -0
|
@@ -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.")
|
|
@@ -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,6 +146,44 @@ 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"
|
|
@@ -161,12 +199,11 @@ class AllAnimeProvider(Provider):
|
|
|
161
199
|
|
|
162
200
|
# Free public CORS proxies that support POST + JSON bodies. SNI tries
|
|
163
201
|
# each one in order when the direct request to api.allanime.day is
|
|
164
|
-
# captcha-walled.
|
|
165
|
-
#
|
|
166
|
-
#
|
|
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.
|
|
167
205
|
PUBLIC_PROXIES = [
|
|
168
206
|
"https://proxy.cors.sh/{url}",
|
|
169
|
-
"https://thingproxy.freeboard.io/fetch/{url}",
|
|
170
207
|
]
|
|
171
208
|
|
|
172
209
|
def __init__(self, cookies: str = "", cf_worker_url: str = ""):
|
|
@@ -269,10 +306,8 @@ class AllAnimeProvider(Provider):
|
|
|
269
306
|
)
|
|
270
307
|
return raw
|
|
271
308
|
|
|
272
|
-
# All fallbacks failed
|
|
273
|
-
raise last_error
|
|
274
|
-
"All AllAnime API fallbacks failed for /api/graphql",
|
|
275
|
-
)
|
|
309
|
+
# All fallbacks failed — wrap the error in a user-friendly message
|
|
310
|
+
raise _wrap_fallback_error(last_error, "/api/graphql")
|
|
276
311
|
|
|
277
312
|
async def _get_persisted(self, variables: dict) -> dict:
|
|
278
313
|
"""GET /api?variables=...&extensions=persistedQuery...
|
|
@@ -349,9 +384,7 @@ class AllAnimeProvider(Provider):
|
|
|
349
384
|
)
|
|
350
385
|
return raw
|
|
351
386
|
|
|
352
|
-
raise last_error
|
|
353
|
-
"All AllAnime API fallbacks failed for /api persisted query",
|
|
354
|
-
)
|
|
387
|
+
raise _wrap_fallback_error(last_error, "/api persisted query")
|
|
355
388
|
|
|
356
389
|
def _check_graphql_errors(self, raw: dict) -> None:
|
|
357
390
|
"""Inspect a parsed GraphQL response for errors, raising the right
|
sni_cli-1.2.1/sni/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.1"
|
|
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
|
|
File without changes
|
|
File without changes
|