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.
Files changed (44) hide show
  1. {sni_cli-1.2.1 → sni_cli-1.2.2}/PKG-INFO +1 -1
  2. {sni_cli-1.2.1 → sni_cli-1.2.2}/pyproject.toml +1 -1
  3. sni_cli-1.2.2/sni/__init__.py +1 -0
  4. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/cli.py +11 -0
  5. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/allanime.py +46 -13
  6. sni_cli-1.2.1/sni/__init__.py +0 -1
  7. {sni_cli-1.2.1 → sni_cli-1.2.2}/.gitignore +0 -0
  8. {sni_cli-1.2.1 → sni_cli-1.2.2}/LICENSE +0 -0
  9. {sni_cli-1.2.1 → sni_cli-1.2.2}/README.md +0 -0
  10. {sni_cli-1.2.1 → sni_cli-1.2.2}/install.ps1 +0 -0
  11. {sni_cli-1.2.1 → sni_cli-1.2.2}/install.sh +0 -0
  12. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/__main__.py +0 -0
  13. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/config.py +0 -0
  14. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/exceptions.py +0 -0
  15. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/logger.py +0 -0
  16. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/player.py +0 -0
  17. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/__init__.py +0 -0
  18. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/base.py +0 -0
  19. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/cache.py +0 -0
  20. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/extractors/__init__.py +0 -0
  21. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/extractors/megacloud.py +0 -0
  22. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/extractors/vixcloud.py +0 -0
  23. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/providers/registry.py +0 -0
  24. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/__init__.py +0 -0
  25. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/app.py +0 -0
  26. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/bridge.py +0 -0
  27. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/__init__.py +0 -0
  28. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/help.py +0 -0
  29. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/history.py +0 -0
  30. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/home.py +0 -0
  31. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/screens/player.py +0 -0
  32. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/widgets/__init__.py +0 -0
  33. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/widgets/ascii_art.py +0 -0
  34. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/tui/widgets/info_box.py +0 -0
  35. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/ui.py +0 -0
  36. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/watch_history.py +0 -0
  37. {sni_cli-1.2.1 → sni_cli-1.2.2}/sni/wizard.py +0 -0
  38. {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/__init__.py +0 -0
  39. {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/conftest.py +0 -0
  40. {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_allanime_captcha.py +0 -0
  41. {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_cache.py +0 -0
  42. {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_config.py +0 -0
  43. {sni_cli-1.2.1 → sni_cli-1.2.2}/tests/test_exceptions.py +0 -0
  44. {sni_cli-1.2.1 → 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.1
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
@@ -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.1"
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 = 15.0
62
- CLOCK_TIMEOUT = 20.0
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. proxy.cors.sh is the most reliable as of 2026; the
165
- # others are kept as backup in case cors.sh ever rate-limits or goes
166
- # down. Format: a format string with a single {url} placeholder.
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 or CaptchaRequiredError(
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 or CaptchaRequiredError(
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
@@ -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