cinnamon-cli 0.2.31__tar.gz → 0.2.33__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.31/cinnamon_cli.egg-info → cinnamon_cli-0.2.33}/PKG-INFO +1 -1
  2. cinnamon_cli-0.2.33/cinnamon/__init__.py +1 -0
  3. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/cli.py +149 -10
  4. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/player.py +4 -0
  5. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/scrapers/webstream.py +9 -21
  6. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33/cinnamon_cli.egg-info}/PKG-INFO +1 -1
  7. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/pyproject.toml +1 -1
  8. cinnamon_cli-0.2.31/cinnamon/__init__.py +0 -1
  9. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/LICENSE +0 -0
  10. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/README.md +0 -0
  11. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/anilist.py +0 -0
  12. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/config.py +0 -0
  13. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/downloads.py +0 -0
  14. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/errors.py +0 -0
  15. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/history.py +0 -0
  16. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/scrapers/__init__.py +0 -0
  17. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/scrapers/anime.py +0 -0
  18. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/scrapers/base.py +0 -0
  19. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/scrapers/torrentio.py +0 -0
  20. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/scrapers/vidsrc.py +0 -0
  21. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/tmdb.py +0 -0
  22. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon/tui.py +0 -0
  23. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon_cli.egg-info/SOURCES.txt +0 -0
  24. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon_cli.egg-info/dependency_links.txt +0 -0
  25. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon_cli.egg-info/entry_points.txt +0 -0
  26. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon_cli.egg-info/requires.txt +0 -0
  27. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/cinnamon_cli.egg-info/top_level.txt +0 -0
  28. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/setup.cfg +0 -0
  29. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/tests/test_directstream.py +0 -0
  30. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/tests/test_directstream2.py +0 -0
  31. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/tests/test_directstream3.py +0 -0
  32. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/tests/test_directstream4.py +0 -0
  33. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/tests/test_directstream5.py +0 -0
  34. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/tests/test_directstream6.py +0 -0
  35. {cinnamon_cli-0.2.31 → cinnamon_cli-0.2.33}/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.31
3
+ Version: 0.2.33
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
@@ -0,0 +1 @@
1
+ __version__ = "0.2.33"
@@ -160,6 +160,141 @@ def _prompt(message, default=None, password=False):
160
160
  return default
161
161
 
162
162
 
