cinnamon-cli 0.2.27__tar.gz → 0.2.29__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 (35) hide show
  1. {cinnamon_cli-0.2.27/cinnamon_cli.egg-info → cinnamon_cli-0.2.29}/PKG-INFO +3 -3
  2. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/README.md +2 -2
  3. cinnamon_cli-0.2.29/cinnamon/__init__.py +1 -0
  4. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/cli.py +2 -0
  5. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/player.py +3 -1
  6. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/anime.py +58 -7
  7. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/webstream.py +29 -12
  8. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29/cinnamon_cli.egg-info}/PKG-INFO +3 -3
  9. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/pyproject.toml +1 -1
  10. cinnamon_cli-0.2.27/cinnamon/__init__.py +0 -1
  11. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/LICENSE +0 -0
  12. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/anilist.py +0 -0
  13. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/config.py +0 -0
  14. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/downloads.py +0 -0
  15. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/errors.py +0 -0
  16. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/history.py +0 -0
  17. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/__init__.py +0 -0
  18. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/base.py +0 -0
  19. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/torrentio.py +0 -0
  20. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/vidsrc.py +0 -0
  21. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/tmdb.py +0 -0
  22. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/tui.py +0 -0
  23. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/SOURCES.txt +0 -0
  24. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/dependency_links.txt +0 -0
  25. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/entry_points.txt +0 -0
  26. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/requires.txt +0 -0
  27. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/top_level.txt +0 -0
  28. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/setup.cfg +0 -0
  29. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream.py +0 -0
  30. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream2.py +0 -0
  31. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream3.py +0 -0
  32. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream4.py +0 -0
  33. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream5.py +0 -0
  34. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream6.py +0 -0
  35. {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_ui_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinnamon-cli
3
- Version: 0.2.27
3
+ Version: 0.2.29
4
4
  Summary: CLI to search TV shows, movies & anime via TMDB/AniList, scrape m3u8 links, and play in VLC/mpv
5
5
  Author: pizza-droid
6
6
  License: CC BY-NC 4.0
@@ -78,8 +78,8 @@ By default you'll be asked whether to watch sub or dub. Skip the prompt with
78
78
  `-sub` / `-dub`:
79
79
 
80
80
  ```bash
81
- cinnamon anime "Chainsaw Man" --dub
82
- cinnamon anime "Frieren" --sub
81
+ cinnamon anime "Chainsaw Man" --sub
82
+ cinnamon anime "Frieren" --dub
83
83
  ```
84
84
 
85
85
 
@@ -49,8 +49,8 @@ By default you'll be asked whether to watch sub or dub. Skip the prompt with
49
49
  `-sub` / `-dub`:
50
50
 
51
51
  ```bash
52
- cinnamon anime "Chainsaw Man" --dub
53
- cinnamon anime "Frieren" --sub
52
+ cinnamon anime "Chainsaw Man" --sub
53
+ cinnamon anime "Frieren" --dub
54
54
  ```
55
55
 
56
56
 
@@ -0,0 +1 @@
1
+ __version__ = "0.2.29"
@@ -531,6 +531,8 @@ def _resolve_and_play(show, season_num, ep_num, ep_name, scraper, player, qualit
531
531
  info["quality"] = quality
532
532
  if translation:
533
533
  info["translation"] = translation
534
+ if download:
535
+ info["download"] = True
534
536
  future = pool.submit(attempt.resolve, info)
535
537
  result = future.result(timeout=RESOLVE_TIMEOUT)
536
538
  if result:
@@ -470,7 +470,9 @@ def download_video(url, title="", referer=None, output_dir=".", track_id=None):
470
470
  safe = "".join(c if c.isalnum() or c in " .-_()" else "_" for c in title) or "video"
471
471
  outtmpl = os.path.join(output_dir, f"{safe}.%(ext)s")
472
472
 
473
- cmd = [exe, "--no-mtime", "--no-warnings", "-o", outtmpl]
473
+ cmd = [
474
+ exe, "--no-mtime", "--no-warnings", "-o", outtmpl,
475
+ ]
474
476
  if referer:
475
477
  cmd += ["--referer", referer]
476
478
  cmd.append(url)
@@ -2,15 +2,60 @@ import base64
2
2
  import hashlib
3
3
  import json
4
4
  import re
5
+ import socket
5
6
  import time as _time
7
+ import contextlib
6
8
  from typing import Optional, Tuple
7
9
 
8
10
  import requests
11
+ from requests.adapters import HTTPAdapter
12
+ from urllib3.util.retry import Retry
9
13
 
10
14
  from ..errors import ScraperNetworkError, ScraperNoStreamError, ScraperParseError
11
15
  from ..player import DEFAULT_UA as UA
12
16
  from .base import BaseScraper, ScraperResult
13
17
 
18
+
19
+ @contextlib.contextmanager
20
+ def _force_ipv4():
21
+ """Temporarily make getaddrinfo prefer IPv4.
22
+
23
+ allanime's API is served behind Cloudflare which publishes AAAA (IPv6)
24
+ records first. On hosts without working IPv6, Python's getaddrinfo can
25
+ intermittently fail with 'Errno 11002 getaddrinfo failed', so we bias
26
+ resolution toward IPv4 for the duration of a request.
27
+ """
28
+ orig = socket.getaddrinfo
29
+
30
+ def _prefer_v4(host, port, family=socket.AF_UNSPEC, type=0, proto=0, flags=0):
31
+ try:
32
+ return orig(host, port, socket.AF_INET, type, proto, flags)
33
+ except socket.gaierror:
34
+ return orig(host, port, family, type, proto, flags)
35
+
36
+ socket.getaddrinfo = _prefer_v4
37
+ try:
38
+ yield
39
+ finally:
40
+ socket.getaddrinfo = orig
41
+
42
+
43
+ def _new_session(ua=UA, origin="https://youtu-chan.com"):
44
+ """Build a requests.Session with retries and IPv4-biased DNS."""
45
+ s = requests.Session()
46
+ s.headers.update({"User-Agent": ua, "Origin": origin})
47
+ retry = Retry(
48
+ total=3,
49
+ backoff_factor=1.0,
50
+ status_forcelist=[500, 502, 503, 504],
51
+ allowed_methods=frozenset(["GET", "POST"]),
52
+ raise_on_status=False,
53
+ )
54
+ adapter = HTTPAdapter(max_retries=retry, pool_connections=4, pool_maxsize=4)
55
+ s.mount("https://", adapter)
56
+ s.mount("http://", adapter)
57
+ return s
58
+
14
59
  TIMEOUT = 15
15
60
  API = "https://api.allanime.day/api"
16
61
 
@@ -84,8 +129,7 @@ def _aa_source_query_hash(chunk_js: str) -> Optional[str]:
84
129
  def _aa_fetch():
85
130
  """Fetch (expires_ms, epoch, key, mask, query_hash) from the live site."""
86
131
  try:
87
- s = requests.Session()
88
- s.headers.update({"User-Agent": _BROWSER_UA})
132
+ s = _new_session(ua=_BROWSER_UA, origin="https://mkissa.to")
89
133
  html = s.get(_MKISSA_URL, timeout=10).text
90
134
  aa = json.loads(re.search(r"window\.__aaCrypto\s*=\s*(\{.*?\})", html).group(1))
91
135
  part_b, epoch = aa["partB"], int(aa["epoch"])
@@ -179,7 +223,11 @@ def _gql(session, query, variables):
179
223
  if elapsed < 3.5:
180
224
  _time.sleep(3.5 - elapsed)
181
225
  body = json.dumps({"variables": json.dumps(variables), "query": query}, ensure_ascii=False, separators=(",", ":"))
182
- resp = session.post(API, data=body, timeout=TIMEOUT, headers={"Content-Type": "application/json"})
226
+ try:
227
+ with _force_ipv4():
228
+ resp = session.post(API, data=body, timeout=TIMEOUT, headers={"Content-Type": "application/json"})
229
+ except (requests.ConnectionError, requests.Timeout) as e:
230
+ raise ScraperNetworkError("allanime", f"Could not reach allanime API (check your connection / DNS): {e}")
183
231
  _LAST_REQUEST = _time.time()
184
232
  if resp.status_code != 200:
185
233
  raise ScraperNetworkError("allanime", f"GraphQL returned {resp.status_code}")
@@ -199,7 +247,11 @@ def _gql_src(session, variables, query_hash=None, aa_req=None):
199
247
  {"extensions": extensions, "variables": json.dumps(variables)},
200
248
  ensure_ascii=False, separators=(",", ":"),
201
249
  )
202
- resp = session.post(API, data=body, timeout=TIMEOUT, headers={"Content-Type": "application/json"})
250
+ try:
251
+ with _force_ipv4():
252
+ resp = session.post(API, data=body, timeout=TIMEOUT, headers={"Content-Type": "application/json"})
253
+ except (requests.ConnectionError, requests.Timeout) as e:
254
+ raise ScraperNetworkError("allanime", f"Could not reach allanime API (check your connection / DNS): {e}")
203
255
  _LAST_REQUEST = _time.time()
204
256
  if resp.status_code != 200:
205
257
  raise ScraperNetworkError("allanime", f"Source GQL returned {resp.status_code}")
@@ -274,8 +326,7 @@ class AnimeScraper(BaseScraper):
274
326
  raise ScraperParseError(self.name, "Missing show name in episode_info")
275
327
 
276
328
  deadline = _time.time() + 30
277
- session = requests.Session()
278
- session.headers.update({"User-Agent": UA, "Origin": "https://youtu-chan.com"})
329
+ session = _new_session()
279
330
 
280
331
  if _time.time() >= deadline:
281
332
  raise ScraperNoStreamError(self.name, "Timeout searching allanime")
@@ -301,7 +352,7 @@ class AnimeScraper(BaseScraper):
301
352
  try:
302
353
  direct = _extract_mp4upload(session, url)
303
354
  if direct:
304
- label = quality.upper() if quality else "Mp4upload"
355
+ label = "Dub" if translation == "dub" else "Sub"
305
356
  return ScraperResult(
306
357
  title=f"{show_name} E{episode:02d} ({label})",
307
358
  m3u8_url=direct,
@@ -42,6 +42,9 @@ class WebStreamScraper(BaseScraper):
42
42
  season = episode_info.get("season", 1)
43
43
  episode = episode_info.get("episode", 1)
44
44
  quality = episode_info.get("quality", "")
45
+ # vidlink uses a different CDN that is not throttled like vixsrc's
46
+ # segment edge, so prefer it when downloading to avoid 503 stalls.
47
+ prefer_vidlink = bool(episode_info.get("download"))
45
48
 
46
49
  if not tmdb_id:
47
50
  raise ScraperParseError(self.name, "Missing tmdb_id/movie_id/tv_id in episode_info")
@@ -49,28 +52,34 @@ class WebStreamScraper(BaseScraper):
49
52
  deadline = _time.time() + 30
50
53
 
51
54
  if media_type == "movie":
52
- # Try vixsrc.to first
55
+ if prefer_vidlink:
56
+ vixsrc_fn, vidlink_fn = _try_vidlink_movie, _try_vixsrc_movie
57
+ vixsrc_ref = f"https://vixsrc.to/embed/movie/{tmdb_id}"
58
+ else:
59
+ vixsrc_fn, vidlink_fn = _try_vixsrc_movie, _try_vidlink_movie
60
+ vixsrc_ref = f"https://vixsrc.to/embed/movie/{tmdb_id}"
61
+
53
62
  if _time.time() < deadline:
54
63
  try:
55
- result = _try_vixsrc_movie(tmdb_id, quality)
64
+ result = vixsrc_fn(tmdb_id, quality)
56
65
  if result:
57
- label = quality.upper() if quality else "vixsrc"
66
+ label = quality.upper() if quality else "Auto"
58
67
  return ScraperResult(
59
68
  title=f"{show} ({label})",
60
69
  m3u8_url=result,
61
- referer=f"https://vixsrc.to/embed/movie/{tmdb_id}",
70
+ referer=vixsrc_ref,
62
71
  user_agent=UA,
63
72
  )
64
73
  except ScraperNetworkError:
65
74
  pass
66
75
 
67
- # Try vidlink.pro as fallback
68
76
  if _time.time() < deadline:
69
77
  try:
70
- result = _try_vidlink_movie(tmdb_id)
78
+ result = vidlink_fn(tmdb_id)
71
79
  if result:
80
+ label = quality.upper() if quality else "Auto"
72
81
  return ScraperResult(
73
- title=f"{show} (vidlink)",
82
+ title=f"{show} ({label})",
74
83
  m3u8_url=result,
75
84
  referer="https://vidlink.pro/",
76
85
  user_agent=UA,
@@ -81,15 +90,22 @@ class WebStreamScraper(BaseScraper):
81
90
  raise ScraperNoStreamError(self.name, f"No HTTP stream found for {show}")
82
91
 
83
92
  # TV
93
+ if prefer_vidlink:
94
+ vixsrc_fn, vidlink_fn = _try_vidlink, _try_vixsrc
95
+ vixsrc_ref = f"https://vixsrc.to/embed/tv/{tmdb_id}/{season}/{episode}"
96
+ else:
97
+ vixsrc_fn, vidlink_fn = _try_vixsrc, _try_vidlink
98
+ vixsrc_ref = f"https://vixsrc.to/embed/tv/{tmdb_id}/{season}/{episode}"
99
+
84
100
  if _time.time() < deadline:
85
101
  try:
86
- result = _try_vixsrc(tmdb_id, season, episode, quality)
102
+ result = vixsrc_fn(tmdb_id, season, episode, quality)
87
103
  if result:
88
- label = quality.upper() if quality else "vixsrc"
104
+ label = quality.upper() if quality else "Auto"
89
105
  return ScraperResult(
90
106
  title=f"{show} S{season:02d}E{episode:02d} ({label})",
91
107
  m3u8_url=result,
92
- referer=f"https://vixsrc.to/embed/tv/{tmdb_id}/{season}/{episode}",
108
+ referer=vixsrc_ref,
93
109
  user_agent=UA,
94
110
  )
95
111
  except ScraperNetworkError:
@@ -97,10 +113,11 @@ class WebStreamScraper(BaseScraper):
97
113
 
98
114
  if _time.time() < deadline:
99
115
  try:
100
- result = _try_vidlink(tmdb_id, season, episode)
116
+ result = vidlink_fn(tmdb_id, season, episode)
101
117
  if result:
118
+ label = quality.upper() if quality else "Auto"
102
119
  return ScraperResult(
103
- title=f"{show} S{season:02d}E{episode:02d} (vidlink)",
120
+ title=f"{show} S{season:02d}E{episode:02d} ({label})",
104
121
  m3u8_url=result,
105
122
  referer="https://vidlink.pro/",
106
123
  user_agent=UA,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinnamon-cli
3
- Version: 0.2.27
3
+ Version: 0.2.29
4
4
  Summary: CLI to search TV shows, movies & anime via TMDB/AniList, scrape m3u8 links, and play in VLC/mpv
5
5
  Author: pizza-droid
6
6
  License: CC BY-NC 4.0
@@ -78,8 +78,8 @@ By default you'll be asked whether to watch sub or dub. Skip the prompt with
78
78
  `-sub` / `-dub`:
79
79
 
80
80
  ```bash
81
- cinnamon anime "Chainsaw Man" --dub
82
- cinnamon anime "Frieren" --sub
81
+ cinnamon anime "Chainsaw Man" --sub
82
+ cinnamon anime "Frieren" --dub
83
83
  ```
84
84
 
85
85
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cinnamon-cli"
7
- version = "0.2.27"
7
+ version = "0.2.29"
8
8
  description = "CLI to search TV shows, movies & anime via TMDB/AniList, scrape m3u8 links, and play in VLC/mpv"
9
9
  license = { text = "CC BY-NC 4.0" }
10
10
  readme = "README.md"
@@ -1 +0,0 @@
1
- __version__ = "0.2.27"
File without changes
File without changes