cinnamon-cli 0.2.26__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.
Files changed (35) hide show
  1. {cinnamon_cli-0.2.26/cinnamon_cli.egg-info → cinnamon_cli-0.2.27}/PKG-INFO +15 -9
  2. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/README.md +14 -8
  3. cinnamon_cli-0.2.27/cinnamon/__init__.py +1 -0
  4. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/cli.py +75 -72
  5. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/player.py +9 -5
  6. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/scrapers/webstream.py +47 -20
  7. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27/cinnamon_cli.egg-info}/PKG-INFO +15 -9
  8. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/pyproject.toml +1 -1
  9. cinnamon_cli-0.2.26/cinnamon/__init__.py +0 -1
  10. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/LICENSE +0 -0
  11. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/anilist.py +0 -0
  12. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/config.py +0 -0
  13. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/downloads.py +0 -0
  14. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/errors.py +0 -0
  15. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/history.py +0 -0
  16. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/scrapers/__init__.py +0 -0
  17. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/scrapers/anime.py +0 -0
  18. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/scrapers/base.py +0 -0
  19. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/scrapers/torrentio.py +0 -0
  20. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/scrapers/vidsrc.py +0 -0
  21. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/tmdb.py +0 -0
  22. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon/tui.py +0 -0
  23. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/SOURCES.txt +0 -0
  24. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/dependency_links.txt +0 -0
  25. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/entry_points.txt +0 -0
  26. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/requires.txt +0 -0
  27. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/cinnamon_cli.egg-info/top_level.txt +0 -0
  28. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/setup.cfg +0 -0
  29. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/tests/test_directstream.py +0 -0
  30. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/tests/test_directstream2.py +0 -0
  31. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/tests/test_directstream3.py +0 -0
  32. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/tests/test_directstream4.py +0 -0
  33. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/tests/test_directstream5.py +0 -0
  34. {cinnamon_cli-0.2.26 → cinnamon_cli-0.2.27}/tests/test_directstream6.py +0 -0
  35. {cinnamon_cli-0.2.26 → 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.26
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,7 +29,7 @@ Dynamic: license-file
29
29
 
30
30
  # cinnamon
31
31
 
32
- > Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
32
+ Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
33
33
 
34
34
  [![version](https://img.shields.io/pypi/v/cinnamon-cli)](https://pypi.org/project/cinnamon-cli/)
35
35
  [![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)](#)
@@ -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)), your preferred player, and a color theme.
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,23 +65,25 @@ 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
- For Anime you can run:
71
+ For Anime you can run (Works even if you dont have an api key):
69
72
 
70
73
  ```bash
71
74
  cinnamon anime "Chainsaw Man"
72
75
  ```
73
76
 
74
-
75
- Movies work the same way:
77
+ By default you'll be asked whether to watch sub or dub. Skip the prompt with
78
+ `-sub` / `-dub`:
76
79
 
77
80
  ```bash
78
- cinnamon search "Inception"
79
- cinnamon watch --type movie --query "Inception"
81
+ cinnamon anime "Chainsaw Man" --dub
82
+ cinnamon anime "Frieren" --sub
80
83
  ```
81
84
 
82
- you can also run this:
85
+
86
+ you can also run it like this:
83
87
 
84
88
  ```bash
85
89
  cinnamon Inception
@@ -130,6 +134,8 @@ cinnamon search "Breaking Bad" -d -e 1-5
130
134
  | `--scraper <name>` | Force a specific scraper |
131
135
  | `--player mpv` or `--player vlc` | Choose player |
132
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) |
133
139
  | `--info-only` | Just print the stream URL |
134
140
 
135
141
  ---
@@ -1,6 +1,6 @@
1
1
  # cinnamon
2
2
 
3
- > Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
3
+ Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
4
4
 
5
5
  [![version](https://img.shields.io/pypi/v/cinnamon-cli)](https://pypi.org/project/cinnamon-cli/)
6
6
  [![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)](#)
@@ -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)), your preferred player, and a color theme.
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,23 +36,25 @@ 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
- For Anime you can run:
42
+ For Anime you can run (Works even if you dont have an api key):
40
43
 
41
44
  ```bash
42
45
  cinnamon anime "Chainsaw Man"
43
46
  ```
44
47
 
45
-
46
- Movies work the same way:
48
+ By default you'll be asked whether to watch sub or dub. Skip the prompt with
49
+ `-sub` / `-dub`:
47
50
 
48
51
  ```bash
49
- cinnamon search "Inception"
50
- cinnamon watch --type movie --query "Inception"
52
+ cinnamon anime "Chainsaw Man" --dub
53
+ cinnamon anime "Frieren" --sub
51
54
  ```
52
55
 
53
- you can also run this:
56
+
57
+ you can also run it like this:
54
58
 
55
59
  ```bash
56
60
  cinnamon Inception
@@ -101,6 +105,8 @@ cinnamon search "Breaking Bad" -d -e 1-5
101
105
  | `--scraper <name>` | Force a specific scraper |
102
106
  | `--player mpv` or `--player vlc` | Choose player |
103
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) |
104
110
  | `--info-only` | Just print the stream URL |
105
111
 
106
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]... (timeout {RESOLVE_TIMEOUT}s)", spinner="dots"):
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, "season": season_num, "episode": ep_num}
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
- if not result:
644
- _print_error(f"Scraper [bold]{scraper_name}[/bold] returned nothing.")
645
- return
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
- theme = get_theme()
648
- console.clear()
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
- _set_history(show_name, None, None, scraper=scraper_name, quality=quality)
645
+ try:
646
+ proc.wait()
647
+ except AttributeError:
648
+ return
655
649
 
