weeb-cli 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.
weeb_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
weeb_cli/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,126 @@
1
+ import questionary
2
+ from rich.console import Console
3
+ from rich.live import Live
4
+ from rich.table import Table
5
+ from weeb_cli.services.downloader import queue_manager
6
+ from weeb_cli.i18n import i18n
7
+ from weeb_cli.ui.header import show_header
8
+ import time
9
+
10
+ console = Console()
11
+
12
+ def show_downloads():
13
+ while True:
14
+ console.clear()
15
+ show_header(i18n.get("downloads.title"))
16
+
17
+ pending = queue_manager.get_pending_count()
18
+ is_running = queue_manager.is_running()
19
+
20
+ if pending > 0:
21
+ status = i18n.get("downloads.queue_running") if is_running else i18n.get("downloads.queue_stopped")
22
+ console.print(f"[cyan]{i18n.t('downloads.pending_count', count=pending)}[/cyan] - {status}\n")
23
+
24
+ queue = queue_manager.queue
25
+
26
+ if not queue:
27
+ console.print(f"[dim]{i18n.get('downloads.empty')}[/dim]")
28
+ try:
29
+ input(i18n.get("common.continue_key"))
30
+ except KeyboardInterrupt:
31
+ pass
32
+ return
33
+
34
+ opt_view = i18n.get("downloads.view_queue")
35
+ opt_start = i18n.get("downloads.start_queue")
36
+ opt_stop = i18n.get("downloads.stop_queue")
37
+ opt_clear = i18n.get("downloads.clear_completed")
38
+
39
+ choices = [opt_view]
40
+ if pending > 0:
41
+ if is_running:
42
+ choices.append(opt_stop)
43
+ else:
44
+ choices.append(opt_start)
45
+ choices.append(opt_clear)
46
+
47
+ try:
48
+ action = questionary.select(
49
+ i18n.get("downloads.action_prompt"),
50
+ choices=choices,
51
+ pointer=">",
52
+ use_shortcuts=False
53
+ ).ask()
54
+
55
+ if action is None:
56
+ return
57
+
58
+ if action == opt_view:
59
+ show_queue_live()
60
+ elif action == opt_start:
61
+ queue_manager.start_queue()
62
+ console.print(f"[green]{i18n.get('downloads.queue_started')}[/green]")
63
+ time.sleep(0.5)
64
+ elif action == opt_stop:
65
+ queue_manager.stop_queue()
66
+ console.print(f"[yellow]{i18n.get('downloads.queue_stopped')}[/yellow]")
67
+ time.sleep(0.5)
68
+ elif action == opt_clear:
69
+ queue_manager.queue = [i for i in queue_manager.queue if i["status"] in ["pending", "processing"]]
70
+ queue_manager._save_queue()
71
+ console.print(f"[green]{i18n.get('downloads.cleared')}[/green]")
72
+ time.sleep(0.5)
73
+
74
+ except KeyboardInterrupt:
75
+ return
76
+
77
+ def show_queue_live():
78
+ def generate_table():
79
+ table = Table(show_header=True, header_style="bold cyan")
80
+ table.add_column(i18n.get("watchlist.anime_title"), width=30)
81
+ table.add_column(i18n.get("details.episode"), justify="right", width=5)
82
+ table.add_column(i18n.get("downloads.status"), width=15)
83
+ table.add_column(i18n.get("downloads.progress"), width=20)
84
+
85
+ active = [i for i in queue_manager.queue if i["status"] == "processing"]
86
+ pending = [i for i in queue_manager.queue if i["status"] == "pending"]
87
+ finished = [i for i in queue_manager.queue if i["status"] in ["completed", "failed"]]
88
+ finished = finished[-10:]
89
+
90
+ display_list = active + pending + finished
91
+
92
+ for item in display_list:
93
+ status = item["status"]
94
+ style = "white"
95
+ if status == "processing":
96
+ style = "cyan"
97
+ elif status == "completed":
98
+ style = "green"
99
+ elif status == "failed":
100
+ style = "red"
101
+ elif status == "pending":
102
+ style = "dim"
103
+
104
+ progress = item.get("progress", 0)
105
+ bars = int(progress / 5)
106
+ bar_str = "█" * bars + "░" * (20 - bars)
107
+
108
+ status_text = i18n.get(f"downloads.status_{status}", status.upper())
109
+ p_text = f"{progress}%" if status == "processing" else ""
110
+
111
+ table.add_row(
112
+ f"[{style}]{item['anime_title'][:28]}[/{style}]",
113
+ f"{item['episode_number']}",
114
+ f"[{style}]{status_text}[/{style}]",
115
+ f"[{style}]{bar_str} {p_text}[/{style}]"
116
+ )
117
+
118
+ return table
119
+
120
+ try:
121
+ with Live(generate_table(), refresh_per_second=1) as live:
122
+ while True:
123
+ live.update(generate_table())
124
+ time.sleep(1)
125
+ except KeyboardInterrupt:
126
+ return
@@ -0,0 +1,428 @@
1
+ import questionary
2
+ from rich.console import Console
3
+ from weeb_cli.i18n import i18n
4
+ from weeb_cli.ui.header import show_header
5
+ from weeb_cli.services.search import search
6
+ from weeb_cli.services.details import get_details
7
+ from weeb_cli.services.watch import get_streams
8
+ from weeb_cli.services.player import player
9
+ from weeb_cli.services.progress import progress_tracker
10
+ from weeb_cli.services.downloader import queue_manager
11
+ from weeb_cli.services.scraper import scraper
12
+ import time
13
+
14
+ console = Console()
15
+
16
+ PLAYER_PRIORITY = [
17
+ "ALUCARD", "AMATERASU", "SIBNET", "MP4UPLOAD", "UQLOAD",
18
+ "MAIL", "DAILYMOTION", "SENDVID", "ODNOKLASSNIKI", "VK",
19
+ "VIDMOLY", "YOURUPLOAD", "MYVI", "GDRIVE", "PIXELDRAIN", "HDVID", "YADISK"
20
+ ]
21
+
22
+ def _get_player_priority(server_name: str) -> int:
23
+ server_upper = server_name.upper()
24
+ for i, p in enumerate(PLAYER_PRIORITY):
25
+ if p in server_upper:
26
+ return i
27
+ return 999
28
+
29
+ def _sort_streams(streams: list) -> list:
30
+ return sorted(streams, key=lambda s: _get_player_priority(s.get("server", "")))
31
+
32
+ def search_anime():
33
+ while True:
34
+ console.clear()
35
+ show_header(i18n.get("menu.options.search"), show_source=True)
36
+
37
+ history = progress_tracker.get_search_history()
38
+ if history:
39
+ console.print(f"[dim]{i18n.get('search.recent')}: {', '.join(history[:5])}[/dim]\n", justify="left")
40
+
41
+ try:
42
+ query = questionary.text(
43
+ i18n.get("search.prompt") + ":",
44
+ qmark=">",
45
+ style=questionary.Style([
46
+ ('qmark', 'fg:cyan bold'),
47
+ ('question', 'fg:white'),
48
+ ('answer', 'fg:cyan bold'),
49
+ ])
50
+ ).ask()
51
+
52
+ if query is None:
53
+ return
54
+
55
+ if not query.strip():
56
+ continue
57
+
58
+ progress_tracker.add_search_history(query.strip())
59
+
60
+ with console.status(i18n.get("search.searching"), spinner="dots"):
61
+ data = search(query)
62
+
63
+ if data is None:
64
+ time.sleep(1)
65
+ continue
66
+
67
+ if isinstance(data, dict):
68
+ if "results" in data and isinstance(data["results"], list):
69
+ data = data["results"]
70
+ elif "data" in data:
71
+ inner = data["data"]
72
+ if isinstance(inner, list):
73
+ data = inner
74
+ elif isinstance(inner, dict):
75
+ if "results" in inner and isinstance(inner["results"], list):
76
+ data = inner["results"]
77
+ elif "animes" in inner and isinstance(inner["animes"], list):
78
+ data = inner["animes"]
79
+ elif "items" in inner and isinstance(inner["items"], list):
80
+ data = inner["items"]
81
+
82
+ if not data or not isinstance(data, list):
83
+ console.print(f"[red]{i18n.get('search.no_results')}[/red]")
84
+ time.sleep(1.5)
85
+ continue
86
+
87
+ choices = []
88
+ for item in data:
89
+ title = item.get("title") or item.get("name")
90
+ if title:
91
+ choices.append(questionary.Choice(title, value=item))
92
+
93
+ if not choices:
94
+ console.print(f"[red]{i18n.get('search.no_results')}[/red]")
95
+ time.sleep(1.5)
96
+ continue
97
+
98
+
99
+
100
+ selected = questionary.select(
101
+ i18n.get("search.results"),
102
+ choices=choices,
103
+ pointer=">",
104
+ use_shortcuts=False,
105
+ style=questionary.Style([
106
+ ('pointer', 'fg:cyan bold'),
107
+ ('highlighted', 'fg:cyan'),
108
+ ('selected', 'fg:cyan bold'),
109
+ ])
110
+ ).ask()
111
+
112
+ if selected == "cancel" or selected is None:
113
+ continue
114
+
115
+ show_anime_details(selected)
116
+
117
+ except KeyboardInterrupt:
118
+ return
119
+
120
+ from weeb_cli.services.details import get_details
121
+
122
+ def show_anime_details(anime):
123
+ slug = anime.get("slug") or anime.get("id")
124
+ if not slug:
125
+ console.print(f"[red]{i18n.get('details.error_slug')}[/red]")
126
+ time.sleep(1)
127
+ return
128
+
129
+ while True:
130
+ console.clear()
131
+ show_header(anime.get("title") or anime.get("name") or "Anime")
132
+
133
+ with console.status(i18n.get("common.processing"), spinner="dots"):
134
+ details = get_details(slug)
135
+
136
+ # Parse response structure: { data: { details: { ... } } }
137
+ if isinstance(details, dict):
138
+ if "data" in details and isinstance(details["data"], dict):
139
+ details = details["data"]
140
+
141
+ # Capture source
142
+ source = details.get("source")
143
+
144
+ if "details" in details and isinstance(details["details"], dict):
145
+ details = details["details"]
146
+ # Restore source if captured
147
+ if source:
148
+ details["source"] = source
149
+
150
+ if not details:
151
+ console.print(f"[red]{i18n.get('details.not_found')}[/red]")
152
+ time.sleep(1)
153
+ return
154
+
155
+ from weeb_cli.config import config
156
+ desc = details.get("description") or details.get("synopsis") or details.get("desc")
157
+ show_desc = config.get("show_description", True)
158
+
159
+ opt_watch = i18n.get("details.watch")
160
+ opt_dl = i18n.get("details.download")
161
+
162
+ console.clear()
163
+ show_header(details.get("title", ""))
164
+
165
+ if show_desc and desc:
166
+ console.print(f"\n[dim]{desc}[/dim]\n", justify="left")
167
+
168
+ try:
169
+ action = questionary.select(
170
+ i18n.get("details.action_prompt"),
171
+ choices=[opt_watch, opt_dl],
172
+ pointer=">",
173
+ use_shortcuts=False
174
+ ).ask()
175
+
176
+ if action is None:
177
+ return
178
+
179
+ if action == opt_dl:
180
+ handle_download_flow(slug, details)
181
+ elif action == opt_watch:
182
+ handle_watch_flow(slug, details)
183
+
184
+ except KeyboardInterrupt:
185
+ return
186
+
187
+ def get_episodes_safe(details):
188
+ episodes = None
189
+ for k in ["episodes", "episodes_list", "episode_list", "results", "chapters"]:
190
+ if k in details and isinstance(details[k], list):
191
+ episodes = details[k]
192
+ break
193
+
194
+ if not episodes:
195
+ for v in details.values():
196
+ if isinstance(v, list) and v and isinstance(v[0], dict) and ("number" in v[0] or "ep_num" in v[0] or "url" in v[0]):
197
+ episodes = v
198
+ break
199
+ return episodes
200
+
201
+ def handle_watch_flow(slug, details):
202
+ episodes = get_episodes_safe(details)
203
+ if not episodes:
204
+ console.print(f"[yellow]{i18n.get('details.no_episodes')}[/yellow]")
205
+ time.sleep(1.5)
206
+ return
207
+
208
+ prog_data = progress_tracker.get_anime_progress(slug)
209
+ completed_ids = set(prog_data.get("completed", []))
210
+ last_watched = prog_data.get("last_watched", 0)
211
+ next_ep_num = last_watched + 1
212
+
213
+ while True:
214
+ ep_choices = []
215
+ for ep in episodes:
216
+ num_val = ep.get('number') or ep.get('ep_num')
217
+ try:
218
+ num = int(num_val)
219
+ except:
220
+ num = -1
221
+
222
+ prefix = " "
223
+ if num in completed_ids:
224
+ prefix = "✓ "
225
+ elif num == next_ep_num:
226
+ prefix = "● "
227
+
228
+ name = f"{prefix}{i18n.get('details.episode')} {num_val}"
229
+ ep_choices.append(questionary.Choice(name, value=ep))
230
+
231
+ try:
232
+ selected_ep = questionary.select(
233
+ i18n.get("details.select_episode") + ":",
234
+ choices=ep_choices,
235
+ pointer=">",
236
+ use_shortcuts=False
237
+ ).ask()
238
+
239
+ if selected_ep is None:
240
+ return
241
+
242
+ ep_id = selected_ep.get("id")
243
+ ep_num = selected_ep.get("number")
244
+
245
+ if not ep_id:
246
+ console.print(f"[red]{i18n.get('details.invalid_ep_id')}[/red]")
247
+ time.sleep(1)
248
+ continue
249
+
250
+ with console.status(i18n.get("common.processing"), spinner="dots"):
251
+ stream_resp = get_streams(slug, ep_id)
252
+
253
+ streams_list = []
254
+ if stream_resp and isinstance(stream_resp, dict):
255
+ data_node = stream_resp
256
+ for _ in range(3):
257
+ if "data" in data_node and isinstance(data_node["data"], (dict, list)):
258
+ data_node = data_node["data"]
259
+ else:
260
+ break
261
+
262
+ sources = None
263
+ if isinstance(data_node, list):
264
+ sources = data_node
265
+ elif isinstance(data_node, dict):
266
+ sources = data_node.get("links") or data_node.get("sources")
267
+
268
+ if sources and isinstance(sources, list):
269
+ streams_list = sources
270
+
271
+ if not streams_list:
272
+ error_msg = i18n.get('details.stream_not_found')
273
+ if scraper.last_error:
274
+ error_msg += f" [{scraper.last_error}]"
275
+ console.print(f"[red]{error_msg}[/red]")
276
+ time.sleep(1.5)
277
+ continue
278
+
279
+ streams_list = _sort_streams(streams_list)
280
+
281
+ stream_choices = []
282
+ for idx, s in enumerate(streams_list):
283
+ server = s.get("server", "Unknown")
284
+ quality = s.get("quality", "auto")
285
+ label = f"{server} ({quality})"
286
+ stream_choices.append(questionary.Choice(label, value=s))
287
+
288
+ if len(streams_list) == 1:
289
+ selected_stream = streams_list[0]
290
+ else:
291
+ selected_stream = questionary.select(
292
+ i18n.get("details.select_source"),
293
+ choices=stream_choices,
294
+ pointer=">",
295
+ use_shortcuts=False
296
+ ).ask()
297
+
298
+ if selected_stream is None:
299
+ continue
300
+
301
+ stream_url = selected_stream.get("url")
302
+
303
+ if not stream_url:
304
+ console.print(f"[red]{i18n.get('details.stream_not_found')}[/red]")
305
+ time.sleep(1.5)
306
+ continue
307
+
308
+ console.print(f"[green]{i18n.get('details.player_starting')}[/green]")
309
+ title = f"{details.get('title', 'Anime')} - Ep {ep_num}"
310
+
311
+ headers = {}
312
+ if details.get("source") == "hianime":
313
+ headers["Referer"] = "https://hianime.to"
314
+
315
+ success = player.play(stream_url, title=title, headers=headers)
316
+
317
+ if success:
318
+ try:
319
+ ans = questionary.confirm(i18n.get("details.mark_watched")).ask()
320
+ if ans:
321
+ n = int(ep_num)
322
+ total_eps = details.get("total_episodes") or len(episodes)
323
+ progress_tracker.mark_watched(
324
+ slug,
325
+ n,
326
+ title=details.get("title"),
327
+ total_episodes=total_eps
328
+ )
329
+
330
+ completed_ids.add(n)
331
+ if n >= next_ep_num:
332
+ next_ep_num = n + 1
333
+ except:
334
+ pass
335
+
336
+ except KeyboardInterrupt:
337
+ return
338
+
339
+ def handle_download_flow(slug, details):
340
+ episodes = get_episodes_safe(details)
341
+ if not episodes:
342
+ console.print(f"[yellow]{i18n.get('details.no_episodes')}[/yellow]")
343
+ time.sleep(1.5)
344
+ return
345
+
346
+ opt_all = i18n.get("details.download_options.all")
347
+ opt_manual = i18n.get("details.download_options.manual")
348
+ opt_range = i18n.get("details.download_options.range")
349
+
350
+ try:
351
+ mode = questionary.select(
352
+ i18n.get("details.download_options.prompt"),
353
+ choices=[opt_all, opt_manual, opt_range],
354
+ pointer=">",
355
+ use_shortcuts=False
356
+ ).ask()
357
+
358
+ if mode is None:
359
+ return
360
+
361
+ selected_eps = []
362
+
363
+ if mode == opt_all:
364
+ selected_eps = episodes
365
+
366
+ elif mode == opt_manual:
367
+ choices = []
368
+ for ep in episodes:
369
+ name = f"{i18n.get('details.episode')} {ep.get('number')}"
370
+ choices.append(questionary.Choice(name, value=ep))
371
+
372
+ selected_eps = questionary.checkbox(
373
+ "Select Episodes:",
374
+ choices=choices
375
+ ).ask()
376
+
377
+ elif mode == opt_range:
378
+ r_str = questionary.text(i18n.get("details.download_options.range_input")).ask()
379
+ if not r_str: return
380
+ nums = set()
381
+ try:
382
+ parts = r_str.split(',')
383
+ for p in parts:
384
+ p = p.strip()
385
+ if '-' in p:
386
+ s, e = p.split('-')
387
+ for x in range(int(s), int(e)+1): nums.add(x)
388
+ elif p.isdigit():
389
+ nums.add(int(p))
390
+ except:
391
+ console.print(f"[red]{i18n.get('details.download_options.range_error')}[/red]")
392
+ time.sleep(1)
393
+ return
394
+
395
+ selected_eps = [ep for ep in episodes if int(ep.get('number', -1)) in nums]
396
+
397
+ if not selected_eps:
398
+ return
399
+
400
+ anime_title = details.get("title") or "Unknown Anime"
401
+
402
+ opt_now = i18n.get("downloads.start_now")
403
+ opt_queue = i18n.get("downloads.add_to_queue")
404
+
405
+ action = questionary.select(
406
+ i18n.get("downloads.action_prompt"),
407
+ choices=[opt_now, opt_queue],
408
+ pointer=">",
409
+ use_shortcuts=False
410
+ ).ask()
411
+
412
+ if action is None:
413
+ return
414
+
415
+ added = queue_manager.add_to_queue(anime_title, selected_eps, slug)
416
+
417
+ if added > 0:
418
+ console.print(f"[green]{i18n.t('downloads.queued', count=added)}[/green]")
419
+
420
+ if action == opt_now:
421
+ queue_manager.start_queue()
422
+ else:
423
+ console.print(f"[yellow]{i18n.get('downloads.already_in_queue')}[/yellow]")
424
+
425
+ time.sleep(1)
426
+
427
+ except KeyboardInterrupt:
428
+ return