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.
- {cinnamon_cli-0.2.27/cinnamon_cli.egg-info → cinnamon_cli-0.2.29}/PKG-INFO +3 -3
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/README.md +2 -2
- cinnamon_cli-0.2.29/cinnamon/__init__.py +1 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/cli.py +2 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/player.py +3 -1
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/anime.py +58 -7
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/webstream.py +29 -12
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29/cinnamon_cli.egg-info}/PKG-INFO +3 -3
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/pyproject.toml +1 -1
- cinnamon_cli-0.2.27/cinnamon/__init__.py +0 -1
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/LICENSE +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/anilist.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/config.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/downloads.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/errors.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/history.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/__init__.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/base.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/torrentio.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/scrapers/vidsrc.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/tmdb.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon/tui.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/SOURCES.txt +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/dependency_links.txt +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/entry_points.txt +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/requires.txt +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/cinnamon_cli.egg-info/top_level.txt +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/setup.cfg +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream2.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream3.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream4.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream5.py +0 -0
- {cinnamon_cli-0.2.27 → cinnamon_cli-0.2.29}/tests/test_directstream6.py +0 -0
- {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.
|
|
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" --
|
|
82
|
-
cinnamon anime "Frieren" --
|
|
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" --
|
|
53
|
-
cinnamon anime "Frieren" --
|
|
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 = [
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
64
|
+
result = vixsrc_fn(tmdb_id, quality)
|
|
56
65
|
if result:
|
|
57
|
-
label = quality.upper() if quality else "
|
|
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=
|
|
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 =
|
|
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} (
|
|
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 =
|
|
102
|
+
result = vixsrc_fn(tmdb_id, season, episode, quality)
|
|
87
103
|
if result:
|
|
88
|
-
label = quality.upper() if quality else "
|
|
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=
|
|
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 =
|
|
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} (
|
|
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.
|
|
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" --
|
|
82
|
-
cinnamon anime "Frieren" --
|
|
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.
|
|
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
|
|
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
|