cinnamon-cli 0.2.25__tar.gz → 0.2.27__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.25/cinnamon_cli.egg-info → cinnamon_cli-0.2.27}/PKG-INFO +25 -13
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/README.md +24 -12
- cinnamon_cli-0.2.27/cinnamon/__init__.py +1 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/cli.py +75 -72
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/player.py +10 -5
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/scrapers/webstream.py +47 -20
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27/cinnamon_cli.egg-info}/PKG-INFO +25 -13
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/pyproject.toml +1 -1
- cinnamon_cli-0.2.25/cinnamon/__init__.py +0 -1
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/LICENSE +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/anilist.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/config.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/downloads.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/errors.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/history.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/scrapers/__init__.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/scrapers/anime.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/scrapers/base.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/scrapers/torrentio.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/scrapers/vidsrc.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/tmdb.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon/tui.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/SOURCES.txt +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/dependency_links.txt +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/entry_points.txt +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/requires.txt +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/top_level.txt +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/setup.cfg +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/tests/test_directstream.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/tests/test_directstream2.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/tests/test_directstream3.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/tests/test_directstream4.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/tests/test_directstream5.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/tests/test_directstream6.py +0 -0
- {cinnamon_cli-0.2.25 → cinnamon_cli-0.2.27}/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.27
|
|
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
|
|
@@ -29,9 +29,9 @@ Dynamic: license-file
|
|
|
29
29
|
|
|
30
30
|
# cinnamon
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
|
|
33
33
|
|
|
34
|
-
[](https://pypi.org/project/cinnamon-cli/)
|
|
35
35
|
[](#)
|
|
36
36
|
[](#)
|
|
37
37
|
[](#)
|
|
@@ -47,7 +47,9 @@ pip install cinnamon-cli
|
|
|
47
47
|
cinnamon setup
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The setup wizard asks for a TMDB API key ([free account](https://www.themoviedb.org/signup)),
|
|
50
|
+
> The setup wizard asks for a TMDB API key ([Create a free account here](https://www.themoviedb.org/signup)), the api key is used for show and movie information.
|
|
51
|
+
|
|
52
|
+
|
|
51
53
|
|
|
52
54
|
### Get a player
|
|
53
55
|
> Please note that MacOS version has not been tested since we dont have access to a mac
|
|
@@ -63,35 +65,43 @@ The setup wizard asks for a TMDB API key ([free account](https://www.themoviedb.
|
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
cinnamon search "Breaking Bad"
|
|
68
|
+
cinnamon search "Inception"
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
For Anime you can run (Works even if you dont have an api key):
|
|
69
72
|
|
|
70
73
|
```bash
|
|
71
|
-
cinnamon
|
|
74
|
+
cinnamon anime "Chainsaw Man"
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
By default you'll be asked whether to watch sub or dub. Skip the prompt with
|
|
78
|
+
`-sub` / `-dub`:
|
|
75
79
|
|
|
76
80
|
```bash
|
|
77
|
-
cinnamon
|
|
78
|
-
cinnamon
|
|
81
|
+
cinnamon anime "Chainsaw Man" --dub
|
|
82
|
+
cinnamon anime "Frieren" --sub
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
### Download episodes
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
you can also run it like this:
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
|
-
|
|
89
|
+
cinnamon Inception
|
|
90
|
+
cinnamon Breaking bad
|
|
91
|
+
cinnamon chainsaw man
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
### Download episodes
|
|
95
|
+
|
|
96
|
+
Downloading needs [yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
|
97
|
+
|
|
90
98
|
|
|
91
99
|
```bash
|
|
92
100
|
pip install yt-dlp
|
|
93
101
|
```
|
|
94
102
|
|
|
103
|
+
Now you can run:
|
|
104
|
+
|
|
95
105
|
```bash
|
|
96
106
|
cinnamon search "Breaking Bad" -d -e 1-5
|
|
97
107
|
```
|
|
@@ -124,6 +134,8 @@ cinnamon search "Breaking Bad" -d -e 1-5
|
|
|
124
134
|
| `--scraper <name>` | Force a specific scraper |
|
|
125
135
|
| `--player mpv` or `--player vlc` | Choose player |
|
|
126
136
|
| `-q 720p` | Pick quality (480p, 720p, 1080p, best, worst) |
|
|
137
|
+
| `--sub` | Prefer subtitled audio (skips the translation prompt) |
|
|
138
|
+
| `--dub` | Prefer dubbed audio (skips the translation prompt) |
|
|
127
139
|
| `--info-only` | Just print the stream URL |
|
|
128
140
|
|
|
129
141
|
---
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# cinnamon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
|
|
4
4
|
|
|
5
|
-
[](https://pypi.org/project/cinnamon-cli/)
|
|
6
6
|
[](#)
|
|
7
7
|
[](#)
|
|
8
8
|
[](#)
|
|
@@ -18,7 +18,9 @@ pip install cinnamon-cli
|
|
|
18
18
|
cinnamon setup
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The setup wizard asks for a TMDB API key ([free account](https://www.themoviedb.org/signup)),
|
|
21
|
+
> The setup wizard asks for a TMDB API key ([Create a free account here](https://www.themoviedb.org/signup)), the api key is used for show and movie information.
|
|
22
|
+
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
### Get a player
|
|
24
26
|
> Please note that MacOS version has not been tested since we dont have access to a mac
|
|
@@ -34,35 +36,43 @@ The setup wizard asks for a TMDB API key ([free account](https://www.themoviedb.
|
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
38
|
cinnamon search "Breaking Bad"
|
|
39
|
+
cinnamon search "Inception"
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
For Anime you can run (Works even if you dont have an api key):
|
|
40
43
|
|
|
41
44
|
```bash
|
|
42
|
-
cinnamon
|
|
45
|
+
cinnamon anime "Chainsaw Man"
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
By default you'll be asked whether to watch sub or dub. Skip the prompt with
|
|
49
|
+
`-sub` / `-dub`:
|
|
46
50
|
|
|
47
51
|
```bash
|
|
48
|
-
cinnamon
|
|
49
|
-
cinnamon
|
|
52
|
+
cinnamon anime "Chainsaw Man" --dub
|
|
53
|
+
cinnamon anime "Frieren" --sub
|
|
50
54
|
```
|
|
51
55
|
|
|
52
|
-
### Download episodes
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
you can also run it like this:
|
|
55
58
|
|
|
56
59
|
```bash
|
|
57
|
-
|
|
60
|
+
cinnamon Inception
|
|
61
|
+
cinnamon Breaking bad
|
|
62
|
+
cinnamon chainsaw man
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
### Download episodes
|
|
66
|
+
|
|
67
|
+
Downloading needs [yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
|
68
|
+
|
|
61
69
|
|
|
62
70
|
```bash
|
|
63
71
|
pip install yt-dlp
|
|
64
72
|
```
|
|
65
73
|
|
|
74
|
+
Now you can run:
|
|
75
|
+
|
|
66
76
|
```bash
|
|
67
77
|
cinnamon search "Breaking Bad" -d -e 1-5
|
|
68
78
|
```
|
|
@@ -95,6 +105,8 @@ cinnamon search "Breaking Bad" -d -e 1-5
|
|
|
95
105
|
| `--scraper <name>` | Force a specific scraper |
|
|
96
106
|
| `--player mpv` or `--player vlc` | Choose player |
|
|
97
107
|
| `-q 720p` | Pick quality (480p, 720p, 1080p, best, worst) |
|
|
108
|
+
| `--sub` | Prefer subtitled audio (skips the translation prompt) |
|
|
109
|
+
| `--dub` | Prefer dubbed audio (skips the translation prompt) |
|
|
98
110
|
| `--info-only` | Just print the stream URL |
|
|
99
111
|
|
|
100
112
|
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.27"
|
|
@@ -518,9 +518,15 @@ def _resolve_and_play(show, season_num, ep_num, ep_name, scraper, player, qualit
|
|
|
518
518
|
if not attempt:
|
|
519
519
|
continue
|
|
520
520
|
try:
|
|
521
|
-
with console.status(f"Resolving via [bold]{attempt_name}[/bold]...
|
|
521
|
+
with console.status(f"Resolving via [bold]{attempt_name}[/bold]...", spinner="dots"):
|
|
522
522
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
523
|
-
info = {"show": show_name, "tv_id": show_id
|
|
523
|
+
info = {"show": show_name, "tv_id": show_id}
|
|
524
|
+
if season_num is not None:
|
|
525
|
+
info["season"] = season_num
|
|
526
|
+
if ep_num is not None:
|
|
527
|
+
info["episode"] = ep_num
|
|
528
|
+
else:
|
|
529
|
+
info["media_type"] = "movie"
|
|
524
530
|
if quality:
|
|
525
531
|
info["quality"] = quality
|
|
526
532
|
if translation:
|
|
@@ -625,75 +631,49 @@ def _play_movie(show, scraper, player, quality, info_only, download=False):
|
|
|
625
631
|
return
|
|
626
632
|
|
|
627
633
|
console.print()
|
|
628
|
-
try:
|
|
629
|
-
with console.status(f"Resolving via [bold]{scraper_name}[/bold]... (timeout {RESOLVE_TIMEOUT}s)", spinner="dots"):
|
|
630
|
-
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
631
|
-
info = {"show": show_name, "movie_id": show_id, "media_type": "movie"}
|
|
632
|
-
if quality:
|
|
633
|
-
info["quality"] = quality
|
|
634
|
-
future = pool.submit(scraper_instance.resolve, info)
|
|
635
|
-
result = future.result(timeout=RESOLVE_TIMEOUT)
|
|
636
|
-
except concurrent.futures.TimeoutError:
|
|
637
|
-
_print_error(f"Scraper [bold]{scraper_name}[/bold] timed out after {RESOLVE_TIMEOUT}s.")
|
|
638
|
-
return
|
|
639
|
-
except ScraperError as e:
|
|
640
|
-
_print_error(str(e))
|
|
641
|
-
return
|
|
642
634
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
635
|
+
movie_show = {"name": show_name, "id": show_id, "title": show_name}
|
|
636
|
+
while True:
|
|
637
|
+
proc = _resolve_and_play(
|
|
638
|
+
movie_show, None, None, f"{show_name}", scraper_name,
|
|
639
|
+
player, quality, info_only, download,
|
|
640
|
+
)
|
|
646
641
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
console.print(Panel(
|
|
650
|
-
f"[{theme['success']}]Stream ready![/{theme['success']}] [bold]{result.title}[/bold]",
|
|
651
|
-
border_style=theme["success"],
|
|
652
|
-
))
|
|
642
|
+
if proc is None or info_only or download:
|
|
643
|
+
return
|
|
653
644
|
|
|
654
|
-
|
|
645
|
+
try:
|
|
646
|
+
proc.wait()
|
|
647
|
+
except AttributeError:
|
|
648
|
+
return
|
|
655
649
|
|
|
656
|
-
|
|
657
|
-
console.print(f" [{theme['dim']}]URL:[/{theme['dim']}] {result.m3u8_url}")
|
|
658
|
-
return
|
|
650
|
+
_set_history(show_name, None, None, scraper=scraper_name, quality=quality)
|
|
659
651
|
|
|
660
|
-
|
|
661
|
-
if result.m3u8_url.startswith("magnet:"):
|
|
662
|
-
_print_error("Download not supported for torrent/magnet links.")
|
|
663
|
-
return
|
|
664
|
-
from .downloads import create as _track_create
|
|
665
|
-
from .downloads import remove as _track_remove
|
|
666
|
-
from .downloads import update as _track_update
|
|
667
|
-
track_id = _track_create({
|
|
668
|
-
"title": result.title,
|
|
669
|
-
"url": result.m3u8_url,
|
|
670
|
-
"tv_id": show_id,
|
|
671
|
-
"season": None,
|
|
672
|
-
"episode": None,
|
|
673
|
-
"quality": quality or "",
|
|
674
|
-
"referer": result.referer,
|
|
675
|
-
"scraper": scraper_name,
|
|
676
|
-
})
|
|
652
|
+
console.print()
|
|
677
653
|
try:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
654
|
+
choice = _select(
|
|
655
|
+
"Options",
|
|
656
|
+
choices=[
|
|
657
|
+
questionary.Choice(title="Replay", value="replay"),
|
|
658
|
+
questionary.Choice(title="Change quality", value="quality"),
|
|
659
|
+
questionary.Choice(title="Quit", value="quit"),
|
|
660
|
+
],
|
|
661
|
+
)
|
|
662
|
+
except Exception:
|
|
663
|
+
return
|
|
688
664
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
665
|
+
if not choice or choice == "quit":
|
|
666
|
+
return
|
|
667
|
+
elif choice == "quality":
|
|
668
|
+
try:
|
|
669
|
+
quality = _select(
|
|
670
|
+
"Quality",
|
|
671
|
+
choices=["480p", "720p", "1080p", "best", "worst"],
|
|
672
|
+
default=quality or "best",
|
|
673
|
+
)
|
|
674
|
+
except Exception:
|
|
675
|
+
quality = Prompt.ask("Quality (480p, 720p, 1080p, best, worst)", default=quality or "best")
|
|
676
|
+
# "replay" falls through and loops with the same quality.
|
|
697
677
|
|
|
698
678
|
|
|
699
679
|
# ---------------------------------------------------------------------------
|
|
@@ -987,10 +967,25 @@ def search(query, media_type, season, ep_str, scraper, player, quality, download
|
|
|
987
967
|
def watch(query, tmdb_id, media_type, season, ep_str, scraper, player, download, info_only, quality):
|
|
988
968
|
"""Browse episodes interactively and play one."""
|
|
989
969
|
_check_for_updates()
|
|
990
|
-
tmdb = _get_tmdb()
|
|
991
970
|
|
|
992
971
|
ep_start, ep_end = _parse_episode(ep_str) if ep_str else (None, None)
|
|
993
972
|
|
|
973
|
+
# Explicit id given: skip TMDB entirely (no API key needed) and let the
|
|
974
|
+
# scraper resolve the stream straight from the id.
|
|
975
|
+
if tmdb_id and media_type == "movie":
|
|
976
|
+
show = {"title": f"Movie {tmdb_id}", "id": tmdb_id}
|
|
977
|
+
_play_movie(show, scraper, player, quality, info_only, download)
|
|
978
|
+
return
|
|
979
|
+
if tmdb_id and media_type == "tv":
|
|
980
|
+
show = {"name": f"Show {tmdb_id}", "id": tmdb_id}
|
|
981
|
+
if season is not None and ep_start is not None:
|
|
982
|
+
_play_with_menu(show, season, ep_start, ep_end, f"E{ep_start}", scraper, player, quality, info_only, download)
|
|
983
|
+
else:
|
|
984
|
+
_interactive_episode_picker(None, show, scraper, player, quality, info_only, ep_start=ep_start, ep_end=ep_end)
|
|
985
|
+
return
|
|
986
|
+
|
|
987
|
+
tmdb = _get_tmdb()
|
|
988
|
+
|
|
994
989
|
# Explicit movie request.
|
|
995
990
|
if media_type == "movie":
|
|
996
991
|
if not tmdb_id and query:
|
|
@@ -999,8 +994,6 @@ def watch(query, tmdb_id, media_type, season, ep_str, scraper, player, download,
|
|
|
999
994
|
if not show:
|
|
1000
995
|
return
|
|
1001
996
|
tmdb_id = show["id"]
|
|
1002
|
-
elif tmdb_id:
|
|
1003
|
-
show = tmdb.get_movie_details(tmdb_id)
|
|
1004
997
|
else:
|
|
1005
998
|
query = _prompt("Search for a movie")
|
|
1006
999
|
results = tmdb.search_movie(query).get("results", [])
|
|
@@ -1013,7 +1006,9 @@ def watch(query, tmdb_id, media_type, season, ep_str, scraper, player, download,
|
|
|
1013
1006
|
# Explicit TV request (or a TMDB id with no type hint).
|
|
1014
1007
|
if media_type == "tv" or (tmdb_id and media_type is None):
|
|
1015
1008
|
if tmdb_id:
|
|
1016
|
-
|
|
1009
|
+
# Explicit id given: skip the TMDB metadata lookup (no API key
|
|
1010
|
+
# needed); the scraper resolves the stream from the id.
|
|
1011
|
+
show = {"name": f"Show {tmdb_id}", "id": tmdb_id}
|
|
1017
1012
|
elif query:
|
|
1018
1013
|
results = tmdb.search_tv(query).get("results", [])
|
|
1019
1014
|
show = _pick_with_arrows(results, "name", "first_air_date", "Select a show:")
|
|
@@ -1534,7 +1529,9 @@ def install(name):
|
|
|
1534
1529
|
@click.option("--player", help="Player: vlc, mpv, or auto")
|
|
1535
1530
|
@click.option("-q", "--quality", help="Video quality: 480p, 720p, 1080p, best, worst")
|
|
1536
1531
|
@click.option("--info-only", is_flag=True, help="Show the stream URL without playing")
|
|
1537
|
-
|
|
1532
|
+
@click.option("--sub", "prefer_sub", is_flag=True, help="Prefer subtitled audio")
|
|
1533
|
+
@click.option("--dub", "prefer_dub", is_flag=True, help="Prefer dubbed audio")
|
|
1534
|
+
def anime(query, season, ep_str, download, player, quality, info_only, prefer_sub, prefer_dub):
|
|
1538
1535
|
"""Search anime via AniList (no API key needed) and stream from allanime."""
|
|
1539
1536
|
_check_for_updates()
|
|
1540
1537
|
|
|
@@ -1611,10 +1608,14 @@ def anime(query, season, ep_str, download, player, quality, info_only):
|
|
|
1611
1608
|
_print_error("No episode data from allanime.")
|
|
1612
1609
|
return
|
|
1613
1610
|
|
|
1614
|
-
|
|
1611
|
+
if prefer_sub and prefer_dub:
|
|
1612
|
+
_print_info("Both --sub and --dub given; ignoring and asking.")
|
|
1613
|
+
prefer_sub = prefer_dub = False
|
|
1614
|
+
translation = "sub" if prefer_sub else ("dub" if prefer_dub else None)
|
|
1615
|
+
_run_anime_flow(show_name, episodes_detail, season, ep_str, player, quality, info_only, download, translation=translation)
|
|
1615
1616
|
|
|
1616
1617
|
|
|
1617
|
-
def _run_anime_flow(show_name, episodes_detail, season=None, ep_str=None, player=None, quality=None, info_only=False, download=False):
|
|
1618
|
+
def _run_anime_flow(show_name, episodes_detail, season=None, ep_str=None, player=None, quality=None, info_only=False, download=False, translation=None):
|
|
1618
1619
|
"""Drive the episode picker from allanime's *available* episodes (not TMDB's
|
|
1619
1620
|
full list), so we never offer an episode the anime source doesn't have."""
|
|
1620
1621
|
parsed = {}
|
|
@@ -1648,7 +1649,9 @@ def _run_anime_flow(show_name, episodes_detail, season=None, ep_str=None, player
|
|
|
1648
1649
|
return
|
|
1649
1650
|
|
|
1650
1651
|
tt_keys = list(season_data.keys())
|
|
1651
|
-
if
|
|
1652
|
+
if translation and translation in tt_keys:
|
|
1653
|
+
tt = translation
|
|
1654
|
+
elif "sub" in tt_keys and len(tt_keys) > 1:
|
|
1652
1655
|
try:
|
|
1653
1656
|
tt = _select("Translation:", choices=[
|
|
1654
1657
|
questionary.Choice(title=k, value=k) for k in tt_keys
|
|
@@ -275,10 +275,13 @@ def _launch(player_name, cmd):
|
|
|
275
275
|
|
|
276
276
|
# Give the player a moment to fail (missing display, codec, etc.).
|
|
277
277
|
try:
|
|
278
|
-
proc.wait(timeout=
|
|
278
|
+
proc.wait(timeout=3.0)
|
|
279
279
|
except subprocess.TimeoutExpired:
|
|
280
|
-
# Still alive after
|
|
281
|
-
if proc.
|
|
280
|
+
# Still alive after 3s — it started successfully. Detach stderr.
|
|
281
|
+
if proc.poll() is not None:
|
|
282
|
+
# Exited in the gap between the timeout firing and us checking.
|
|
283
|
+
pass
|
|
284
|
+
elif proc.stderr and not proc.stderr.closed:
|
|
282
285
|
try:
|
|
283
286
|
proc.stderr.close()
|
|
284
287
|
except OSError:
|
|
@@ -298,7 +301,7 @@ def _launch(player_name, cmd):
|
|
|
298
301
|
elif err:
|
|
299
302
|
msg += f": {err[:300]}"
|
|
300
303
|
else:
|
|
301
|
-
msg += "
|
|
304
|
+
msg += " — the stream could not be loaded (source may be down or blocked). Try another scraper, quality, or title."
|
|
302
305
|
raise PlayerLaunchError(player_name, msg)
|
|
303
306
|
|
|
304
307
|
|
|
@@ -321,7 +324,9 @@ def play_mpv(url, title="", referer=None):
|
|
|
321
324
|
raise PlayerNotFoundError("mpv")
|
|
322
325
|
if _in_termux():
|
|
323
326
|
return _termux_open(url, "mpv", referer=referer, user_agent=DEFAULT_UA)
|
|
324
|
-
cmd = [exe, f"--title={title}", "--alang=eng", "--slang=eng", "--subs-with-matching-audio=yes"
|
|
327
|
+
cmd = [exe, f"--title={title}", "--alang=eng", "--slang=eng", "--subs-with-matching-audio=yes",
|
|
328
|
+
"--cache=yes", "--cache-secs=300", "--ytdl=no",
|
|
329
|
+
"--network-timeout=20", "--keep-open=no"]
|
|
325
330
|
if referer:
|
|
326
331
|
cmd += ["--http-header-fields=Referer: " + referer]
|
|
327
332
|
cmd.append(url)
|
|
@@ -11,6 +11,23 @@ from ..player import DEFAULT_UA as UA
|
|
|
11
11
|
TIMEOUT = 15
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
|
|
15
|
+
def _verify_playlist(session, master_url, source):
|
|
16
|
+
"""Fetch the master playlist and confirm it is a real HLS playlist.
|
|
17
|
+
|
|
18
|
+
vixsrc sometimes returns an error page (e.g. HTTP 502) instead of a
|
|
19
|
+
playlist. Returning that to the player yields a silent black screen, so we
|
|
20
|
+
raise ScraperNoStreamError to let the caller fall back to another source."""
|
|
21
|
+
try:
|
|
22
|
+
resp = session.get(master_url, timeout=TIMEOUT)
|
|
23
|
+
except requests.exceptions.RequestException as e:
|
|
24
|
+
raise ScraperNoStreamError(source, f"Master playlist request failed: {e}")
|
|
25
|
+
if resp.status_code != 200 or not resp.text.lstrip().startswith("#EXTM3U"):
|
|
26
|
+
raise ScraperNoStreamError(source, f"Master playlist unavailable (status {resp.status_code}).")
|
|
27
|
+
return resp
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
14
31
|
class WebStreamScraper(BaseScraper):
|
|
15
32
|
name = "webstream"
|
|
16
33
|
description = "HTTP streams from vixsrc.to and vidlink.pro (no browser needed)"
|
|
@@ -102,10 +119,13 @@ def _try_vixsrc(tmdb_id, season, episode, quality="") -> Optional[str]:
|
|
|
102
119
|
session.headers.update({"User-Agent": UA})
|
|
103
120
|
|
|
104
121
|
api_url = f"https://vixsrc.to/api/tv/{tmdb_id}/{season}/{episode}?lang=en"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
try:
|
|
123
|
+
embed_resp = session.get(api_url, timeout=TIMEOUT, headers={
|
|
124
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
125
|
+
"Referer": "https://vixsrc.to",
|
|
126
|
+
})
|
|
127
|
+
except requests.exceptions.RequestException as e:
|
|
128
|
+
raise ScraperNetworkError("vixsrc", f"API request failed: {e}")
|
|
109
129
|
if embed_resp.status_code != 200:
|
|
110
130
|
raise ScraperNetworkError("vixsrc", f"API returned {embed_resp.status_code}")
|
|
111
131
|
embed_src = embed_resp.json().get("src")
|
|
@@ -115,9 +135,12 @@ def _try_vixsrc(tmdb_id, season, episode, quality="") -> Optional[str]:
|
|
|
115
135
|
full_embed = "https://vixsrc.to" + embed_src
|
|
116
136
|
sep = "&" if "?" in full_embed else "?"
|
|
117
137
|
full_embed += f"{sep}lang=en"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
try:
|
|
139
|
+
html = session.get(full_embed, timeout=TIMEOUT, headers={
|
|
140
|
+
"Accept": "text/html,application/xhtml+xml,*/*",
|
|
141
|
+
}).text
|
|
142
|
+
except requests.exceptions.RequestException as e:
|
|
143
|
+
raise ScraperNetworkError("vixsrc", f"Embed page request failed: {e}")
|
|
121
144
|
|
|
122
145
|
token = re.search(r'token["\']\s*:\s*["\']([^"\']+)', html)
|
|
123
146
|
expires = re.search(r'expires["\']\s*:\s*["\']([^"\']+)', html)
|
|
@@ -129,12 +152,11 @@ def _try_vixsrc(tmdb_id, season, episode, quality="") -> Optional[str]:
|
|
|
129
152
|
master_url = f'{playlist.group(1)}{sep}token={token.group(1)}&expires={expires.group(1)}&h=1&lang=en'
|
|
130
153
|
|
|
131
154
|
if not quality:
|
|
155
|
+
_verify_playlist(session, master_url, "vixsrc")
|
|
132
156
|
return master_url
|
|
133
157
|
|
|
134
158
|
# Parse master playlist and filter by quality
|
|
135
|
-
master_resp = session
|
|
136
|
-
if master_resp.status_code != 200:
|
|
137
|
-
return master_url
|
|
159
|
+
master_resp = _verify_playlist(session, master_url, "vixsrc")
|
|
138
160
|
|
|
139
161
|
variants = re.findall(
|
|
140
162
|
r'#EXT-X-STREAM-INF:.*?RESOLUTION=\d+x(\d+).*?\n(https?://\S+)',
|
|
@@ -195,10 +217,13 @@ def _try_vixsrc_movie(tmdb_id, quality="") -> Optional[str]:
|
|
|
195
217
|
session.headers.update({"User-Agent": UA})
|
|
196
218
|
|
|
197
219
|
api_url = f"https://vixsrc.to/api/movie/{tmdb_id}?lang=en"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
try:
|
|
221
|
+
embed_resp = session.get(api_url, timeout=TIMEOUT, headers={
|
|
222
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
223
|
+
"Referer": "https://vixsrc.to",
|
|
224
|
+
})
|
|
225
|
+
except requests.exceptions.RequestException as e:
|
|
226
|
+
raise ScraperNetworkError("vixsrc", f"API request failed: {e}")
|
|
202
227
|
if embed_resp.status_code != 200:
|
|
203
228
|
raise ScraperNetworkError("vixsrc", f"API returned {embed_resp.status_code}")
|
|
204
229
|
embed_src = embed_resp.json().get("src")
|
|
@@ -208,9 +233,12 @@ def _try_vixsrc_movie(tmdb_id, quality="") -> Optional[str]:
|
|
|
208
233
|
full_embed = "https://vixsrc.to" + embed_src
|
|
209
234
|
sep = "&" if "?" in full_embed else "?"
|
|
210
235
|
full_embed += f"{sep}lang=en"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
try:
|
|
237
|
+
html = session.get(full_embed, timeout=TIMEOUT, headers={
|
|
238
|
+
"Accept": "text/html,application/xhtml+xml,*/*",
|
|
239
|
+
}).text
|
|
240
|
+
except requests.exceptions.RequestException as e:
|
|
241
|
+
raise ScraperNetworkError("vixsrc", f"Embed page request failed: {e}")
|
|
214
242
|
|
|
215
243
|
token = re.search(r'token["\']\s*:\s*["\']([^"\']+)', html)
|
|
216
244
|
expires = re.search(r'expires["\']\s*:\s*["\']([^"\']+)', html)
|
|
@@ -222,11 +250,10 @@ def _try_vixsrc_movie(tmdb_id, quality="") -> Optional[str]:
|
|
|
222
250
|
master_url = f'{playlist.group(1)}{sep}token={token.group(1)}&expires={expires.group(1)}&h=1&lang=en'
|
|
223
251
|
|
|
224
252
|
if not quality:
|
|
253
|
+
_verify_playlist(session, master_url, "vixsrc")
|
|
225
254
|
return master_url
|
|
226
255
|
|
|
227
|
-
master_resp = session
|
|
228
|
-
if master_resp.status_code != 200:
|
|
229
|
-
return master_url
|
|
256
|
+
master_resp = _verify_playlist(session, master_url, "vixsrc")
|
|
230
257
|
|
|
231
258
|
variants = re.findall(
|
|
232
259
|
r'#EXT-X-STREAM-INF:.*?RESOLUTION=\d+x(\d+).*?\n(https?://\S+)',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cinnamon-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.27
|
|
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
|
|
@@ -29,9 +29,9 @@ Dynamic: license-file
|
|
|
29
29
|
|
|
30
30
|
# cinnamon
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
|
|
33
33
|
|
|
34
|
-
[](https://pypi.org/project/cinnamon-cli/)
|
|
35
35
|
[](#)
|
|
36
36
|
[](#)
|
|
37
37
|
[](#)
|
|
@@ -47,7 +47,9 @@ pip install cinnamon-cli
|
|
|
47
47
|
cinnamon setup
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The setup wizard asks for a TMDB API key ([free account](https://www.themoviedb.org/signup)),
|
|
50
|
+
> The setup wizard asks for a TMDB API key ([Create a free account here](https://www.themoviedb.org/signup)), the api key is used for show and movie information.
|
|
51
|
+
|
|
52
|
+
|
|
51
53
|
|
|
52
54
|
### Get a player
|
|
53
55
|
> Please note that MacOS version has not been tested since we dont have access to a mac
|
|
@@ -63,35 +65,43 @@ The setup wizard asks for a TMDB API key ([free account](https://www.themoviedb.
|
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
cinnamon search "Breaking Bad"
|
|
68
|
+
cinnamon search "Inception"
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
For Anime you can run (Works even if you dont have an api key):
|
|
69
72
|
|
|
70
73
|
```bash
|
|
71
|
-
cinnamon
|
|
74
|
+
cinnamon anime "Chainsaw Man"
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
By default you'll be asked whether to watch sub or dub. Skip the prompt with
|
|
78
|
+
`-sub` / `-dub`:
|
|
75
79
|
|
|
76
80
|
```bash
|
|
77
|
-
cinnamon
|
|
78
|
-
cinnamon
|
|
81
|
+
cinnamon anime "Chainsaw Man" --dub
|
|
82
|
+
cinnamon anime "Frieren" --sub
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
### Download episodes
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
you can also run it like this:
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
|
-
|
|
89
|
+
cinnamon Inception
|
|
90
|
+
cinnamon Breaking bad
|
|
91
|
+
cinnamon chainsaw man
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
### Download episodes
|
|
95
|
+
|
|
96
|
+
Downloading needs [yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
|
97
|
+
|
|
90
98
|
|
|
91
99
|
```bash
|
|
92
100
|
pip install yt-dlp
|
|
93
101
|
```
|
|
94
102
|
|
|
103
|
+
Now you can run:
|
|
104
|
+
|
|
95
105
|
```bash
|
|
96
106
|
cinnamon search "Breaking Bad" -d -e 1-5
|
|
97
107
|
```
|
|
@@ -124,6 +134,8 @@ cinnamon search "Breaking Bad" -d -e 1-5
|
|
|
124
134
|
| `--scraper <name>` | Force a specific scraper |
|
|
125
135
|
| `--player mpv` or `--player vlc` | Choose player |
|
|
126
136
|
| `-q 720p` | Pick quality (480p, 720p, 1080p, best, worst) |
|
|
137
|
+
| `--sub` | Prefer subtitled audio (skips the translation prompt) |
|
|
138
|
+
| `--dub` | Prefer dubbed audio (skips the translation prompt) |
|
|
127
139
|
| `--info-only` | Just print the stream URL |
|
|
128
140
|
|
|
129
141
|
---
|
|
@@ -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.27"
|
|
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.25"
|
|
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
|