163
+ def _pick_episode_keys(items, label_fn, message):
164
+ """Keyboard-driven episode picker.
165
+
166
+ Press a number once to *select* (highlight) that episode; press the same
167
+ number again to *confirm* and play it. Arrow keys / j / k move the
168
+ highlight, Enter confirms the highlighted episode, and q / esc abort.
169
+
170
+ Multi-digit entry works too: typing "1" then "2" highlights episode 12,
171
+ and repeating the final digit (or pressing Enter) confirms it.
172
+ """
173
+ if not items:
174
+ return None
175
+ n = len(items)
176
+
177
+ try:
178
+ from prompt_toolkit import Application
179
+ from prompt_toolkit.layout import Layout, Window
180
+ from prompt_toolkit.layout.controls import FormattedTextControl
181
+ from prompt_toolkit.key_binding import KeyBindings
182
+ except Exception:
183
+ return None
184
+
185
+ t = get_theme()
186
+ accent = _q_hex(t.get("accent", "orange1"))
187
+ dim = _q_hex(t.get("dim", "grey50"))
188
+
189
+ class _State:
190
+ sel = 0
191
+ buffer = ""
192
+ last_key = None
193
+ last_press = 0.0
194
+ result = None
195
+ abort = False
196
+
197
+ state = _State()
198
+ app_ref = [None]
199
+
200
+ def _render():
201
+ lines = [(f"bold fg:{accent}", f"{message}\n")]
202
+ lines.append((f"fg:{dim}", " press a number to select · press it again to play · ↑↓/jk move · enter play · q quit\n\n"))
203
+ start = max(0, min(state.sel - 7, n - 15))
204
+ end = min(n, start + 15)
205
+ for i in range(start, end):
206
+ ep = items[i]
207
+ label = label_fn(ep)
208
+ num = f"{i + 1:>3}"
209
+ if i == state.sel:
210
+ if state.buffer:
211
+ lines.append((f"bold fg:{accent}", f" {_POINTER} {num} {label}\n"))
212
+ else:
213
+ lines.append((f"bold fg:{accent}", f" {_POINTER} {num} {label}\n"))
214
+ else:
215
+ lines.append((f"fg:{dim}", f" {num} {label}\n"))
216
+ return lines
217
+
218
+ control = FormattedTextControl(_render)
219
+
220
+ def _confirm():
221
+ state.result = items[state.sel]
222
+ if app_ref[0] is not None:
223
+ app_ref[0].exit()
224
+
225
+ def _abort():
226
+ state.abort = True
227
+ if app_ref[0] is not None:
228
+ app_ref[0].exit()
229
+
230
+ def _press_digit(d):
231
+ now = _time.time()
232
+ if state.last_key is None or (now - state.last_press) > 1.2:
233
+ state.buffer = str(d)
234
+ else:
235
+ state.buffer += str(d)
236
+ state.last_press = now
237
+ last_key = state.last_key
238
+ state.last_key = d
239
+ try:
240
+ idx = int(state.buffer) - 1
241
+ except ValueError:
242
+ idx = -1
243
+ if 0 <= idx < n:
244
+ state.sel = idx
245
+ # Pressing the same digit twice in a row confirms the highlight.
246
+ if d == last_key:
247
+ _confirm()
248
+ return
249
+ if app_ref[0] is not None:
250
+ app_ref[0].invalidate()
251
+
252
+ kb = KeyBindings()
253
+
254
+ for d in range(10):
255
+ kb.add(str(d))(lambda event, d=d: _press_digit(d))
256
+
257
+ @kb.add("up")
258
+ @kb.add("k")
259
+ def _up(event):
260
+ state.buffer = ""
261
+ state.last_key = None
262
+ state.sel = (state.sel - 1) % n
263
+ event.app.invalidate()
264
+
265
+ @kb.add("down")
266
+ @kb.add("j")
267
+ def _down(event):
268
+ state.buffer = ""
269
+ state.last_key = None
270
+ state.sel = (state.sel + 1) % n
271
+ event.app.invalidate()
272
+
273
+ @kb.add("enter")
274
+ def _enter(event):
275
+ _confirm()
276
+
277
+ @kb.add("q")
278
+ @kb.add("escape")
279
+ def _quit(event):
280
+ _abort()
281
+
282
+ app = Application(
283
+ layout=Layout(Window(control, always_scroll=True)),
284
+ key_bindings=kb,
285
+ mouse_support=False,
286
+ full_screen=False,
287
+ )
288
+ app_ref[0] = app
289
+ try:
290
+ app.run()
291
+ except Exception:
292
+ return None
293
+ if state.abort:
294
+ return None
295
+ return state.result
296
+
297
+
163
298
  _UPDATE_CHECK_CACHE = 86400 # 24 hours
164
299
 
165
300
 
