weeb-cli 0.1.2__tar.gz → 0.1.3__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.
- {weeb_cli-0.1.2/weeb_cli.egg-info → weeb_cli-0.1.3}/PKG-INFO +1 -1
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/pyproject.toml +7 -3
- weeb_cli-0.1.3/weeb_cli/__init__.py +1 -0
- weeb_cli-0.1.3/weeb_cli/commands/downloads.py +70 -0
- weeb_cli-0.1.3/weeb_cli/commands/search.py +345 -0
- weeb_cli-0.1.3/weeb_cli/commands/settings.py +238 -0
- weeb_cli-0.1.3/weeb_cli/commands/setup.py +26 -0
- weeb_cli-0.1.3/weeb_cli/commands/watchlist.py +14 -0
- weeb_cli-0.1.3/weeb_cli/locales/en.json +109 -0
- weeb_cli-0.1.3/weeb_cli/locales/tr.json +109 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli/main.py +5 -1
- weeb_cli-0.1.3/weeb_cli/services/__init__.py +0 -0
- weeb_cli-0.1.3/weeb_cli/services/api.py +46 -0
- weeb_cli-0.1.3/weeb_cli/services/dependency_manager.py +321 -0
- weeb_cli-0.1.3/weeb_cli/services/details.py +4 -0
- weeb_cli-0.1.3/weeb_cli/services/downloader.py +241 -0
- weeb_cli-0.1.3/weeb_cli/services/player.py +47 -0
- weeb_cli-0.1.3/weeb_cli/services/progress.py +60 -0
- weeb_cli-0.1.3/weeb_cli/services/search.py +4 -0
- weeb_cli-0.1.3/weeb_cli/services/updater.py +54 -0
- weeb_cli-0.1.3/weeb_cli/services/watch.py +4 -0
- weeb_cli-0.1.3/weeb_cli/ui/__init__.py +1 -0
- weeb_cli-0.1.3/weeb_cli/ui/header.py +30 -0
- weeb_cli-0.1.3/weeb_cli/ui/menu.py +59 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3/weeb_cli.egg-info}/PKG-INFO +1 -1
- weeb_cli-0.1.3/weeb_cli.egg-info/SOURCES.txt +34 -0
- weeb_cli-0.1.2/weeb_cli/__init__.py +0 -1
- weeb_cli-0.1.2/weeb_cli.egg-info/SOURCES.txt +0 -14
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/LICENSE +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/README.md +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/setup.cfg +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli/__main__.py +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli/config.py +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli/i18n.py +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli.egg-info/dependency_links.txt +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli.egg-info/entry_points.txt +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli.egg-info/requires.txt +0 -0
- {weeb_cli-0.1.2 → weeb_cli-0.1.3}/weeb_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weeb-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Tarayıcı yok, reklam yok, dikkat dağıtıcı unsur yok. Sadece siz ve eşsiz bir anime izleme deneyimi."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "ewgsta", email = "ewgst@proton.me" }]
|
|
@@ -23,8 +23,12 @@ dependencies = [
|
|
|
23
23
|
"requests"
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
[tool.setuptools]
|
|
27
|
-
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["weeb_cli*"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
weeb_cli = ["locales/*.json"]
|
|
28
32
|
|
|
29
33
|
[project.urls]
|
|
30
34
|
Homepage = "https://github.com/ewgsta/weeb-cli"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.live import Live
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich.progress import ProgressBar, BarColumn
|
|
5
|
+
from weeb_cli.services.downloader import queue_manager
|
|
6
|
+
from weeb_cli.i18n import i18n
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
def show_downloads():
|
|
12
|
+
console.clear()
|
|
13
|
+
console.print(f"[bold cyan]{i18n.get('details.download')}[/bold cyan] ([dim]Ctrl+C to exit[/dim])\n")
|
|
14
|
+
|
|
15
|
+
def generate_table():
|
|
16
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
17
|
+
table.add_column("Anime", width=30)
|
|
18
|
+
table.add_column("Ep", justify="right", width=5)
|
|
19
|
+
table.add_column("Status", width=15)
|
|
20
|
+
table.add_column("Progress", width=20)
|
|
21
|
+
table.add_column("ETA", width=10)
|
|
22
|
+
|
|
23
|
+
queue = queue_manager.queue
|
|
24
|
+
|
|
25
|
+
active = [i for i in queue if i["status"] == "processing"]
|
|
26
|
+
pending = [i for i in queue if i["status"] == "pending"]
|
|
27
|
+
finished = [i for i in queue if i["status"] in ["completed", "failed"]]
|
|
28
|
+
finished = finished[-5:]
|
|
29
|
+
|
|
30
|
+
display_list = active + pending + finished
|
|
31
|
+
|
|
32
|
+
if not display_list:
|
|
33
|
+
table.add_row("-", "-", "Empty", "-", "-")
|
|
34
|
+
return table
|
|
35
|
+
|
|
36
|
+
for item in display_list:
|
|
37
|
+
status = item["status"]
|
|
38
|
+
style = "white"
|
|
39
|
+
if status == "processing":
|
|
40
|
+
style = "cyan"
|
|
41
|
+
elif status == "completed":
|
|
42
|
+
style = "green"
|
|
43
|
+
elif status == "failed":
|
|
44
|
+
style = "red"
|
|
45
|
+
elif status == "pending":
|
|
46
|
+
style = "dim"
|
|
47
|
+
|
|
48
|
+
progress = item.get("progress", 0)
|
|
49
|
+
bars = int(progress / 5)
|
|
50
|
+
bar_str = "█" * bars + "░" * (20 - bars)
|
|
51
|
+
|
|
52
|
+
p_text = f"{progress}%" if status == "processing" else ""
|
|
53
|
+
|
|
54
|
+
table.add_row(
|
|
55
|
+
f"[{style}]{item['anime_title'][:28]}[/{style}]",
|
|
56
|
+
f"{item['episode_number']}",
|
|
57
|
+
f"[{style}]{status.upper()}[/{style}]",
|
|
58
|
+
f"[{style}]{bar_str} {p_text}[/{style}]",
|
|
59
|
+
f"{item.get('eta', '-')}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return table
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
with Live(generate_table(), refresh_per_second=1) as live:
|
|
66
|
+
while True:
|
|
67
|
+
live.update(generate_table())
|
|
68
|
+
time.sleep(1)
|
|
69
|
+
except KeyboardInterrupt:
|
|
70
|
+
return
|
|
@@ -0,0 +1,345 @@
|
|
|
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
|
+
import time
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
def search_anime():
|
|
16
|
+
while True:
|
|
17
|
+
console.clear()
|
|
18
|
+
show_header(i18n.get("menu.options.search"), show_source=True)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
query = questionary.text(
|
|
22
|
+
i18n.get("search.prompt") + ":",
|
|
23
|
+
qmark=">",
|
|
24
|
+
style=questionary.Style([
|
|
25
|
+
('qmark', 'fg:cyan bold'),
|
|
26
|
+
('question', 'fg:white'),
|
|
27
|
+
('answer', 'fg:cyan bold'),
|
|
28
|
+
])
|
|
29
|
+
).ask()
|
|
30
|
+
|
|
31
|
+
if query is None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if not query.strip():
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
with console.status(i18n.get("search.searching"), spinner="dots"):
|
|
38
|
+
data = search(query)
|
|
39
|
+
|
|
40
|
+
if data is None:
|
|
41
|
+
time.sleep(1)
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
if isinstance(data, dict):
|
|
45
|
+
if "results" in data and isinstance(data["results"], list):
|
|
46
|
+
data = data["results"]
|
|
47
|
+
elif "data" in data:
|
|
48
|
+
inner = data["data"]
|
|
49
|
+
if isinstance(inner, list):
|
|
50
|
+
data = inner
|
|
51
|
+
elif isinstance(inner, dict):
|
|
52
|
+
if "results" in inner and isinstance(inner["results"], list):
|
|
53
|
+
data = inner["results"]
|
|
54
|
+
elif "animes" in inner and isinstance(inner["animes"], list):
|
|
55
|
+
data = inner["animes"]
|
|
56
|
+
elif "items" in inner and isinstance(inner["items"], list):
|
|
57
|
+
data = inner["items"]
|
|
58
|
+
|
|
59
|
+
if not data or not isinstance(data, list):
|
|
60
|
+
console.print(f"[red]{i18n.get('search.no_results')}[/red]")
|
|
61
|
+
time.sleep(1.5)
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
choices = []
|
|
65
|
+
for item in data:
|
|
66
|
+
title = item.get("title") or item.get("name")
|
|
67
|
+
if title:
|
|
68
|
+
choices.append(questionary.Choice(title, value=item))
|
|
69
|
+
|
|
70
|
+
if not choices:
|
|
71
|
+
console.print(f"[red]{i18n.get('search.no_results')}[/red]")
|
|
72
|
+
time.sleep(1.5)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
selected = questionary.select(
|
|
78
|
+
i18n.get("search.results"),
|
|
79
|
+
choices=choices,
|
|
80
|
+
pointer=">",
|
|
81
|
+
use_shortcuts=False,
|
|
82
|
+
style=questionary.Style([
|
|
83
|
+
('pointer', 'fg:cyan bold'),
|
|
84
|
+
('highlighted', 'fg:cyan'),
|
|
85
|
+
('selected', 'fg:cyan bold'),
|
|
86
|
+
])
|
|
87
|
+
).ask()
|
|
88
|
+
|
|
89
|
+
if selected == "cancel" or selected is None:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
show_anime_details(selected)
|
|
93
|
+
|
|
94
|
+
except KeyboardInterrupt:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
from weeb_cli.services.details import get_details
|
|
98
|
+
|
|
99
|
+
def show_anime_details(anime):
|
|
100
|
+
slug = anime.get("slug") or anime.get("id")
|
|
101
|
+
if not slug:
|
|
102
|
+
console.print(f"[red]{i18n.get('details.error_slug')}[/red]")
|
|
103
|
+
time.sleep(1)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
while True:
|
|
107
|
+
console.clear()
|
|
108
|
+
show_header(anime.get("title") or anime.get("name") or "Anime")
|
|
109
|
+
|
|
110
|
+
with console.status(i18n.get("common.processing"), spinner="dots"):
|
|
111
|
+
details = get_details(slug)
|
|
112
|
+
|
|
113
|
+
# Parse response structure: { data: { details: { ... } } }
|
|
114
|
+
if isinstance(details, dict):
|
|
115
|
+
if "data" in details and isinstance(details["data"], dict):
|
|
116
|
+
details = details["data"]
|
|
117
|
+
|
|
118
|
+
# Capture source
|
|
119
|
+
source = details.get("source")
|
|
120
|
+
|
|
121
|
+
if "details" in details and isinstance(details["details"], dict):
|
|
122
|
+
details = details["details"]
|
|
123
|
+
# Restore source if captured
|
|
124
|
+
if source:
|
|
125
|
+
details["source"] = source
|
|
126
|
+
|
|
127
|
+
if not details:
|
|
128
|
+
console.print(f"[red]{i18n.get('details.not_found')}[/red]")
|
|
129
|
+
time.sleep(1)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
desc = details.get("description") or details.get("synopsis") or details.get("desc")
|
|
133
|
+
if desc:
|
|
134
|
+
console.print(f"\n[dim]{desc[:300]}...[/dim]\n", justify="left")
|
|
135
|
+
|
|
136
|
+
opt_watch = i18n.get("details.watch")
|
|
137
|
+
opt_dl = i18n.get("details.download")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
action = questionary.select(
|
|
141
|
+
i18n.get("details.action_prompt"),
|
|
142
|
+
choices=[opt_watch, opt_dl],
|
|
143
|
+
pointer=">",
|
|
144
|
+
use_shortcuts=False
|
|
145
|
+
).ask()
|
|
146
|
+
|
|
147
|
+
if action is None:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if action == opt_dl:
|
|
151
|
+
handle_download_flow(slug, details)
|
|
152
|
+
elif action == opt_watch:
|
|
153
|
+
handle_watch_flow(slug, details)
|
|
154
|
+
|
|
155
|
+
except KeyboardInterrupt:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
def get_episodes_safe(details):
|
|
159
|
+
episodes = None
|
|
160
|
+
for k in ["episodes", "episodes_list", "episode_list", "results", "chapters"]:
|
|
161
|
+
if k in details and isinstance(details[k], list):
|
|
162
|
+
episodes = details[k]
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
if not episodes:
|
|
166
|
+
for v in details.values():
|
|
167
|
+
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]):
|
|
168
|
+
episodes = v
|
|
169
|
+
break
|
|
170
|
+
return episodes
|
|
171
|
+
|
|
172
|
+
def handle_watch_flow(slug, details):
|
|
173
|
+
episodes = get_episodes_safe(details)
|
|
174
|
+
if not episodes:
|
|
175
|
+
console.print(f"[yellow]{i18n.get('details.no_episodes')}[/yellow]")
|
|
176
|
+
time.sleep(1.5)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
prog_data = progress_tracker.get_anime_progress(slug)
|
|
180
|
+
completed_ids = set(prog_data.get("completed", []))
|
|
181
|
+
last_watched = prog_data.get("last_watched", 0)
|
|
182
|
+
next_ep_num = last_watched + 1
|
|
183
|
+
|
|
184
|
+
while True:
|
|
185
|
+
ep_choices = []
|
|
186
|
+
for ep in episodes:
|
|
187
|
+
num_val = ep.get('number') or ep.get('ep_num')
|
|
188
|
+
try:
|
|
189
|
+
num = int(num_val)
|
|
190
|
+
except:
|
|
191
|
+
num = -1
|
|
192
|
+
|
|
193
|
+
prefix = " "
|
|
194
|
+
if num in completed_ids:
|
|
195
|
+
prefix = "✓ "
|
|
196
|
+
elif num == next_ep_num:
|
|
197
|
+
prefix = "● "
|
|
198
|
+
|
|
199
|
+
name = f"{prefix}{i18n.get('details.episode')} {num_val}"
|
|
200
|
+
ep_choices.append(questionary.Choice(name, value=ep))
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
selected_ep = questionary.select(
|
|
204
|
+
i18n.get("details.select_episode") + ":",
|
|
205
|
+
choices=ep_choices,
|
|
206
|
+
pointer=">",
|
|
207
|
+
use_shortcuts=False
|
|
208
|
+
).ask()
|
|
209
|
+
|
|
210
|
+
if selected_ep is None:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
ep_id = selected_ep.get("id")
|
|
214
|
+
ep_num = selected_ep.get("number")
|
|
215
|
+
|
|
216
|
+
if not ep_id:
|
|
217
|
+
console.print(f"[red]{i18n.get('details.invalid_ep_id')}[/red]")
|
|
218
|
+
time.sleep(1)
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
with console.status(i18n.get("common.processing"), spinner="dots"):
|
|
222
|
+
stream_resp = get_streams(slug, ep_id)
|
|
223
|
+
stream_url = None
|
|
224
|
+
if stream_resp and isinstance(stream_resp, dict):
|
|
225
|
+
data_node = stream_resp
|
|
226
|
+
for _ in range(3):
|
|
227
|
+
if "data" in data_node and isinstance(data_node["data"], (dict, list)):
|
|
228
|
+
data_node = data_node["data"]
|
|
229
|
+
else:
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
sources = None
|
|
233
|
+
if isinstance(data_node, list):
|
|
234
|
+
sources = data_node
|
|
235
|
+
elif isinstance(data_node, dict):
|
|
236
|
+
sources = data_node.get("links") or data_node.get("sources")
|
|
237
|
+
|
|
238
|
+
if sources and isinstance(sources, list):
|
|
239
|
+
first = sources[0]
|
|
240
|
+
if isinstance(first, dict):
|
|
241
|
+
stream_url = first.get("url")
|
|
242
|
+
elif isinstance(data_node, dict) and "url" in data_node:
|
|
243
|
+
stream_url = data_node["url"]
|
|
244
|
+
|
|
245
|
+
if not stream_url:
|
|
246
|
+
console.print(f"[red]{i18n.get('details.stream_not_found')}[/red]")
|
|
247
|
+
time.sleep(1.5)
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
console.print(f"[green]{i18n.get('details.player_starting')}[/green]")
|
|
251
|
+
title = f"{details.get('title', 'Anime')} - Ep {ep_num}"
|
|
252
|
+
|
|
253
|
+
headers = {}
|
|
254
|
+
if details.get("source") == "hianime":
|
|
255
|
+
headers["Referer"] = "https://hianime.to"
|
|
256
|
+
|
|
257
|
+
success = player.play(stream_url, title=title, headers=headers)
|
|
258
|
+
|
|
259
|
+
if success:
|
|
260
|
+
try:
|
|
261
|
+
ans = questionary.confirm(i18n.get("details.mark_watched")).ask()
|
|
262
|
+
if ans:
|
|
263
|
+
n = int(ep_num)
|
|
264
|
+
progress_tracker.mark_watched(slug, n)
|
|
265
|
+
|
|
266
|
+
completed_ids.add(n)
|
|
267
|
+
if n >= next_ep_num:
|
|
268
|
+
next_ep_num = n + 1
|
|
269
|
+
except:
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
except KeyboardInterrupt:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
def handle_download_flow(slug, details):
|
|
276
|
+
episodes = get_episodes_safe(details)
|
|
277
|
+
if not episodes:
|
|
278
|
+
console.print(f"[yellow]{i18n.get('details.no_episodes')}[/yellow]")
|
|
279
|
+
time.sleep(1.5)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
opt_all = i18n.get("details.download_options.all")
|
|
283
|
+
opt_manual = i18n.get("details.download_options.manual")
|
|
284
|
+
opt_range = i18n.get("details.download_options.range")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
mode = questionary.select(
|
|
288
|
+
i18n.get("details.download_options.prompt"),
|
|
289
|
+
choices=[opt_all, opt_manual, opt_range],
|
|
290
|
+
pointer=">",
|
|
291
|
+
use_shortcuts=False
|
|
292
|
+
).ask()
|
|
293
|
+
|
|
294
|
+
if mode is None:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
selected_eps = []
|
|
298
|
+
|
|
299
|
+
if mode == opt_all:
|
|
300
|
+
selected_eps = episodes
|
|
301
|
+
|
|
302
|
+
elif mode == opt_manual:
|
|
303
|
+
choices = []
|
|
304
|
+
for ep in episodes:
|
|
305
|
+
name = f"{i18n.get('details.episode')} {ep.get('number')}"
|
|
306
|
+
choices.append(questionary.Choice(name, value=ep))
|
|
307
|
+
|
|
308
|
+
selected_eps = questionary.checkbox(
|
|
309
|
+
"Select Episodes:",
|
|
310
|
+
choices=choices
|
|
311
|
+
).ask()
|
|
312
|
+
|
|
313
|
+
elif mode == opt_range:
|
|
314
|
+
r_str = questionary.text(i18n.get("details.download_options.range_input")).ask()
|
|
315
|
+
if not r_str: return
|
|
316
|
+
nums = set()
|
|
317
|
+
try:
|
|
318
|
+
parts = r_str.split(',')
|
|
319
|
+
for p in parts:
|
|
320
|
+
p = p.strip()
|
|
321
|
+
if '-' in p:
|
|
322
|
+
s, e = p.split('-')
|
|
323
|
+
for x in range(int(s), int(e)+1): nums.add(x)
|
|
324
|
+
elif p.isdigit():
|
|
325
|
+
nums.add(int(p))
|
|
326
|
+
except:
|
|
327
|
+
console.print(f"[red]{i18n.get('details.download_options.range_error')}[/red]")
|
|
328
|
+
time.sleep(1)
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
selected_eps = [ep for ep in episodes if int(ep.get('number', -1)) in nums]
|
|
332
|
+
|
|
333
|
+
if not selected_eps:
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
console.print(f"[green]{i18n.get('details.download_options.started')}[/green]")
|
|
337
|
+
console.print(f"[dim]{i18n.t('details.queueing', count=len(selected_eps))}[/dim]")
|
|
338
|
+
|
|
339
|
+
anime_title = details.get("title") or "Unknown Anime"
|
|
340
|
+
queue_manager.add_to_queue(anime_title, selected_eps, slug)
|
|
341
|
+
|
|
342
|
+
time.sleep(1.5)
|
|
343
|
+
|
|
344
|
+
except KeyboardInterrupt:
|
|
345
|
+
return
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import questionary
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from weeb_cli.i18n import i18n
|
|
4
|
+
from weeb_cli.config import config
|
|
5
|
+
import time
|
|
6
|
+
from weeb_cli.ui.header import show_header
|
|
7
|
+
import os
|
|
8
|
+
from weeb_cli.services.dependency_manager import dependency_manager
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def toggle_config(key, name):
|
|
14
|
+
current = config.get(key)
|
|
15
|
+
new_val = not current
|
|
16
|
+
|
|
17
|
+
if new_val:
|
|
18
|
+
dep_name = name.lower()
|
|
19
|
+
if "aria2" in dep_name: dep_name = "aria2"
|
|
20
|
+
elif "yt-dlp" in dep_name: dep_name = "yt-dlp"
|
|
21
|
+
|
|
22
|
+
path = dependency_manager.check_dependency(dep_name)
|
|
23
|
+
if not path:
|
|
24
|
+
console.print(f"[cyan]{i18n.t('setup.downloading', tool=name)}...[/cyan]")
|
|
25
|
+
if not dependency_manager.install_dependency(dep_name):
|
|
26
|
+
console.print(f"[red]{i18n.t('setup.failed', tool=name)}[/red]")
|
|
27
|
+
time.sleep(1)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
config.set(key, new_val)
|
|
31
|
+
|
|
32
|
+
msg_key = "settings.toggle_on" if new_val else "settings.toggle_off"
|
|
33
|
+
console.print(f"[green]{i18n.t(msg_key, tool=name)}[/green]")
|
|
34
|
+
time.sleep(0.5)
|
|
35
|
+
|
|
36
|
+
def open_settings():
|
|
37
|
+
while True:
|
|
38
|
+
console.clear()
|
|
39
|
+
show_header(i18n.get("settings.title"))
|
|
40
|
+
|
|
41
|
+
lang = config.get("language")
|
|
42
|
+
source = config.get("scraping_source", "local")
|
|
43
|
+
display_source = "weeb" if source == "local" else source
|
|
44
|
+
|
|
45
|
+
aria2_state = i18n.get("common.enabled") if config.get("aria2_enabled") else i18n.get("common.disabled")
|
|
46
|
+
ytdlp_state = i18n.get("common.enabled") if config.get("ytdlp_enabled") else i18n.get("common.disabled")
|
|
47
|
+
|
|
48
|
+
opt_lang = i18n.get("settings.language")
|
|
49
|
+
opt_source = f"{i18n.get('settings.source')} [{display_source}]"
|
|
50
|
+
opt_download = i18n.get("settings.download_settings")
|
|
51
|
+
opt_aria2 = f"{i18n.get('settings.aria2')} [{aria2_state}]"
|
|
52
|
+
opt_ytdlp = f"{i18n.get('settings.ytdlp')} [{ytdlp_state}]"
|
|
53
|
+
|
|
54
|
+
opt_aria2_conf = f" ↳ {i18n.get('settings.aria2_config')}"
|
|
55
|
+
opt_ytdlp_conf = f" ↳ {i18n.get('settings.ytdlp_config')}"
|
|
56
|
+
|
|
57
|
+
choices = [opt_lang, opt_source, opt_download, opt_aria2]
|
|
58
|
+
if config.get("aria2_enabled"):
|
|
59
|
+
choices.append(opt_aria2_conf)
|
|
60
|
+
|
|
61
|
+
choices.append(opt_ytdlp)
|
|
62
|
+
if config.get("ytdlp_enabled"):
|
|
63
|
+
choices.append(opt_ytdlp_conf)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
answer = questionary.select(
|
|
67
|
+
i18n.get("settings.title"),
|
|
68
|
+
choices=choices,
|
|
69
|
+
pointer=">",
|
|
70
|
+
use_shortcuts=False,
|
|
71
|
+
style=questionary.Style([
|
|
72
|
+
('pointer', 'fg:cyan bold'),
|
|
73
|
+
('highlighted', 'fg:cyan'),
|
|
74
|
+
('selected', 'fg:cyan bold'),
|
|
75
|
+
])
|
|
76
|
+
).ask()
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if answer == opt_lang:
|
|
81
|
+
change_language()
|
|
82
|
+
elif answer == opt_source:
|
|
83
|
+
change_source()
|
|
84
|
+
elif answer == opt_download:
|
|
85
|
+
download_settings_menu()
|
|
86
|
+
elif answer == opt_aria2:
|
|
87
|
+
toggle_config("aria2_enabled", "Aria2")
|
|
88
|
+
elif answer == opt_aria2_conf:
|
|
89
|
+
aria2_settings_menu()
|
|
90
|
+
elif answer == opt_ytdlp:
|
|
91
|
+
toggle_config("ytdlp_enabled", "yt-dlp")
|
|
92
|
+
elif answer == opt_ytdlp_conf:
|
|
93
|
+
ytdlp_settings_menu()
|
|
94
|
+
elif answer is None:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
def change_language():
|
|
98
|
+
langs = {"Türkçe": "tr", "English": "en"}
|
|
99
|
+
try:
|
|
100
|
+
selected = questionary.select(
|
|
101
|
+
"Select Language / Dil Seçiniz:",
|
|
102
|
+
choices=list(langs.keys()),
|
|
103
|
+
pointer=">",
|
|
104
|
+
use_shortcuts=False
|
|
105
|
+
).ask()
|
|
106
|
+
|
|
107
|
+
if selected:
|
|
108
|
+
lang_code = langs[selected]
|
|
109
|
+
i18n.set_language(lang_code)
|
|
110
|
+
|
|
111
|
+
new_source = "local" if lang_code == "tr" else "hianime"
|
|
112
|
+
config.set("scraping_source", new_source)
|
|
113
|
+
|
|
114
|
+
console.print(f"[green]{i18n.get('settings.language_changed')}[/green]")
|
|
115
|
+
time.sleep(1)
|
|
116
|
+
except KeyboardInterrupt:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def change_source():
|
|
120
|
+
current_lang = config.get("language")
|
|
121
|
+
|
|
122
|
+
sources = []
|
|
123
|
+
if current_lang == "tr":
|
|
124
|
+
sources = ["weeb", "animecix", "turkanime", "anizle"]
|
|
125
|
+
else:
|
|
126
|
+
sources = ["hianime", "allanime"]
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
selected = questionary.select(
|
|
130
|
+
i18n.get("settings.source"),
|
|
131
|
+
choices=sources,
|
|
132
|
+
pointer=">",
|
|
133
|
+
use_shortcuts=False
|
|
134
|
+
).ask()
|
|
135
|
+
|
|
136
|
+
if selected:
|
|
137
|
+
save_val = "local" if selected == "Weeb" else selected
|
|
138
|
+
config.set("scraping_source", save_val)
|
|
139
|
+
console.print(f"[green]{i18n.t('settings.source_changed', source=selected)}[/green]")
|
|
140
|
+
time.sleep(1)
|
|
141
|
+
except KeyboardInterrupt:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def aria2_settings_menu():
|
|
147
|
+
while True:
|
|
148
|
+
console.clear()
|
|
149
|
+
show_header(i18n.get("settings.aria2_config"))
|
|
150
|
+
|
|
151
|
+
curr_conn = config.get("aria2_max_connections", 16)
|
|
152
|
+
|
|
153
|
+
opt_conn = f"{i18n.get('settings.max_conn')} [{curr_conn}]"
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
sel = questionary.select(
|
|
157
|
+
i18n.get("settings.aria2_config"),
|
|
158
|
+
choices=[opt_conn],
|
|
159
|
+
pointer=">",
|
|
160
|
+
use_shortcuts=False
|
|
161
|
+
).ask()
|
|
162
|
+
|
|
163
|
+
if sel == opt_conn:
|
|
164
|
+
val = questionary.text(f"{i18n.get('settings.enter_conn')}:", default=str(curr_conn)).ask()
|
|
165
|
+
if val and val.isdigit():
|
|
166
|
+
config.set("aria2_max_connections", int(val))
|
|
167
|
+
elif sel is None:
|
|
168
|
+
return
|
|
169
|
+
except KeyboardInterrupt:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
def download_settings_menu():
|
|
173
|
+
while True:
|
|
174
|
+
console.clear()
|
|
175
|
+
show_header(i18n.get("settings.download_settings"))
|
|
176
|
+
|
|
177
|
+
curr_dir = config.get("download_dir")
|
|
178
|
+
console.print(f"[dim]Current: {curr_dir}[/dim]\n", justify="left")
|
|
179
|
+
|
|
180
|
+
curr_concurrent = config.get("max_concurrent_downloads", 3)
|
|
181
|
+
|
|
182
|
+
opt_name = i18n.get("settings.change_folder_name")
|
|
183
|
+
opt_path = i18n.get("settings.change_full_path")
|
|
184
|
+
opt_concurrent = f"{i18n.get('settings.concurrent_downloads')} [{curr_concurrent}]"
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
sel = questionary.select(
|
|
188
|
+
i18n.get("settings.download_settings"),
|
|
189
|
+
choices=[opt_name, opt_path, opt_concurrent],
|
|
190
|
+
pointer=">",
|
|
191
|
+
use_shortcuts=False
|
|
192
|
+
).ask()
|
|
193
|
+
|
|
194
|
+
if sel == opt_name:
|
|
195
|
+
val = questionary.text("Folder Name:", default="weeb-downloads").ask()
|
|
196
|
+
if val:
|
|
197
|
+
new_path = os.path.join(os.getcwd(), val)
|
|
198
|
+
config.set("download_dir", new_path)
|
|
199
|
+
elif sel == opt_path:
|
|
200
|
+
val = questionary.text("Full Path:", default=curr_dir).ask()
|
|
201
|
+
if val:
|
|
202
|
+
config.set("download_dir", val)
|
|
203
|
+
elif sel == opt_concurrent:
|
|
204
|
+
val = questionary.text(i18n.get("settings.enter_concurrent"), default=str(curr_concurrent)).ask()
|
|
205
|
+
if val and val.isdigit():
|
|
206
|
+
n = int(val)
|
|
207
|
+
if 1 <= n <= 5:
|
|
208
|
+
config.set("max_concurrent_downloads", n)
|
|
209
|
+
elif sel is None:
|
|
210
|
+
return
|
|
211
|
+
except KeyboardInterrupt:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def ytdlp_settings_menu():
|
|
216
|
+
while True:
|
|
217
|
+
console.clear()
|
|
218
|
+
show_header(i18n.get("settings.ytdlp_config"))
|
|
219
|
+
|
|
220
|
+
curr_fmt = config.get("ytdlp_format", "best")
|
|
221
|
+
opt_fmt = f"{i18n.get('settings.format')} [{curr_fmt}]"
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
sel = questionary.select(
|
|
225
|
+
i18n.get("settings.ytdlp_config"),
|
|
226
|
+
choices=[opt_fmt],
|
|
227
|
+
pointer=">",
|
|
228
|
+
use_shortcuts=False
|
|
229
|
+
).ask()
|
|
230
|
+
|
|
231
|
+
if sel == opt_fmt:
|
|
232
|
+
val = questionary.text(f"{i18n.get('settings.enter_format')}:", default=curr_fmt).ask()
|
|
233
|
+
if val:
|
|
234
|
+
config.set("ytdlp_format", val)
|
|
235
|
+
elif sel is None:
|
|
236
|
+
return
|
|
237
|
+
except KeyboardInterrupt:
|
|
238
|
+
return
|