ani-cli-ar 1.9.0__tar.gz → 1.9.2__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 (40) hide show
  1. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/PKG-INFO +2 -1
  2. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/PKG-INFO +2 -1
  3. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/SOURCES.txt +1 -0
  4. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/requires.txt +1 -0
  5. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/app.py +184 -5
  6. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/config.py +3 -0
  7. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/player.py +130 -34
  8. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/version.py +1 -1
  9. ani_cli_ar-1.9.2/ani_cli_arabic/watch_together.py +823 -0
  10. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/pyproject.toml +1 -0
  11. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/LICENSE +0 -0
  12. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/README.md +0 -0
  13. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/dependency_links.txt +0 -0
  14. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/entry_points.txt +0 -0
  15. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/top_level.txt +0 -0
  16. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/__init__.py +0 -0
  17. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/__main__.py +0 -0
  18. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/api.py +0 -0
  19. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/cli.py +0 -0
  20. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/deps.py +0 -0
  21. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/discord_rpc.py +0 -0
  22. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/favorites.py +0 -0
  23. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/history.py +0 -0
  24. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/models.py +0 -0
  25. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/monitoring.py +0 -0
  26. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/__init__.py +0 -0
  27. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/anikoto.py +0 -0
  28. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/animepahe.py +0 -0
  29. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/api_provider.py +0 -0
  30. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/base.py +0 -0
  31. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/gogoanime.py +0 -0
  32. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/miruro.py +0 -0
  33. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/mkissa.py +0 -0
  34. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/provider_manager.py +0 -0
  35. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/settings.py +0 -0
  36. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/storage.py +0 -0
  37. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/ui.py +0 -0
  38. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/updater.py +0 -0
  39. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/utils.py +0 -0
  40. {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ani-cli-ar
3
- Version: 1.9.0
3
+ Version: 1.9.2
4
4
  Summary: Terminal-based anime streaming with Arabic subtitles
5
5
  Author: NoBy
6
6
  Maintainer: NoBy
@@ -33,6 +33,7 @@ Requires-Dist: Pillow>=10.0.0
33
33
  Requires-Dist: numpy>=1.24.0
34
34
  Requires-Dist: pycryptodomex
35
35
  Requires-Dist: playwright>=1.40.0
36
+ Requires-Dist: supabase>=2.0.0
36
37
  Dynamic: license-file
37
38
 
38
39
  <h1 align="center"> NEW MAJOR UPDATE COMING IN LATE SUMMER </h1>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ani-cli-ar
3
- Version: 1.9.0
3
+ Version: 1.9.2
4
4
  Summary: Terminal-based anime streaming with Arabic subtitles
5
5
  Author: NoBy
6
6
  Maintainer: NoBy
@@ -33,6 +33,7 @@ Requires-Dist: Pillow>=10.0.0
33
33
  Requires-Dist: numpy>=1.24.0
34
34
  Requires-Dist: pycryptodomex
35
35
  Requires-Dist: playwright>=1.40.0
36
+ Requires-Dist: supabase>=2.0.0
36
37
  Dynamic: license-file
37
38
 
38
39
  <h1 align="center"> NEW MAJOR UPDATE COMING IN LATE SUMMER </h1>
@@ -26,6 +26,7 @@ ani_cli_arabic/ui.py
26
26
  ani_cli_arabic/updater.py
27
27
  ani_cli_arabic/utils.py
28
28
  ani_cli_arabic/version.py
29
+ ani_cli_arabic/watch_together.py
29
30
  ani_cli_arabic/scrapers/__init__.py
30
31
  ani_cli_arabic/scrapers/anikoto.py
31
32
  ani_cli_arabic/scrapers/animepahe.py
@@ -7,3 +7,4 @@ Pillow>=10.0.0
7
7
  numpy>=1.24.0
8
8
  pycryptodomex
9
9
  playwright>=1.40.0
10
+ supabase>=2.0.0
@@ -22,6 +22,7 @@ from .favorites import FavoritesManager
22
22
  from .updater import check_for_updates, get_version_status
23
23
  from .deps import ensure_dependencies
24
24
  from .config import GOODBYE_ART
25
+ from . import watch_together
25
26
  import shutil
26
27
  import argparse
27
28
 
@@ -39,6 +40,8 @@ class AniCliArApp:
39
40
  self.force_cli = False
40
41
  self._cleaned_up = False
41
42
  self._language_override = None
43
+ self.watch_host = None
44
+ self.watch_guest = None
42
45
 
43
46
  def run(self):
44
47
  parser = argparse.ArgumentParser(
@@ -224,10 +227,15 @@ class AniCliArApp:
224
227
  else:
225
228
  self.ui.print(Align.center(Text.from_markup("Discord Rich Presence [dim](disabled)[/dim]", style="dim")))
226
229
 
230
+ if self.watch_host is not None:
231
+ self.ui.print(Align.center(Text.from_markup(f"Watch Together: [bold green]Hosting Room {self.watch_host.code}[/bold green]", style="secondary")))
232
+ elif self.watch_guest is not None:
233
+ self.ui.print(Align.center(Text.from_markup(f"Watch Together: [bold yellow]Joined Room {self.watch_guest.code} (syncing)[/bold yellow]", style="secondary")))
234
+
227
235
  self.ui.print()
228
236
 
229
237
  keybinds_panel = Panel(
230
- Text("T: Trending | P: Popular | G: Genres | S: Studios | L: History | F: Favorites | C: Settings | Q: Quit", style="info", justify="center"),
238
+ Text("T: Trending | P: Popular | G: Genres | S: Studios | L: History | F: Favorites | W: Watch Together | C: Settings | Q: Quit", style="info", justify="center"),
231
239
  box=HEAVY,
232
240
  border_style=COLOR_BORDER
233
241
  )
@@ -298,6 +306,9 @@ class AniCliArApp:
298
306
  self.rpc.update_settings()
299
307
  self.ui.settings_menu(self.settings)
300
308
  continue
309
+ elif query == 'w':
310
+ self.handle_watch_together()
311
+ continue
301
312
  elif query == 'a':
302
313
  self.ui.show_credits()
303
314
  continue
@@ -317,6 +328,137 @@ class AniCliArApp:
317
328
 
318
329
  self.handle_anime_selection(results)
319
330
 
331
+ def handle_watch_together(self):
332
+ if self.watch_host is not None or self.watch_guest is not None:
333
+ choice = self.ui.selection_menu(
334
+ ["Stop Watch Together", "Keep Watching"],
335
+ title="Watch Together",
336
+ )
337
+ if choice == "Stop Watch Together":
338
+ self._stop_watch_session()
339
+ return
340
+
341
+ choice = self.ui.selection_menu(
342
+ ["Host a Room", "Join a Room"],
343
+ title="Watch Together",
344
+ )
345
+ if choice is None:
346
+ return
347
+ if choice == "Host a Room":
348
+ self._start_watch_host()
349
+ else:
350
+ self._join_watch_room()
351
+
352
+ def _stop_watch_session(self):
353
+ if self.watch_host is not None:
354
+ try:
355
+ self.watch_host.stop()
356
+ except Exception:
357
+ pass
358
+ self.watch_host = None
359
+ if self.watch_guest is not None:
360
+ try:
361
+ self.watch_guest.stop()
362
+ except Exception:
363
+ pass
364
+ self.watch_guest = None
365
+ self.ui.render_message(
366
+ "Watch Together",
367
+ "Session stopped.",
368
+ "info",
369
+ )
370
+
371
+ def _select_watch_player(self):
372
+ players = self.player.get_available_players()
373
+ names = [n.lower() for n in players]
374
+ available = [p for p in ("mpv", "vlc") if p in names]
375
+ if not available:
376
+ self.ui.render_message(
377
+ "Watch Together",
378
+ "No supported player (mpv/VLC) found.",
379
+ "error",
380
+ )
381
+ return None
382
+ preferred = (self.settings.get('player') or 'ask').strip().lower()
383
+ if preferred in available:
384
+ return preferred
385
+ if len(available) == 1:
386
+ return available[0]
387
+ choice = self.ui.selection_menu(
388
+ [p.upper() for p in available],
389
+ title="Select Player",
390
+ )
391
+ if choice is None:
392
+ return None
393
+ return choice.lower()
394
+
395
+ def _start_watch_host(self):
396
+ player_kind = self._select_watch_player()
397
+ if player_kind is None:
398
+ return
399
+ host = watch_together.WatchHost(player_kind=player_kind)
400
+ if not host.start():
401
+ self.ui.render_message(
402
+ "Watch Together",
403
+ "Could not connect to Supabase Realtime.\n\nCheck your internet connection and make sure\nRealtime is enabled on the Supabase project.",
404
+ "error",
405
+ )
406
+ return
407
+ self.watch_host = host
408
+ self.ui.clear()
409
+ panel = Panel(
410
+ Text(
411
+ f"Room code: {host.code}\n\nShare this 6-digit code with your friends.\n"
412
+ "They join with: Watch Together -> Join a Room",
413
+ style="bold",
414
+ justify="center",
415
+ ),
416
+ title=Text("HOSTING ROOM", style="title"),
417
+ box=HEAVY,
418
+ border_style=COLOR_BORDER,
419
+ )
420
+ self.ui.print(Align.center(panel, vertical="middle", height=self.ui.console.height))
421
+ input("\nPress ENTER to return to the menu...")
422
+
423
+ def _join_watch_room(self):
424
+ code = Prompt.ask(
425
+ "Enter 6-digit room code",
426
+ console=self.ui.console,
427
+ ).strip()
428
+ if len(code) != 6 or not code.isdigit():
429
+ self.ui.render_message(
430
+ "Watch Together",
431
+ "Invalid code. Room codes are 6 digits.",
432
+ "error",
433
+ )
434
+ return
435
+ player_kind = self._select_watch_player()
436
+ if player_kind is None:
437
+ return
438
+ guest = watch_together.WatchGuest(code, player_kind=player_kind)
439
+ if not guest.start():
440
+ self.ui.render_message(
441
+ "Watch Together",
442
+ "Could not connect to the room.\n\nMake sure the code is correct and the host\nis online. Also check your internet connection.",
443
+ "error",
444
+ )
445
+ return
446
+ self.watch_guest = guest
447
+ self.ui.clear()
448
+ panel = Panel(
449
+ Text(
450
+ f"Joined room {code}.\n\nWaiting for the host to start an episode.\n"
451
+ "Your player will launch automatically and stay in sync.",
452
+ style="bold",
453
+ justify="center",
454
+ ),
455
+ title=Text("WATCHING TOGETHER", style="title"),
456
+ box=HEAVY,
457
+ border_style=COLOR_BORDER,
458
+ )
459
+ self.ui.print(Align.center(panel, vertical="middle", height=self.ui.console.height))
460
+ input("\nPress ENTER to return to the menu...")
461
+
320
462
  def handle_anime_selection_with_lazy_load(self, results, load_more_callback):
321
463
  while True:
322
464
  anime_idx = self.ui.anime_selection_menu(results, load_more_callback=load_more_callback)
@@ -938,7 +1080,7 @@ class AniCliArApp:
938
1080
  return "download"
939
1081
  return None
940
1082
  else:
941
- player_type = self.settings.get('player')
1083
+ player_type = self.watch_host.player_kind if self.watch_host else self.settings.get('player')
942
1084
 
943
1085
  from rich.text import Text
944
1086
  from rich.panel import Panel
@@ -970,7 +1112,18 @@ class AniCliArApp:
970
1112
 
971
1113
  monitor.track_video_play(selected_anime.title_en, str(selected_ep.display_num))
972
1114
 
973
- self.player.play(direct_url, f"{selected_anime.title_en} - Ep {selected_ep.display_num} ({quality.name})", player_type=player_type)
1115
+ ipc_socket = None
1116
+ rc_port = None
1117
+ if self.watch_host is not None:
1118
+ if self.watch_host.player_kind == "vlc":
1119
+ rc_port = self.watch_host.rc_port
1120
+ else:
1121
+ ipc_socket = self.watch_host.socket_path
1122
+ self.watch_host.notify_load(selected_anime.title_en, selected_ep.display_num, "Arabic Sub")
1123
+
1124
+ self.player.play(direct_url, f"{selected_anime.title_en} - Ep {selected_ep.display_num} ({quality.name})", player_type=player_type, ipc_socket=ipc_socket, rc_port=rc_port)
1125
+ if self.watch_host is not None:
1126
+ self.watch_host.notify_stop()
974
1127
  self.ui.clear()
975
1128
  self.history.mark_watched(selected_anime.id, selected_ep.display_num, selected_anime.title_en)
976
1129
  self.rpc.update_selecting_episode(selected_anime.title_en, selected_anime.thumbnail)
@@ -1040,7 +1193,7 @@ class AniCliArApp:
1040
1193
  return None
1041
1194
 
1042
1195
  direct_url, stream_headers = url_and_headers
1043
- player_type = self.settings.get('player')
1196
+ player_type = self.watch_host.player_kind if self.watch_host else self.settings.get('player')
1044
1197
  suffix = " [English Dub]" if dub else " [English Sub]"
1045
1198
 
1046
1199
  watching_text = Text()
@@ -1067,7 +1220,22 @@ class AniCliArApp:
1067
1220
  self.rpc.update_watching(selected_anime.title_en, str(selected_ep.display_num), selected_anime.thumbnail)
1068
1221
  monitor.track_video_play(selected_anime.title_en, str(selected_ep.display_num))
1069
1222
 
1070
- self.player.play(direct_url, f"{selected_anime.title_en} - Ep {selected_ep.display_num}", player_type=player_type, headers=stream_headers)
1223
+ ipc_socket = None
1224
+ rc_port = None
1225
+ if self.watch_host is not None:
1226
+ if self.watch_host.player_kind == "vlc":
1227
+ rc_port = self.watch_host.rc_port
1228
+ else:
1229
+ ipc_socket = self.watch_host.socket_path
1230
+ self.watch_host.notify_load(
1231
+ selected_anime.title_en,
1232
+ selected_ep.display_num,
1233
+ "English Dub" if dub else "English Sub",
1234
+ )
1235
+
1236
+ self.player.play(direct_url, f"{selected_anime.title_en} - Ep {selected_ep.display_num}", player_type=player_type, headers=stream_headers, ipc_socket=ipc_socket, rc_port=rc_port)
1237
+ if self.watch_host is not None:
1238
+ self.watch_host.notify_stop()
1071
1239
  self.ui.clear()
1072
1240
  self.history.mark_watched(selected_anime.id, selected_ep.display_num, selected_anime.title_en)
1073
1241
  self.rpc.update_selecting_episode(selected_anime.title_en, selected_anime.thumbnail)
@@ -1116,6 +1284,17 @@ class AniCliArApp:
1116
1284
  except Exception:
1117
1285
  pass
1118
1286
 
1287
+ if self.watch_host is not None:
1288
+ try:
1289
+ self.watch_host.stop()
1290
+ except Exception:
1291
+ pass
1292
+ if self.watch_guest is not None:
1293
+ try:
1294
+ self.watch_guest.stop()
1295
+ except Exception:
1296
+ pass
1297
+
1119
1298
  # Only show TUI goodbye if we are NOT in CLI mode
1120
1299
  if self.current_mode != "cli":
1121
1300
  self.ui.clear()
@@ -6,6 +6,9 @@ DISCORD_LOGO_URL = "https://i.postimg.cc/DydJfKY3/logo.gif"
6
6
  DISCORD_LOGO_TEXT = f"ani-cli-arabic {APP_VERSION}"
7
7
  MYANIMELIST_API_BASE = "https://api.jikan.moe/v4/anime/"
8
8
 
9
+ SUPABASE_DEFAULT_URL = "https://mxacanxpjkqeudlwzhwr.supabase.co"
10
+ SUPABASE_DEFAULT_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im14YWNhbnhwamtxZXVkbHd6aHdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODU0ODUzMTYsImV4cCI6MjEwMTA2MTMxNn0.16feJcv05kBlNLNbbdYZS1riN_MMhIgaZzaqOsw_ilI"
11
+
9
12
  DEFAULT_HEADER_ART = f"""
10
13
  ▄████████ ███▄▄▄▄ ▄█ ▄████████ ▄█ ▄█ ▄████████ ▄████████
11
14
  ███ ███ ███▀▀▀██▄ ███ ███ ███ ███ ███ ███ ███ ███ ███
@@ -7,11 +7,127 @@ import tempfile
7
7
  from typing import Optional
8
8
  from .utils import is_bundled
9
9
 
10
+ _GUEST_VOLUME_BINDINGS = (
11
+ "VOLUME_UP add volume 5",
12
+ "VOLUME_DOWN add volume -5",
13
+ "MUTE cycle mute",
14
+ "9 add volume 5",
15
+ "0 add volume -5",
16
+ "MOUSE_BTN_WHEEL_UP add volume 5",
17
+ "MOUSE_BTN_WHEEL_DOWN add volume -5",
18
+ )
19
+
10
20
  class PlayerManager:
11
21
  def __init__(self, rpc_manager=None, console=None):
12
22
  self.temp_mpv_path = None
13
23
  self.rpc_manager = rpc_manager
14
24
  self.console = console
25
+ self.guest_input_conf_path = None
26
+
27
+ def build_mpv_args(
28
+ self,
29
+ mpv_path: str,
30
+ url: str,
31
+ title: str = "",
32
+ headers: Optional[dict] = None,
33
+ ipc_socket: Optional[str] = None,
34
+ lock_controls: bool = False,
35
+ ) -> list:
36
+ """Build mpv arguments. With lock_controls, all default keybindings are
37
+ disabled so guests cannot pause/seek manually; volume-only keys are bound
38
+ via a generated input.conf."""
39
+ mpv_args = [
40
+ mpv_path,
41
+ '--fullscreen',
42
+ '--keep-open=yes',
43
+ '--cache=yes',
44
+ '--demuxer-max-bytes=150M',
45
+ '--demuxer-max-back-bytes=64M',
46
+ '--demuxer-readahead-secs=20',
47
+ '--hwdec=auto-safe',
48
+ '--sub-auto=fuzzy',
49
+ '--force-window=yes',
50
+ ]
51
+ if title:
52
+ mpv_args.append('--force-media-title=' + title)
53
+ if ipc_socket:
54
+ mpv_args.append('--input-ipc-server=' + ipc_socket)
55
+ if lock_controls:
56
+ mpv_args.append('--no-input-default-bindings')
57
+ conf = self._create_guest_input_conf()
58
+ if conf:
59
+ mpv_args.append('--input-conf=' + conf)
60
+ if headers:
61
+ ref = headers.get('Referer')
62
+ if ref:
63
+ mpv_args += ['--http-header-fields=Referer: ' + ref]
64
+ ua = headers.get('User-Agent')
65
+ if ua:
66
+ mpv_args += ['--user-agent=' + ua]
67
+ mpv_args.append(url)
68
+ return mpv_args
69
+
70
+ def build_vlc_args(
71
+ self,
72
+ vlc_path: str,
73
+ url: str,
74
+ title: str = "",
75
+ headers: Optional[dict] = None,
76
+ rc_port: Optional[int] = None,
77
+ lock_controls: bool = False,
78
+ ) -> list:
79
+ """Build VLC arguments. rc_port enables the rc interface over TCP
80
+ (used for Watch Together sync). With lock_controls, playback hotkeys
81
+ are unbound so guests cannot pause/seek manually."""
82
+ vlc_args = [vlc_path, '--fullscreen', '--no-video-title-show']
83
+ if title:
84
+ vlc_args.append('--meta-title=' + title)
85
+ if rc_port:
86
+ vlc_args += [
87
+ '--extraintf=rc',
88
+ '--rc-host=127.0.0.1:' + str(rc_port),
89
+ ]
90
+ else:
91
+ vlc_args.append('--play-and-exit')
92
+ if lock_controls:
93
+ vlc_args += [
94
+ '--key-play=',
95
+ '--key-jump+short=',
96
+ '--key-jump+medium=',
97
+ '--key-jump+long=',
98
+ '--key-jump+extrashort=',
99
+ '--key-next=',
100
+ '--key-prev=',
101
+ '--key-stop=',
102
+ '--key-quit=',
103
+ ]
104
+ if headers:
105
+ ref = headers.get('Referer')
106
+ if ref:
107
+ vlc_args.append('--http-referrer=' + ref)
108
+ ua = headers.get('User-Agent')
109
+ if ua:
110
+ vlc_args.append('--http-user-agent=' + ua)
111
+ vlc_args.append(url)
112
+ return vlc_args
113
+
114
+ def _create_guest_input_conf(self) -> Optional[str]:
115
+ try:
116
+ fd, path = tempfile.mkstemp(prefix='ani_cli_guest_input_', suffix='.conf')
117
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
118
+ f.write("\n".join(_GUEST_VOLUME_BINDINGS) + "\n")
119
+ self.guest_input_conf_path = path
120
+ return path
121
+ except (OSError, IOError):
122
+ return None
123
+
124
+ def cleanup_guest_input_conf(self):
125
+ if self.guest_input_conf_path:
126
+ try:
127
+ os.unlink(self.guest_input_conf_path)
128
+ except OSError:
129
+ pass
130
+ self.guest_input_conf_path = None
15
131
 
16
132
  def get_mpv_path(self) -> Optional[str]:
17
133
  if is_bundled():
@@ -110,7 +226,7 @@ class PlayerManager:
110
226
 
111
227
  return players
112
228
 
113
- def play(self, url: str, title: str, player_type: str = 'ask', headers: Optional[dict] = None):
229
+ def play(self, url: str, title: str, player_type: str = 'ask', headers: Optional[dict] = None, ipc_socket: Optional[str] = None, rc_port: Optional[int] = None):
114
230
  if not url:
115
231
  msg = "Error: Extracted stream URL is invalid or empty."
116
232
  if self.console:
@@ -195,9 +311,9 @@ class PlayerManager:
195
311
 
196
312
  try:
197
313
  if selected_player == 'VLC':
198
- self._play_vlc(url, title, available_players['VLC'], headers)
314
+ self._play_vlc(url, title, available_players['VLC'], headers, rc_port=rc_port)
199
315
  elif selected_player == 'MPV':
200
- self._play_mpv(url, title, available_players['MPV'], headers)
316
+ self._play_mpv(url, title, available_players['MPV'], headers, ipc_socket=ipc_socket)
201
317
  elif selected_player == 'MPC-HC':
202
318
  self._play_mpc(url, title, available_players['MPC-HC'], headers)
203
319
  except Exception as e:
@@ -209,7 +325,7 @@ class PlayerManager:
209
325
  print(f"Error launching player: {str(e)}", file=sys.stderr)
210
326
  input("Press Enter to continue...")
211
327
 
212
- def _play_vlc(self, url: str, title: str, vlc_path: str = None, headers: dict = None):
328
+ def _play_vlc(self, url: str, title: str, vlc_path: str = None, headers: dict = None, rc_port: Optional[int] = None):
213
329
  if not vlc_path:
214
330
  vlc_path = self.get_available_players().get('VLC')
215
331
 
@@ -221,15 +337,13 @@ class PlayerManager:
221
337
 
222
338
  url = url.strip().strip('"').strip("'")
223
339
 
224
- vlc_args = [
340
+ vlc_args = self.build_vlc_args(
225
341
  vlc_path,
226
- '--fullscreen',
227
- '--play-and-exit',
228
- '--meta-title', title,
229
- ]
230
- if headers and headers.get('Referer'):
231
- vlc_args += ['--http-referrer=' + headers['Referer']]
232
- vlc_args.append(url)
342
+ url,
343
+ title=title,
344
+ headers=headers,
345
+ rc_port=rc_port,
346
+ )
233
347
 
234
348
  if self.console:
235
349
  from rich.text import Text
@@ -257,7 +371,7 @@ class PlayerManager:
257
371
  print(detail, file=sys.stderr)
258
372
  input("Press Enter to continue...")
259
373
 
260
- def _play_mpv(self, url: str, title: str, mpv_path: str = None, headers: dict = None):
374
+ def _play_mpv(self, url: str, title: str, mpv_path: str = None, headers: dict = None, ipc_socket: Optional[str] = None):
261
375
  if not mpv_path:
262
376
  mpv_path = self.get_available_players().get('MPV')
263
377
 
@@ -269,27 +383,9 @@ class PlayerManager:
269
383
 
270
384
  url = url.strip().strip('"').strip("'")
271
385
 
272
- mpv_args = [
273
- mpv_path,
274
- '--fullscreen',
275
- '--keep-open=yes',
276
- '--cache=yes',
277
- '--demuxer-max-bytes=150M',
278
- '--demuxer-max-back-bytes=64M',
279
- '--demuxer-readahead-secs=20',
280
- '--hwdec=auto-safe',
281
- '--sub-auto=fuzzy',
282
- '--force-media-title=' + title,
283
- '--force-window=yes',
284
- ]
285
- if headers:
286
- ref = headers.get('Referer')
287
- if ref:
288
- mpv_args += ['--http-header-fields=Referer: ' + ref]
289
- ua = headers.get('User-Agent')
290
- if ua:
291
- mpv_args += ['--user-agent=' + ua]
292
- mpv_args.append(url)
386
+ mpv_args = self.build_mpv_args(
387
+ mpv_path, url, title=title, headers=headers, ipc_socket=ipc_socket
388
+ )
293
389
 
294
390
  if self.console:
295
391
  from rich.text import Text
@@ -2,7 +2,7 @@
2
2
  # THIS IS THE SINGLE SOURCE OF VERSION
3
3
  # All other files (pyproject.toml, workflows, etc.) read from here
4
4
 
5
- __version__ = "1.9.0"
5
+ __version__ = "1.9.2"
6
6
 
7
7
  APP_VERSION = f"v{__version__}"
8
8
  GITHUB_REPO = "np4abdou1/ani-cli-arabic"