@@ -1153,15 +1288,15 @@ def _interactive_episode_picker(tmdb, show, scraper, player, quality, info_only,
1153
1288
  return
1154
1289
 
1155
1290
  try:
1156
- ep_choices = []
1157
- for ep in episodes:
1291
+ def _ep_label(ep):
1158
1292
  ep_num = ep.get("episode_number", "?")
1159
1293
  ep_title = ep.get("name", f"Episode {ep_num}")
1160
- label = f"E{ep_num:02d} {ep_title}" if isinstance(ep_num, int) else f"{ep_num} {ep_title}"
1161
- ep_choices.append(questionary.Choice(title=label, value=ep))
1162
- ep_chosen = _select(
1163
- f"Season {season_num} - Select an episode:",
1164
- choices=ep_choices,
1294
+ return f"E{ep_num:02d} {ep_title}" if isinstance(ep_num, int) else f"{ep_num} {ep_title}"
1295
+
1296
+ ep_chosen = _pick_episode_keys(
1297
+ episodes,
1298
+ _ep_label,
1299
+ f"Season {season_num} — select an episode",
1165
1300
  )
1166
1301
  if not ep_chosen:
1167
1302
  return
@@ -1680,9 +1815,13 @@ def _run_anime_flow(show_name, episodes_detail, season=None, ep_str=None, player
1680
1815
  ep_end = max_ep
1681
1816
  else:
1682
1817
  try:
1683
- ep_choices = [questionary.Choice(title=f"Episode {e}", value=e) for e in sorted(episodes)]
1684
- ep_chosen = _select("Select an episode:", choices=ep_choices)
1685
- if not ep_chosen:
1818
+ sorted_eps = sorted(episodes)
1819
+ ep_chosen = _pick_episode_keys(
1820
+ sorted_eps,
1821
+ lambda e: f"Episode {e}",
1822
+ "Select an episode",
1823
+ )
1824
+ if ep_chosen is None:
1686
1825
  return
1687
1826
  ep_start = int(ep_chosen)
1688
1827
  except Exception:
@@ -472,6 +472,10 @@ def download_video(url, title="", referer=None, output_dir=".", track_id=None):
472
472
 
473
473
  cmd = [
474
474
  exe, "--no-mtime", "--no-warnings", "-o", outtmpl,
475
+ "--retries", "10",
476
+ "--fragment-retries", "10",
477
+ "--retry-sleep", "2",
478
+ "--socket-timeout", "60",
475
479
  ]
476
480
  if referer:
477
481
  cmd += ["--referer", referer]
@@ -42,9 +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
+ # vixsrc first (sequential HLS that yt-dlp handles reliably); vidlink is the
46
+ # fallback. vidlink's .mp4 CDN (bcdn/stormvv) rate-limits aggressively (HTTP
47
+ # 429), so it is not preferred for downloads.
48
48
 
49
49
  if not tmdb_id:
50
50
  raise ScraperParseError(self.name, "Missing tmdb_id/movie_id/tv_id in episode_info")
@@ -52,16 +52,10 @@ class WebStreamScraper(BaseScraper):
52
52
  deadline = _time.time() + 30
53
53
 
54
54
  if media_type == "movie":
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
-
55
+ vixsrc_ref = f"https://vixsrc.to/embed/movie/{tmdb_id}"
62
56
  if _time.time() < deadline:
63
57
  try:
64
- result = vixsrc_fn(tmdb_id, quality)
58
+ result = _try_vixsrc_movie(tmdb_id, quality)
65
59
  if result:
66
60
  label = quality.upper() if quality else "Auto"
67
61
  return ScraperResult(
@@ -75,7 +69,7 @@ class WebStreamScraper(BaseScraper):
75
69
 
76
70
  if _time.time() < deadline:
77
71
  try:
78
- result = vidlink_fn(tmdb_id, quality)
72
+ result = _try_vidlink_movie(tmdb_id, quality)
79
73
  if result:
80
74
  label = quality.upper() if quality else "Auto"
81
75
  return ScraperResult(
@@ -90,16 +84,10 @@ class WebStreamScraper(BaseScraper):
90
84
  raise ScraperNoStreamError(self.name, f"No HTTP stream found for {show}")
91
85
 
92
86
  # 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
-
87
+ vixsrc_ref = f"https://vixsrc.to/embed/tv/{tmdb_id}/{season}/{episode}"
100
88
  if _time.time() < deadline:
101
89
  try:
102
- result = vixsrc_fn(tmdb_id, season, episode, quality)
90
+ result = _try_vixsrc(tmdb_id, season, episode, quality)
103
91
  if result:
104
92
  label = quality.upper() if quality else "Auto"
105
93
  return ScraperResult(
@@ -113,7 +101,7 @@ class WebStreamScraper(BaseScraper):
113
101
 
114
102
  if _time.time() < deadline:
115
103
  try:
116
- result = vidlink_fn(tmdb_id, season, episode, quality)
104
+ result = _try_vidlink(tmdb_id, season, episode, quality)
117
105
  if result:
118
106
  label = quality.upper() if quality else "Auto"
119
107
  return ScraperResult(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinnamon-cli
3
- Version: 0.2.31
3
+ Version: 0.2.33
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cinnamon-cli"
7
- version = "0.2.31"
7
+ version = "0.2.33"
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.31"
File without changes
File without changes
File without changes