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 +1 -0
- weeb_cli/__main__.py +4 -0
- weeb_cli/commands/downloads.py +126 -0
- weeb_cli/commands/search.py +428 -0
- weeb_cli/commands/settings.py +254 -0
- weeb_cli/commands/setup.py +26 -0
- weeb_cli/commands/watchlist.py +130 -0
- weeb_cli/config.py +50 -0
- weeb_cli/i18n.py +65 -0
- weeb_cli/locales/en.json +168 -0
- weeb_cli/locales/tr.json +168 -0
- weeb_cli/main.py +85 -0
- weeb_cli/providers/__init__.py +21 -0
- weeb_cli/providers/animecix.py +276 -0
- weeb_cli/providers/anizle.py +450 -0
- weeb_cli/providers/base.py +98 -0
- weeb_cli/providers/registry.py +45 -0
- weeb_cli/providers/turkanime.py +499 -0
- weeb_cli/services/__init__.py +0 -0
- weeb_cli/services/dependency_manager.py +321 -0
- weeb_cli/services/details.py +32 -0
- weeb_cli/services/downloader.py +308 -0
- weeb_cli/services/player.py +47 -0
- weeb_cli/services/progress.py +136 -0
- weeb_cli/services/scraper.py +91 -0
- weeb_cli/services/search.py +16 -0
- weeb_cli/services/updater.py +199 -0
- weeb_cli/services/watch.py +19 -0
- weeb_cli/ui/__init__.py +1 -0
- weeb_cli/ui/header.py +30 -0
- weeb_cli/ui/menu.py +59 -0
- weeb_cli/ui/prompt.py +120 -0
- weeb_cli-1.0.0.dist-info/METADATA +148 -0
- weeb_cli-1.0.0.dist-info/RECORD +38 -0
- weeb_cli-1.0.0.dist-info/WHEEL +5 -0
- weeb_cli-1.0.0.dist-info/entry_points.txt +2 -0
- weeb_cli-1.0.0.dist-info/licenses/LICENSE +390 -0
- weeb_cli-1.0.0.dist-info/top_level.txt +1 -0
weeb_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
weeb_cli/__main__.py
ADDED
|
@@ -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
|