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.
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/PKG-INFO +2 -1
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/PKG-INFO +2 -1
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/SOURCES.txt +1 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/requires.txt +1 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/app.py +184 -5
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/config.py +3 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/player.py +130 -34
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/version.py +1 -1
- ani_cli_ar-1.9.2/ani_cli_arabic/watch_together.py +823 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/pyproject.toml +1 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/LICENSE +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/README.md +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/dependency_links.txt +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/entry_points.txt +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_ar.egg-info/top_level.txt +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/__init__.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/__main__.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/api.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/cli.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/deps.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/discord_rpc.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/favorites.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/history.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/models.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/monitoring.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/__init__.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/anikoto.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/animepahe.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/api_provider.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/base.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/gogoanime.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/miruro.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/mkissa.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/scrapers/provider_manager.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/settings.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/storage.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/ui.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/updater.py +0 -0
- {ani_cli_ar-1.9.0 → ani_cli_ar-1.9.2}/ani_cli_arabic/utils.py +0 -0
- {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.
|
|
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.
|
|
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>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|