656
- if info_only:
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
- if download:
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
- download_video(result.m3u8_url, title=result.title, referer=result.referer, track_id=track_id)
679
- except PlayerNotFoundError:
680
- _print_error(f"yt-dlp not found. Install it with: {ytdlp_install_hint()}")
681
- _track_remove(track_id)
682
- except PlayerLaunchError as e:
683
- _print_error(str(e))
684
- _track_update(track_id, status="error")
685
- except KeyboardInterrupt:
686
- raise
687
- return
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
- player_choice = player or config.get("default_player", "auto")
690
- try:
691
- console.print(f" [{theme['info']}]Opening in {player_choice.upper()}...[/{theme['info']}]")
692
- play(result.m3u8_url, title=result.title, player=player_choice, referer=result.referer)
693
- except PlayerNotFoundError as e:
694
- _print_error(str(e))
695
- except Exception as e:
696
- _print_error("Failed to launch player.", str(e))
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
- show = tmdb.get_tv_details(tmdb_id)
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
- def anime(query, season, ep_str, download, player, quality, info_only):
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
- _run_anime_flow(show_name, episodes_detail, season, ep_str, player, quality, info_only, download)
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 "sub" in tt_keys and len(tt_keys) > 1:
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=2.0)
278
+ proc.wait(timeout=3.0)
279
279
  except subprocess.TimeoutExpired:
280
- # Still alive after 2s — it started successfully. Detach stderr.
281
- if proc.stderr and not proc.stderr.closed:
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 += " (no error output likely no display or missing dependencies)."
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
 
@@ -322,7 +325,8 @@ def play_mpv(url, title="", referer=None):
322
325
  if _in_termux():
323
326
  return _termux_open(url, "mpv", referer=referer, user_agent=DEFAULT_UA)
324
327
  cmd = [exe, f"--title={title}", "--alang=eng", "--slang=eng", "--subs-with-matching-audio=yes",
325
- "--seekable=yes", "--force-seekable=yes", "--cache=yes", "--cache-secs=300"]
328
+ "--cache=yes", "--cache-secs=300", "--ytdl=no",
329
+ "--network-timeout=20", "--keep-open=no"]
326
330
  if referer:
327
331
  cmd += ["--http-header-fields=Referer: " + referer]
328
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
- embed_resp = session.get(api_url, timeout=TIMEOUT, headers={
106
- "Accept": "application/json, text/javascript, */*; q=0.01",
107
- "Referer": "https://vixsrc.to",
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
- html = session.get(full_embed, timeout=TIMEOUT, headers={
119
- "Accept": "text/html,application/xhtml+xml,*/*",
120
- }).text
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.get(master_url, timeout=TIMEOUT)
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
- embed_resp = session.get(api_url, timeout=TIMEOUT, headers={
199
- "Accept": "application/json, text/javascript, */*; q=0.01",
200
- "Referer": "https://vixsrc.to",
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
- html = session.get(full_embed, timeout=TIMEOUT, headers={
212
- "Accept": "text/html,application/xhtml+xml,*/*",
213
- }).text
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.get(master_url, timeout=TIMEOUT)
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.26
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,7 +29,7 @@ Dynamic: license-file
29
29
 
30
30
  # cinnamon
31
31
 
32
- > Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
32
+ Watch TV shows, movies, and anime from your terminal. Search, pick a title, and it plays in mpv or VLC.
33
33
 
34
34
  [![version](https://img.shields.io/pypi/v/cinnamon-cli)](https://pypi.org/project/cinnamon-cli/)
35
35
  [![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)](#)
@@ -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)), your preferred player, and a color theme.
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,23 +65,25 @@ 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
- For Anime you can run:
71
+ For Anime you can run (Works even if you dont have an api key):
69
72
 
70
73
  ```bash
71
74
  cinnamon anime "Chainsaw Man"
72
75
  ```
73
76
 
74
-
75
- Movies work the same way:
77
+ By default you'll be asked whether to watch sub or dub. Skip the prompt with
78
+ `-sub` / `-dub`:
76
79
 
77
80
  ```bash
78
- cinnamon search "Inception"
79
- cinnamon watch --type movie --query "Inception"
81
+ cinnamon anime "Chainsaw Man" --dub
82
+ cinnamon anime "Frieren" --sub
80
83
  ```
81
84
 
82
- you can also run this:
85
+
86
+ you can also run it like this:
83
87
 
84
88
  ```bash
85
89
  cinnamon Inception
@@ -130,6 +134,8 @@ cinnamon search "Breaking Bad" -d -e 1-5
130
134
  | `--scraper <name>` | Force a specific scraper |
131
135
  | `--player mpv` or `--player vlc` | Choose player |
132
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) |
133
139
  | `--info-only` | Just print the stream URL |
134
140
 
135
141
  ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cinnamon-cli"
7
- version = "0.2.26"
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.26"
File without changes
File without changes