indonime 1.0.0__py3-none-any.whl
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.
- indonime/__init__.py +369 -0
- indonime/__main__.py +7 -0
- indonime/_mpv_install.py +132 -0
- indonime/ext/__init__.py +0 -0
- indonime/ext/megaNZ.py +365 -0
- indonime/ext/pdrain.py +25 -0
- indonime/ext/videodec.py +146 -0
- indonime/player.py +66 -0
- indonime/plugins/__init__.py +0 -0
- indonime/plugins/_base.py +72 -0
- indonime/plugins/anoboy.py +38 -0
- indonime/plugins/otakudesu.py +49 -0
- indonime/ui.py +259 -0
- indonime-1.0.0.dist-info/METADATA +208 -0
- indonime-1.0.0.dist-info/RECORD +18 -0
- indonime-1.0.0.dist-info/WHEEL +5 -0
- indonime-1.0.0.dist-info/licenses/LICENSE +21 -0
- indonime-1.0.0.dist-info/top_level.txt +1 -0
indonime/__init__.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Indonime TUI — search → select → play loop."""
|
|
2
|
+
import argparse
|
|
3
|
+
import importlib
|
|
4
|
+
import os
|
|
5
|
+
import pkgutil
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from . import player
|
|
9
|
+
from .ui import (
|
|
10
|
+
console, print_banner, print_header, print_step,
|
|
11
|
+
print_success, print_error, print_warning, print_info, print_separator,
|
|
12
|
+
make_episode_table, make_postplay_actions, make_footer,
|
|
13
|
+
make_progress_bar, styled_status, make_style, Palette,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from . import plugins
|
|
17
|
+
import requests
|
|
18
|
+
from InquirerPy import inquirer
|
|
19
|
+
from .ext import pdrain, megaNZ
|
|
20
|
+
|
|
21
|
+
_SESSION = requests.Session()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _play_episode(episode_url, plugin, custom_style):
|
|
25
|
+
"""Resolve stream and play. Returns True if played successfully."""
|
|
26
|
+
with console.status(styled_status("🔍 Resolving stream...")):
|
|
27
|
+
dl_links = plugin.downloads(episode_url)
|
|
28
|
+
|
|
29
|
+
options = []
|
|
30
|
+
opt_map = {}
|
|
31
|
+
for res, servers in dl_links.items():
|
|
32
|
+
for s_name, s_url in servers.items():
|
|
33
|
+
if any(x in s_name.lower() for x in ['pdrain', 'pixeldrain', 'mega']):
|
|
34
|
+
label = f'[{res}] {s_name}'
|
|
35
|
+
options.append({'name': label, 'value': s_url})
|
|
36
|
+
opt_map[s_url] = label
|
|
37
|
+
|
|
38
|
+
if not options:
|
|
39
|
+
print_warning("No compatible servers found.")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
selected_opt = inquirer.select(
|
|
43
|
+
message='📥 Select quality & server:',
|
|
44
|
+
choices=options,
|
|
45
|
+
style=custom_style,
|
|
46
|
+
).execute()
|
|
47
|
+
if not selected_opt:
|
|
48
|
+
return False
|
|
49
|
+
server_url = selected_opt
|
|
50
|
+
last_selected_server_name = opt_map[server_url]
|
|
51
|
+
|
|
52
|
+
final_target = None
|
|
53
|
+
is_temp = False
|
|
54
|
+
|
|
55
|
+
# ── Indeterminate pulse bar while resolving ──
|
|
56
|
+
if 'mega' in last_selected_server_name.lower():
|
|
57
|
+
final_mega_url = None
|
|
58
|
+
with make_progress_bar() as progress:
|
|
59
|
+
task = progress.add_task(
|
|
60
|
+
f"[{Palette.primary}]🔓 Resolving Mega link...",
|
|
61
|
+
total=None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
resp = _SESSION.get(server_url, allow_redirects=True, timeout=15)
|
|
66
|
+
curr = resp.url
|
|
67
|
+
if "mega.nz" in curr and ("#" in curr or "#!" in curr):
|
|
68
|
+
final_mega_url = curr
|
|
69
|
+
else:
|
|
70
|
+
print_error("Redirect tidak mengarah ke Mega.")
|
|
71
|
+
return False
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print_error(f"Requests Error: {e}")
|
|
74
|
+
time.sleep(3)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
if not final_mega_url:
|
|
78
|
+
print_error("Timeout: Gagal mendapatkan link Mega.")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
if "#!" in final_mega_url:
|
|
82
|
+
final_mega_url = final_mega_url.replace("#!", "file/").replace("!", "#", 1)
|
|
83
|
+
|
|
84
|
+
progress.update(task,
|
|
85
|
+
description=f"[{Palette.highlight}]📥 Buffering stream...")
|
|
86
|
+
|
|
87
|
+
# ponytail: sequential streaming (resolve_mega_file_stream) over parallel Range requests.
|
|
88
|
+
# parallel approach fails on some MEGA CDNs that don't support Range or silently drop
|
|
89
|
+
# connections. Streaming is one GET, no Range, more compatible.
|
|
90
|
+
try:
|
|
91
|
+
f_id = final_mega_url.split("file/")[1].split("#")[0]
|
|
92
|
+
stream = megaNZ.resolve_mega_file_stream(final_mega_url, f_id, console)
|
|
93
|
+
if stream is None:
|
|
94
|
+
return False
|
|
95
|
+
path, ready, stop, dl_thread = stream
|
|
96
|
+
|
|
97
|
+
_stall_t0 = time.time()
|
|
98
|
+
while not ready.is_set():
|
|
99
|
+
if time.time() - _stall_t0 > 60:
|
|
100
|
+
print_warning("Buffering timed out (>60s). Check connection or retry.")
|
|
101
|
+
return False
|
|
102
|
+
time.sleep(0.15)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print_error(f"Gagal Streaming: {e}")
|
|
105
|
+
time.sleep(3)
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
# Buffering complete -> launch mpv
|
|
109
|
+
print_step("🚀 Launching mpv player...")
|
|
110
|
+
player.play_with_mpv(path, is_temp_file=True, cleanup=False)
|
|
111
|
+
stop.set()
|
|
112
|
+
dl_thread.join(timeout=10)
|
|
113
|
+
if os.path.exists(path):
|
|
114
|
+
os.remove(path)
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
else:
|
|
118
|
+
# PixelDrain path (indeterminate pulse bar)
|
|
119
|
+
with make_progress_bar() as progress:
|
|
120
|
+
task = progress.add_task(
|
|
121
|
+
f"[{Palette.secondary}]🌀 Bypassing PixelDrain link...",
|
|
122
|
+
total=None,
|
|
123
|
+
)
|
|
124
|
+
import threading as _th
|
|
125
|
+
result_holder = [None]
|
|
126
|
+
def _do_scrape():
|
|
127
|
+
result_holder[0] = pdrain.scrape(server_url)
|
|
128
|
+
scrape_thread = _th.Thread(target=_do_scrape, daemon=True)
|
|
129
|
+
scrape_thread.start()
|
|
130
|
+
while scrape_thread.is_alive():
|
|
131
|
+
time.sleep(0.1)
|
|
132
|
+
final_target = result_holder[0]
|
|
133
|
+
|
|
134
|
+
if final_target:
|
|
135
|
+
print_step("🚀 Launching mpv player...")
|
|
136
|
+
return player.play_with_mpv(final_target, is_temp_file=is_temp)
|
|
137
|
+
else:
|
|
138
|
+
print_error("Stream resolution failed.")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _episode_nav(episode_list, plugin, custom_style, back_label='<< BACK', show_banner=True):
|
|
143
|
+
"""Episode pick → play → post-play loop. Returns 'back' or 'quit'."""
|
|
144
|
+
idx = 0
|
|
145
|
+
while True:
|
|
146
|
+
if show_banner:
|
|
147
|
+
print_banner()
|
|
148
|
+
print_header("📋 EPISODES", "🎬")
|
|
149
|
+
console.print(make_episode_table(episode_list))
|
|
150
|
+
print_separator()
|
|
151
|
+
|
|
152
|
+
ep_choices = [
|
|
153
|
+
{'name': f' EP{i+1:02d} — {ep["title"][:50]}', 'value': i}
|
|
154
|
+
for i, ep in enumerate(episode_list)
|
|
155
|
+
]
|
|
156
|
+
ep_choices.append({'name': f' ↩ {back_label}', 'value': 'back'})
|
|
157
|
+
|
|
158
|
+
selected = inquirer.select(
|
|
159
|
+
message='▶ Select episode:',
|
|
160
|
+
choices=ep_choices,
|
|
161
|
+
default=idx,
|
|
162
|
+
style=custom_style,
|
|
163
|
+
qmark="",
|
|
164
|
+
).execute()
|
|
165
|
+
|
|
166
|
+
if selected == 'back' or selected is None:
|
|
167
|
+
from .plugins._base import cache_clear
|
|
168
|
+
cache_clear()
|
|
169
|
+
return 'back'
|
|
170
|
+
idx = selected
|
|
171
|
+
|
|
172
|
+
# ponytail: inner loop keeps QUALITY/REPLAY on same episode instead of reopening episode list
|
|
173
|
+
while True:
|
|
174
|
+
print_header("🎬 NOW PLAYING", "▶")
|
|
175
|
+
if not _play_episode(episode_list[idx]['url'], plugin, custom_style):
|
|
176
|
+
time.sleep(2)
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
print_separator()
|
|
180
|
+
|
|
181
|
+
post_choices = make_postplay_actions(idx, len(episode_list))
|
|
182
|
+
cmd = inquirer.select(
|
|
183
|
+
message="🎮 Command:",
|
|
184
|
+
choices=post_choices,
|
|
185
|
+
style=custom_style,
|
|
186
|
+
qmark="",
|
|
187
|
+
).execute()
|
|
188
|
+
|
|
189
|
+
if cmd == '▶ NEXT':
|
|
190
|
+
if idx + 1 < len(episode_list):
|
|
191
|
+
idx += 1
|
|
192
|
+
continue
|
|
193
|
+
break
|
|
194
|
+
elif cmd == '◀ PREV':
|
|
195
|
+
if idx > 0:
|
|
196
|
+
idx -= 1
|
|
197
|
+
continue
|
|
198
|
+
break
|
|
199
|
+
elif cmd in ('↺ REPLAY', '⚙ QUALITY'):
|
|
200
|
+
continue # replay same episode / choose different quality
|
|
201
|
+
else:
|
|
202
|
+
return 'quit'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _tui_loop():
|
|
206
|
+
p_name = 'otakudesu'
|
|
207
|
+
_last_plugin_name = None
|
|
208
|
+
_plugin = None
|
|
209
|
+
custom_style = make_style()
|
|
210
|
+
available_providers = [
|
|
211
|
+
m.name for m in pkgutil.iter_modules(plugins.__path__)
|
|
212
|
+
if not m.name.startswith('_')
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
while True:
|
|
216
|
+
print_banner()
|
|
217
|
+
|
|
218
|
+
if p_name != _last_plugin_name:
|
|
219
|
+
_plugin = importlib.import_module(f'indonime.plugins.{p_name}')
|
|
220
|
+
_last_plugin_name = p_name
|
|
221
|
+
|
|
222
|
+
is_switching = False
|
|
223
|
+
try:
|
|
224
|
+
prompt = inquirer.text(
|
|
225
|
+
message='🔍 Search anime:',
|
|
226
|
+
qmark='',
|
|
227
|
+
instruction='[alt+p] switch provider',
|
|
228
|
+
style=custom_style,
|
|
229
|
+
validate=lambda x: True if is_switching else len(x) > 0,
|
|
230
|
+
)
|
|
231
|
+
@prompt.register_kb('alt-p')
|
|
232
|
+
def _(event):
|
|
233
|
+
nonlocal is_switching
|
|
234
|
+
is_switching = True
|
|
235
|
+
event.app.exit(result='/switch')
|
|
236
|
+
result = prompt.execute()
|
|
237
|
+
except KeyboardInterrupt:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
if result == '/switch':
|
|
241
|
+
new_p = inquirer.select(
|
|
242
|
+
message='📡 Select provider:',
|
|
243
|
+
choices=available_providers,
|
|
244
|
+
qmark='',
|
|
245
|
+
style=custom_style,
|
|
246
|
+
).execute()
|
|
247
|
+
if new_p:
|
|
248
|
+
p_name = new_p
|
|
249
|
+
continue
|
|
250
|
+
if not result:
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
print_header("🔎 SEARCHING", "🔎")
|
|
254
|
+
with console.status(styled_status(f'Searching for "{result}"...')):
|
|
255
|
+
results = _plugin.search_anime(result)
|
|
256
|
+
|
|
257
|
+
if not results:
|
|
258
|
+
print_warning("No results found.")
|
|
259
|
+
time.sleep(2)
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
choices = [item['title'] for item in results] + ['-- ABORT --']
|
|
263
|
+
selected_title = inquirer.fuzzy(
|
|
264
|
+
message='📺 Select title:',
|
|
265
|
+
choices=choices,
|
|
266
|
+
style=custom_style,
|
|
267
|
+
).execute()
|
|
268
|
+
|
|
269
|
+
if selected_title == '-- ABORT --' or not selected_title:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
selected_url = next(
|
|
273
|
+
item['url'] for item in results
|
|
274
|
+
if item['title'] == selected_title
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
print_header("📋 EPISODES", "🎬")
|
|
278
|
+
with console.status(styled_status("Fetching episode list...")):
|
|
279
|
+
episode_list = _plugin.episodes(selected_url)
|
|
280
|
+
|
|
281
|
+
if not episode_list:
|
|
282
|
+
print_warning("No episodes found.")
|
|
283
|
+
time.sleep(2)
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
if _episode_nav(
|
|
287
|
+
episode_list, _plugin, custom_style,
|
|
288
|
+
back_label='<< BACK TO SEARCH', show_banner=True
|
|
289
|
+
) == 'quit':
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
print_banner()
|
|
293
|
+
make_footer()
|
|
294
|
+
print_success("Thanks for using Indonime! ~ Sayonara ~")
|
|
295
|
+
|
|
296
|
+
def _search_mode(query, provider='otakudesu'):
|
|
297
|
+
"""One-shot search → play → exit."""
|
|
298
|
+
custom_style = make_style()
|
|
299
|
+
print_banner()
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
plugin = importlib.import_module(f'indonime.plugins.{provider}')
|
|
303
|
+
except Exception as e:
|
|
304
|
+
print_error(f"Plugin error: {e}")
|
|
305
|
+
input('[Press Enter]')
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
print_header("🔎 SEARCHING", "🔎")
|
|
309
|
+
with console.status(styled_status(f'Searching for "{query}"...')):
|
|
310
|
+
results = plugin.search_anime(query)
|
|
311
|
+
|
|
312
|
+
if not results:
|
|
313
|
+
print_warning("No results found.")
|
|
314
|
+
input('[Press Enter]')
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
choices = [item['title'] for item in results] + ['-- ABORT --']
|
|
318
|
+
selected_title = inquirer.fuzzy(
|
|
319
|
+
message='📺 Select title:',
|
|
320
|
+
choices=choices,
|
|
321
|
+
style=custom_style,
|
|
322
|
+
).execute()
|
|
323
|
+
|
|
324
|
+
if selected_title == '-- ABORT --' or not selected_title:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
selected_url = next(
|
|
328
|
+
item['url'] for item in results
|
|
329
|
+
if item['title'] == selected_title
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
print_header("📋 EPISODES", "🎬")
|
|
333
|
+
with console.status(styled_status("Fetching episode list...")):
|
|
334
|
+
episode_list = plugin.episodes(selected_url)
|
|
335
|
+
|
|
336
|
+
if not episode_list:
|
|
337
|
+
print_warning("No episodes found.")
|
|
338
|
+
input('[Press Enter]')
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
_episode_nav(
|
|
342
|
+
episode_list, plugin, custom_style,
|
|
343
|
+
back_label='<< QUIT', show_banner=False
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if player.current_mpv_process and player.current_mpv_process.poll() is None:
|
|
347
|
+
player.current_mpv_process.wait()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main():
|
|
351
|
+
parser = argparse.ArgumentParser(
|
|
352
|
+
description='Indonime — Subtitle Indonesia Anime Searcher'
|
|
353
|
+
)
|
|
354
|
+
parser.add_argument(
|
|
355
|
+
'mode', nargs='?', default='tui',
|
|
356
|
+
help='Mode: tui (interactive, default) or search <query>'
|
|
357
|
+
)
|
|
358
|
+
parser.add_argument('query', nargs='*', help='Search query')
|
|
359
|
+
parser.add_argument(
|
|
360
|
+
'-p', '--provider', default='otakudesu',
|
|
361
|
+
choices=['otakudesu', 'anoboy'],
|
|
362
|
+
help='Provider (default: otakudesu)'
|
|
363
|
+
)
|
|
364
|
+
args = parser.parse_args()
|
|
365
|
+
|
|
366
|
+
if args.mode == 'search' and args.query:
|
|
367
|
+
_search_mode(' '.join(args.query), args.provider)
|
|
368
|
+
else:
|
|
369
|
+
_tui_loop()
|
indonime/__main__.py
ADDED
indonime/_mpv_install.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cross-platform mpv installer for Indonime."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
import urllib.request
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.progress import (
|
|
12
|
+
Progress, BarColumn, DownloadColumn,
|
|
13
|
+
TransferSpeedColumn, TimeRemainingColumn,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
BASE = Path(__file__).resolve().parent.parent # project root (indonime/)
|
|
19
|
+
MPV_DIR = BASE / "mpv"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _download(url, dest, desc="Downloading"):
|
|
23
|
+
dest = Path(dest)
|
|
24
|
+
try:
|
|
25
|
+
resp = urllib.request.urlopen(url)
|
|
26
|
+
total = int(resp.headers.get("content-length", 0))
|
|
27
|
+
with Progress(
|
|
28
|
+
"[progress.description]{task.description}", BarColumn(),
|
|
29
|
+
DownloadColumn(), TransferSpeedColumn(), TimeRemainingColumn(),
|
|
30
|
+
console=console,
|
|
31
|
+
) as p:
|
|
32
|
+
task = p.add_task(f"[cyan]{desc}", total=total)
|
|
33
|
+
with open(dest, "wb") as f:
|
|
34
|
+
for chunk in iter(lambda: resp.read(64 * 1024), b""):
|
|
35
|
+
f.write(chunk)
|
|
36
|
+
p.update(task, advance=len(chunk))
|
|
37
|
+
return True
|
|
38
|
+
except Exception as e:
|
|
39
|
+
console.print(f"[red]✘ Download failed: {e}[/red]")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _install_windows():
|
|
44
|
+
"""Download mpv.7z, extract via 7zr.exe (standalone)."""
|
|
45
|
+
mpv_url = "https://github.com/salsa-ram/indonime/releases/download/v1.0.0-mpv/mpv.7z"
|
|
46
|
+
mpv_7z = BASE / "mpv.7z"
|
|
47
|
+
|
|
48
|
+
if not _download(mpv_url, mpv_7z, "Downloading MPV"):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
console.print("[cyan]Extracting...[/cyan]")
|
|
52
|
+
MPV_DIR.mkdir(exist_ok=True)
|
|
53
|
+
|
|
54
|
+
for exe, args in [
|
|
55
|
+
(r"C:\Program Files\7-Zip\7z.exe", [r"C:\Program Files\7-Zip\7z.exe", "x", str(mpv_7z), f"-o{MPV_DIR}", "-y"]),
|
|
56
|
+
(r"C:\Program Files\WinRAR\WinRAR.exe", [r"C:\Program Files\WinRAR\WinRAR.exe", "x", str(mpv_7z), f"{MPV_DIR}\\"]),
|
|
57
|
+
]:
|
|
58
|
+
if os.path.exists(exe):
|
|
59
|
+
subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
60
|
+
if (MPV_DIR / "mpv.exe").exists():
|
|
61
|
+
break
|
|
62
|
+
else:
|
|
63
|
+
console.print("[cyan]Downloading 7-Zip standalone extractor...[/cyan]")
|
|
64
|
+
if not _download("https://www.7-zip.org/a/7zr.exe", BASE / "7zr.exe"):
|
|
65
|
+
return False
|
|
66
|
+
subprocess.run([str(BASE / "7zr.exe"), "x", str(mpv_7z), f"-o{MPV_DIR}", "-y"],
|
|
67
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
68
|
+
(BASE / "7zr.exe").unlink(missing_ok=True) # ponytail: BCJ2 archive, needs 7z
|
|
69
|
+
|
|
70
|
+
mpv_7z.unlink(missing_ok=True)
|
|
71
|
+
ok = (MPV_DIR / "mpv.exe").exists()
|
|
72
|
+
console.print("[green]✓ mpv installed[/green]" if ok else "[red]✘ Extraction failed[/red]")
|
|
73
|
+
return ok
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _install_macos():
|
|
77
|
+
if shutil.which("brew"):
|
|
78
|
+
console.print("[cyan]Installing mpv via Homebrew...[/cyan]")
|
|
79
|
+
r = subprocess.run(["brew", "install", "mpv"], capture_output=True, text=True)
|
|
80
|
+
if r.returncode == 0:
|
|
81
|
+
console.print("[green]✓ mpv installed via Homebrew[/green]")
|
|
82
|
+
return True
|
|
83
|
+
console.print(f"[yellow]⚠ Homebrew failed: {r.stderr.strip()}[/yellow]")
|
|
84
|
+
console.print("[yellow]Install manually: brew install mpv[/yellow]")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _install_linux():
|
|
89
|
+
for pm, cmd in [
|
|
90
|
+
("apt", ["apt", "install", "-y", "mpv"]),
|
|
91
|
+
("pacman", ["pacman", "-S", "--noconfirm", "mpv"]),
|
|
92
|
+
("dnf", ["dnf", "install", "-y", "mpv"]),
|
|
93
|
+
("zypper", ["zypper", "install", "-y", "mpv"]),
|
|
94
|
+
]:
|
|
95
|
+
if shutil.which(pm):
|
|
96
|
+
console.print(f"[cyan]Installing mpv via {pm}...[/cyan]")
|
|
97
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
98
|
+
if r.returncode == 0:
|
|
99
|
+
console.print(f"[green]✓ mpv installed via {pm}[/green]")
|
|
100
|
+
return True
|
|
101
|
+
console.print(f"[yellow]⚠ {pm} failed[/yellow]")
|
|
102
|
+
console.print("[yellow]Install manually: apt install mpv[/yellow]")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
console.print("[bold cyan]📺 Indonime - MPV Setup[/bold cyan]\n")
|
|
108
|
+
|
|
109
|
+
if shutil.which("mpv"):
|
|
110
|
+
console.print("[green]✓ mpv already in PATH[/green]")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
system = platform.system()
|
|
114
|
+
console.print(f"[dim]OS: {system}[/dim]")
|
|
115
|
+
|
|
116
|
+
ok = {
|
|
117
|
+
"Windows": _install_windows,
|
|
118
|
+
"Darwin": _install_macos,
|
|
119
|
+
}.get(system, _install_linux)()
|
|
120
|
+
|
|
121
|
+
if not ok:
|
|
122
|
+
console.print("\n[yellow]Install mpv manually:[/yellow]")
|
|
123
|
+
console.print(" Windows: https://mpv.io/installation/")
|
|
124
|
+
console.print(" macOS: brew install mpv")
|
|
125
|
+
console.print(" Linux: apt install mpv")
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
console.print("[bold green]✓ Setup complete![/bold green]")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
indonime/ext/__init__.py
ADDED
|
File without changes
|