weeb-cli 1.0.0__py3-none-any.whl → 2.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 -1
- weeb_cli/commands/downloads.py +339 -8
- weeb_cli/commands/settings.py +127 -1
- weeb_cli/config.py +17 -27
- weeb_cli/locales/en.json +31 -3
- weeb_cli/locales/tr.json +31 -3
- weeb_cli/providers/__init__.py +2 -0
- weeb_cli/providers/allanime.py +252 -0
- weeb_cli/providers/extractors/__init__.py +0 -0
- weeb_cli/providers/extractors/megacloud.py +238 -0
- weeb_cli/providers/hianime.py +294 -0
- weeb_cli/services/database.py +313 -0
- weeb_cli/services/downloader.py +60 -49
- weeb_cli/services/local_library.py +263 -0
- weeb_cli/services/logger.py +55 -0
- weeb_cli/services/notifier.py +41 -0
- weeb_cli/services/progress.py +27 -77
- weeb_cli/ui/header.py +1 -1
- weeb_cli/ui/menu.py +2 -2
- weeb_cli-2.0.0.dist-info/METADATA +199 -0
- weeb_cli-2.0.0.dist-info/RECORD +46 -0
- weeb_cli-1.0.0.dist-info/METADATA +0 -148
- weeb_cli-1.0.0.dist-info/RECORD +0 -38
- {weeb_cli-1.0.0.dist-info → weeb_cli-2.0.0.dist-info}/WHEEL +0 -0
- {weeb_cli-1.0.0.dist-info → weeb_cli-2.0.0.dist-info}/entry_points.txt +0 -0
- {weeb_cli-1.0.0.dist-info → weeb_cli-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {weeb_cli-1.0.0.dist-info → weeb_cli-2.0.0.dist-info}/top_level.txt +0 -0
weeb_cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "2.0.0"
|
weeb_cli/commands/downloads.py
CHANGED
|
@@ -1,29 +1,79 @@
|
|
|
1
1
|
import questionary
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
from rich.console import Console
|
|
3
4
|
from rich.live import Live
|
|
4
5
|
from rich.table import Table
|
|
5
6
|
from weeb_cli.services.downloader import queue_manager
|
|
7
|
+
from weeb_cli.services.local_library import local_library
|
|
8
|
+
from weeb_cli.services.player import player
|
|
9
|
+
from weeb_cli.services.progress import progress_tracker
|
|
6
10
|
from weeb_cli.i18n import i18n
|
|
7
11
|
from weeb_cli.ui.header import show_header
|
|
8
12
|
import time
|
|
9
13
|
|
|
10
14
|
console = Console()
|
|
11
15
|
|
|
16
|
+
AUTOCOMPLETE_STYLE = questionary.Style([
|
|
17
|
+
('qmark', 'fg:cyan'),
|
|
18
|
+
('question', 'fg:white bold'),
|
|
19
|
+
('answer', 'fg:cyan bold'),
|
|
20
|
+
('pointer', 'fg:cyan bold'),
|
|
21
|
+
('highlighted', 'fg:white bg:ansiblack bold'),
|
|
22
|
+
('selected', 'fg:white'),
|
|
23
|
+
])
|
|
24
|
+
|
|
12
25
|
def show_downloads():
|
|
26
|
+
local_library.smart_index_all()
|
|
27
|
+
|
|
13
28
|
while True:
|
|
14
29
|
console.clear()
|
|
15
30
|
show_header(i18n.get("downloads.title"))
|
|
16
31
|
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
sources = local_library.get_all_sources()
|
|
33
|
+
active_queue = [i for i in queue_manager.queue if i["status"] in ["pending", "processing"]]
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
choices = []
|
|
36
|
+
|
|
37
|
+
indexed_count = len(local_library.get_indexed_anime())
|
|
38
|
+
if indexed_count > 0:
|
|
39
|
+
choices.append(questionary.Choice(
|
|
40
|
+
f"⌕ {i18n.get('downloads.search_all')} ({indexed_count} anime)",
|
|
41
|
+
value={"type": "search_all"}
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
for source in sources:
|
|
45
|
+
if source["available"]:
|
|
46
|
+
library = local_library.scan_library(source["path"])
|
|
47
|
+
count = len(library)
|
|
48
|
+
if count > 0:
|
|
49
|
+
choices.append(questionary.Choice(
|
|
50
|
+
f"● {source['name']} ({count} anime)",
|
|
51
|
+
value={"type": "source", "data": source}
|
|
52
|
+
))
|
|
53
|
+
else:
|
|
54
|
+
indexed = [a for a in local_library.get_indexed_anime() if a["source_path"] == source["path"]]
|
|
55
|
+
count = len(indexed)
|
|
56
|
+
if count > 0:
|
|
57
|
+
choices.append(questionary.Choice(
|
|
58
|
+
f"○ {source['name']} ({count} anime) - {i18n.get('downloads.offline')}",
|
|
59
|
+
value={"type": "offline", "data": source}
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
if active_queue:
|
|
63
|
+
is_running = queue_manager.is_running()
|
|
21
64
|
status = i18n.get("downloads.queue_running") if is_running else i18n.get("downloads.queue_stopped")
|
|
22
|
-
|
|
65
|
+
choices.append(questionary.Choice(
|
|
66
|
+
f"{i18n.get('downloads.active_downloads')} ({len(active_queue)}) - {status}",
|
|
67
|
+
value={"type": "active"}
|
|
68
|
+
))
|
|
23
69
|
|
|
24
|
-
|
|
70
|
+
if queue_manager.queue:
|
|
71
|
+
choices.append(questionary.Choice(
|
|
72
|
+
i18n.get("downloads.manage_queue"),
|
|
73
|
+
value={"type": "manage"}
|
|
74
|
+
))
|
|
25
75
|
|
|
26
|
-
if not
|
|
76
|
+
if not choices:
|
|
27
77
|
console.print(f"[dim]{i18n.get('downloads.empty')}[/dim]")
|
|
28
78
|
try:
|
|
29
79
|
input(i18n.get("common.continue_key"))
|
|
@@ -31,6 +81,288 @@ def show_downloads():
|
|
|
31
81
|
pass
|
|
32
82
|
return
|
|
33
83
|
|
|
84
|
+
try:
|
|
85
|
+
action = questionary.select(
|
|
86
|
+
i18n.get("downloads.action_prompt"),
|
|
87
|
+
choices=choices,
|
|
88
|
+
pointer=">",
|
|
89
|
+
use_shortcuts=False
|
|
90
|
+
).ask()
|
|
91
|
+
|
|
92
|
+
if action is None:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if action["type"] == "search_all":
|
|
96
|
+
search_all_sources()
|
|
97
|
+
elif action["type"] == "source":
|
|
98
|
+
library = local_library.scan_library(action["data"]["path"])
|
|
99
|
+
show_completed_library(library, action["data"]["name"])
|
|
100
|
+
elif action["type"] == "offline":
|
|
101
|
+
show_offline_library(action["data"])
|
|
102
|
+
elif action["type"] == "active":
|
|
103
|
+
show_queue_live()
|
|
104
|
+
elif action["type"] == "manage":
|
|
105
|
+
manage_queue()
|
|
106
|
+
|
|
107
|
+
except KeyboardInterrupt:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
def fuzzy_match(query: str, text: str) -> float:
|
|
111
|
+
from difflib import SequenceMatcher
|
|
112
|
+
query = query.lower()
|
|
113
|
+
text = text.lower()
|
|
114
|
+
|
|
115
|
+
if query == text:
|
|
116
|
+
return 1.0
|
|
117
|
+
if query in text:
|
|
118
|
+
return 0.9
|
|
119
|
+
|
|
120
|
+
return SequenceMatcher(None, query, text).ratio()
|
|
121
|
+
|
|
122
|
+
def search_all_sources():
|
|
123
|
+
while True:
|
|
124
|
+
console.clear()
|
|
125
|
+
show_header(i18n.get("downloads.search_all"))
|
|
126
|
+
|
|
127
|
+
all_indexed = local_library.get_indexed_anime()
|
|
128
|
+
|
|
129
|
+
anime_map = {}
|
|
130
|
+
for anime in all_indexed:
|
|
131
|
+
progress = local_library.get_anime_progress(anime["title"])
|
|
132
|
+
watched = len(progress.get("completed", []))
|
|
133
|
+
total = anime["episode_count"]
|
|
134
|
+
available = local_library.is_source_available(anime["source_path"])
|
|
135
|
+
|
|
136
|
+
status_icon = "●" if available else "○"
|
|
137
|
+
|
|
138
|
+
if watched >= total and total > 0:
|
|
139
|
+
watch_status = " [✓]"
|
|
140
|
+
elif watched > 0:
|
|
141
|
+
watch_status = f" [{watched}/{total}]"
|
|
142
|
+
else:
|
|
143
|
+
watch_status = ""
|
|
144
|
+
|
|
145
|
+
label = f"{status_icon} {anime['title']} [{anime['source_name']}]{watch_status}"
|
|
146
|
+
anime_map[label] = anime
|
|
147
|
+
|
|
148
|
+
all_choices = list(anime_map.keys())
|
|
149
|
+
|
|
150
|
+
if not all_choices:
|
|
151
|
+
console.print(f"[dim]{i18n.get('downloads.no_indexed')}[/dim]")
|
|
152
|
+
time.sleep(1.5)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
selected_label = questionary.autocomplete(
|
|
157
|
+
i18n.get("downloads.search_anime"),
|
|
158
|
+
choices=all_choices,
|
|
159
|
+
match_middle=True,
|
|
160
|
+
style=AUTOCOMPLETE_STYLE,
|
|
161
|
+
).ask()
|
|
162
|
+
|
|
163
|
+
if selected_label is None:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if selected_label in anime_map:
|
|
167
|
+
anime_info = anime_map[selected_label]
|
|
168
|
+
|
|
169
|
+
if not local_library.is_source_available(anime_info["source_path"]):
|
|
170
|
+
console.print(f"[yellow]{i18n.t('downloads.connect_drive', name=anime_info['source_name'])}[/yellow]")
|
|
171
|
+
time.sleep(1.5)
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
anime_data = {
|
|
175
|
+
"title": anime_info["title"],
|
|
176
|
+
"path": anime_info["folder_path"],
|
|
177
|
+
"episode_count": anime_info["episode_count"],
|
|
178
|
+
"episodes": local_library._scan_anime_folder(Path(anime_info["folder_path"]))
|
|
179
|
+
}
|
|
180
|
+
show_anime_episodes(anime_data)
|
|
181
|
+
|
|
182
|
+
except KeyboardInterrupt:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
def show_offline_library(source):
|
|
186
|
+
while True:
|
|
187
|
+
console.clear()
|
|
188
|
+
show_header(f"{source['name']} ({i18n.get('downloads.offline')})")
|
|
189
|
+
|
|
190
|
+
indexed = [a for a in local_library.get_indexed_anime() if a["source_path"] == source["path"]]
|
|
191
|
+
|
|
192
|
+
if not indexed:
|
|
193
|
+
console.print(f"[dim]{i18n.get('downloads.no_indexed')}[/dim]")
|
|
194
|
+
time.sleep(1.5)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
ep_short = i18n.get("downloads.episode_short")
|
|
198
|
+
anime_map = {}
|
|
199
|
+
for anime in indexed:
|
|
200
|
+
progress = local_library.get_anime_progress(anime["title"])
|
|
201
|
+
watched = len(progress.get("completed", []))
|
|
202
|
+
total = anime["episode_count"]
|
|
203
|
+
|
|
204
|
+
if watched >= total and total > 0:
|
|
205
|
+
status = " [✓]"
|
|
206
|
+
elif watched > 0:
|
|
207
|
+
status = f" [{watched}/{total}]"
|
|
208
|
+
else:
|
|
209
|
+
status = ""
|
|
210
|
+
|
|
211
|
+
label = f"{anime['title']} [{total} {ep_short}]{status}"
|
|
212
|
+
anime_map[label] = anime
|
|
213
|
+
|
|
214
|
+
all_choices = list(anime_map.keys())
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
selected_label = questionary.autocomplete(
|
|
218
|
+
i18n.get("downloads.search_anime"),
|
|
219
|
+
choices=all_choices,
|
|
220
|
+
match_middle=True,
|
|
221
|
+
style=AUTOCOMPLETE_STYLE,
|
|
222
|
+
).ask()
|
|
223
|
+
|
|
224
|
+
if selected_label is None:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if selected_label in anime_map:
|
|
228
|
+
console.print(f"[yellow]{i18n.t('downloads.connect_drive', name=source['name'])}[/yellow]")
|
|
229
|
+
time.sleep(1.5)
|
|
230
|
+
|
|
231
|
+
except KeyboardInterrupt:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
def show_completed_library(library, source_name=None):
|
|
235
|
+
while True:
|
|
236
|
+
console.clear()
|
|
237
|
+
title = source_name or i18n.get("downloads.completed_downloads")
|
|
238
|
+
show_header(title)
|
|
239
|
+
|
|
240
|
+
ep_short = i18n.get("downloads.episode_short")
|
|
241
|
+
anime_map = {}
|
|
242
|
+
for anime in library:
|
|
243
|
+
progress = local_library.get_anime_progress(anime["title"])
|
|
244
|
+
watched = len(progress.get("completed", []))
|
|
245
|
+
total = anime["episode_count"]
|
|
246
|
+
|
|
247
|
+
if watched >= total and total > 0:
|
|
248
|
+
status = " [✓]"
|
|
249
|
+
elif watched > 0:
|
|
250
|
+
status = f" [{watched}/{total}]"
|
|
251
|
+
else:
|
|
252
|
+
status = ""
|
|
253
|
+
|
|
254
|
+
label = f"{anime['title']} [{total} {ep_short}]{status}"
|
|
255
|
+
anime_map[label] = anime
|
|
256
|
+
|
|
257
|
+
all_choices = list(anime_map.keys())
|
|
258
|
+
|
|
259
|
+
choices = [
|
|
260
|
+
questionary.Choice(f"⌕ {i18n.get('downloads.search_anime')}", value="search"),
|
|
261
|
+
]
|
|
262
|
+
for label in all_choices:
|
|
263
|
+
choices.append(questionary.Choice(label, value=label))
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
selected = questionary.select(
|
|
267
|
+
i18n.get("downloads.action_prompt"),
|
|
268
|
+
choices=choices,
|
|
269
|
+
pointer=">",
|
|
270
|
+
use_shortcuts=False,
|
|
271
|
+
).ask()
|
|
272
|
+
|
|
273
|
+
if selected is None:
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
if selected == "search":
|
|
277
|
+
search_result = questionary.autocomplete(
|
|
278
|
+
i18n.get("downloads.search_anime"),
|
|
279
|
+
choices=all_choices,
|
|
280
|
+
match_middle=True,
|
|
281
|
+
style=AUTOCOMPLETE_STYLE,
|
|
282
|
+
).ask()
|
|
283
|
+
|
|
284
|
+
if search_result and search_result in anime_map:
|
|
285
|
+
show_anime_episodes(anime_map[search_result])
|
|
286
|
+
elif selected in anime_map:
|
|
287
|
+
show_anime_episodes(anime_map[selected])
|
|
288
|
+
|
|
289
|
+
except KeyboardInterrupt:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
def show_anime_episodes(anime):
|
|
293
|
+
while True:
|
|
294
|
+
console.clear()
|
|
295
|
+
show_header(anime["title"])
|
|
296
|
+
|
|
297
|
+
progress = local_library.get_anime_progress(anime["title"])
|
|
298
|
+
completed_eps = set(progress.get("completed", []))
|
|
299
|
+
last_watched = progress.get("last_watched", 0)
|
|
300
|
+
next_ep = last_watched + 1
|
|
301
|
+
|
|
302
|
+
episodes = anime["episodes"]
|
|
303
|
+
|
|
304
|
+
choices = []
|
|
305
|
+
for ep in episodes:
|
|
306
|
+
num = ep["number"]
|
|
307
|
+
size = local_library.format_size(ep["size"])
|
|
308
|
+
|
|
309
|
+
prefix = " "
|
|
310
|
+
if num in completed_eps:
|
|
311
|
+
prefix = "✓ "
|
|
312
|
+
elif num == next_ep:
|
|
313
|
+
prefix = "● "
|
|
314
|
+
|
|
315
|
+
choices.append(questionary.Choice(
|
|
316
|
+
f"{prefix}{i18n.get('details.episode')} {num} ({size})",
|
|
317
|
+
value=ep
|
|
318
|
+
))
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
selected = questionary.select(
|
|
322
|
+
i18n.get("details.select_episode"),
|
|
323
|
+
choices=choices,
|
|
324
|
+
pointer=">",
|
|
325
|
+
use_shortcuts=False
|
|
326
|
+
).ask()
|
|
327
|
+
|
|
328
|
+
if selected is None:
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
play_local_episode(anime, selected)
|
|
332
|
+
|
|
333
|
+
except KeyboardInterrupt:
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
def play_local_episode(anime, episode):
|
|
337
|
+
console.print(f"[green]{i18n.get('details.player_starting')}[/green]")
|
|
338
|
+
|
|
339
|
+
title = f"{anime['title']} - {i18n.get('details.episode')} {episode['number']}"
|
|
340
|
+
success = player.play(episode["path"], title=title)
|
|
341
|
+
|
|
342
|
+
if success:
|
|
343
|
+
try:
|
|
344
|
+
ans = questionary.confirm(i18n.get("details.mark_watched")).ask()
|
|
345
|
+
if ans:
|
|
346
|
+
local_library.mark_episode_watched(
|
|
347
|
+
anime["title"],
|
|
348
|
+
episode["number"],
|
|
349
|
+
anime["episode_count"]
|
|
350
|
+
)
|
|
351
|
+
except:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
def manage_queue():
|
|
355
|
+
while True:
|
|
356
|
+
console.clear()
|
|
357
|
+
show_header(i18n.get("downloads.manage_queue"))
|
|
358
|
+
|
|
359
|
+
pending = queue_manager.get_pending_count()
|
|
360
|
+
is_running = queue_manager.is_running()
|
|
361
|
+
|
|
362
|
+
if pending > 0:
|
|
363
|
+
status = i18n.get("downloads.queue_running") if is_running else i18n.get("downloads.queue_stopped")
|
|
364
|
+
console.print(f"[cyan]{i18n.t('downloads.pending_count', count=pending)}[/cyan] - {status}\n")
|
|
365
|
+
|
|
34
366
|
opt_view = i18n.get("downloads.view_queue")
|
|
35
367
|
opt_start = i18n.get("downloads.start_queue")
|
|
36
368
|
opt_stop = i18n.get("downloads.stop_queue")
|
|
@@ -66,8 +398,7 @@ def show_downloads():
|
|
|
66
398
|
console.print(f"[yellow]{i18n.get('downloads.queue_stopped')}[/yellow]")
|
|
67
399
|
time.sleep(0.5)
|
|
68
400
|
elif action == opt_clear:
|
|
69
|
-
queue_manager.
|
|
70
|
-
queue_manager._save_queue()
|
|
401
|
+
queue_manager.clear_completed()
|
|
71
402
|
console.print(f"[green]{i18n.get('downloads.cleared')}[/green]")
|
|
72
403
|
time.sleep(0.5)
|
|
73
404
|
|
weeb_cli/commands/settings.py
CHANGED
|
@@ -49,6 +49,7 @@ def open_settings():
|
|
|
49
49
|
opt_lang = i18n.get("settings.language")
|
|
50
50
|
opt_source = f"{i18n.get('settings.source')} [{display_source}]"
|
|
51
51
|
opt_download = i18n.get("settings.download_settings")
|
|
52
|
+
opt_drives = i18n.get("settings.external_drives")
|
|
52
53
|
opt_desc = f"{i18n.get('settings.show_description')} [{desc_state}]"
|
|
53
54
|
opt_aria2 = f"{i18n.get('settings.aria2')} [{aria2_state}]"
|
|
54
55
|
opt_ytdlp = f"{i18n.get('settings.ytdlp')} [{ytdlp_state}]"
|
|
@@ -56,7 +57,7 @@ def open_settings():
|
|
|
56
57
|
opt_aria2_conf = f" ↳ {i18n.get('settings.aria2_config')}"
|
|
57
58
|
opt_ytdlp_conf = f" ↳ {i18n.get('settings.ytdlp_config')}"
|
|
58
59
|
|
|
59
|
-
choices = [opt_lang, opt_source, opt_download, opt_desc, opt_aria2]
|
|
60
|
+
choices = [opt_lang, opt_source, opt_download, opt_drives, opt_desc, opt_aria2]
|
|
60
61
|
if config.get("aria2_enabled"):
|
|
61
62
|
choices.append(opt_aria2_conf)
|
|
62
63
|
|
|
@@ -85,6 +86,8 @@ def open_settings():
|
|
|
85
86
|
change_source()
|
|
86
87
|
elif answer == opt_download:
|
|
87
88
|
download_settings_menu()
|
|
89
|
+
elif answer == opt_drives:
|
|
90
|
+
external_drives_menu()
|
|
88
91
|
elif answer == opt_desc:
|
|
89
92
|
toggle_description()
|
|
90
93
|
elif answer == opt_aria2:
|
|
@@ -252,3 +255,126 @@ def ytdlp_settings_menu():
|
|
|
252
255
|
return
|
|
253
256
|
except KeyboardInterrupt:
|
|
254
257
|
return
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def external_drives_menu():
|
|
261
|
+
from weeb_cli.services.local_library import local_library
|
|
262
|
+
from pathlib import Path
|
|
263
|
+
|
|
264
|
+
while True:
|
|
265
|
+
console.clear()
|
|
266
|
+
show_header(i18n.get("settings.external_drives"))
|
|
267
|
+
|
|
268
|
+
drives = local_library.get_external_drives()
|
|
269
|
+
|
|
270
|
+
opt_add = i18n.get("settings.add_drive")
|
|
271
|
+
|
|
272
|
+
choices = [questionary.Choice(opt_add, value="add")]
|
|
273
|
+
|
|
274
|
+
for drive in drives:
|
|
275
|
+
path = Path(drive["path"])
|
|
276
|
+
status = "● " if path.exists() else "○ "
|
|
277
|
+
choices.append(questionary.Choice(
|
|
278
|
+
f"{status}{drive['name']} ({drive['path']})",
|
|
279
|
+
value=drive
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
sel = questionary.select(
|
|
284
|
+
i18n.get("settings.external_drives"),
|
|
285
|
+
choices=choices,
|
|
286
|
+
pointer=">",
|
|
287
|
+
use_shortcuts=False
|
|
288
|
+
).ask()
|
|
289
|
+
|
|
290
|
+
if sel is None:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
if sel == "add":
|
|
294
|
+
add_external_drive()
|
|
295
|
+
else:
|
|
296
|
+
manage_drive(sel)
|
|
297
|
+
|
|
298
|
+
except KeyboardInterrupt:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
def add_external_drive():
|
|
302
|
+
from weeb_cli.services.local_library import local_library
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
path = questionary.text(
|
|
306
|
+
i18n.get("settings.enter_drive_path"),
|
|
307
|
+
qmark=">"
|
|
308
|
+
).ask()
|
|
309
|
+
|
|
310
|
+
if not path:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
from pathlib import Path
|
|
314
|
+
if not Path(path).exists():
|
|
315
|
+
console.print(f"[yellow]{i18n.get('settings.drive_not_found')}[/yellow]")
|
|
316
|
+
time.sleep(1)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
name = questionary.text(
|
|
320
|
+
i18n.get("settings.enter_drive_name"),
|
|
321
|
+
default=os.path.basename(path) or path,
|
|
322
|
+
qmark=">"
|
|
323
|
+
).ask()
|
|
324
|
+
|
|
325
|
+
if name:
|
|
326
|
+
local_library.add_external_drive(path, name)
|
|
327
|
+
console.print(f"[green]{i18n.get('settings.drive_added')}[/green]")
|
|
328
|
+
time.sleep(0.5)
|
|
329
|
+
|
|
330
|
+
except KeyboardInterrupt:
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
def manage_drive(drive):
|
|
334
|
+
from weeb_cli.services.local_library import local_library
|
|
335
|
+
|
|
336
|
+
while True:
|
|
337
|
+
console.clear()
|
|
338
|
+
show_header(drive["name"])
|
|
339
|
+
|
|
340
|
+
console.print(f"[dim]{drive['path']}[/dim]\n")
|
|
341
|
+
|
|
342
|
+
opt_rename = i18n.get("settings.rename_drive")
|
|
343
|
+
opt_remove = i18n.get("settings.remove_drive")
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
sel = questionary.select(
|
|
347
|
+
i18n.get("downloads.action_prompt"),
|
|
348
|
+
choices=[opt_rename, opt_remove],
|
|
349
|
+
pointer=">",
|
|
350
|
+
use_shortcuts=False
|
|
351
|
+
).ask()
|
|
352
|
+
|
|
353
|
+
if sel is None:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
if sel == opt_rename:
|
|
357
|
+
new_name = questionary.text(
|
|
358
|
+
i18n.get("settings.enter_drive_name"),
|
|
359
|
+
default=drive["name"],
|
|
360
|
+
qmark=">"
|
|
361
|
+
).ask()
|
|
362
|
+
if new_name:
|
|
363
|
+
local_library.rename_external_drive(drive["path"], new_name)
|
|
364
|
+
drive["name"] = new_name
|
|
365
|
+
console.print(f"[green]{i18n.get('settings.drive_renamed')}[/green]")
|
|
366
|
+
time.sleep(0.5)
|
|
367
|
+
|
|
368
|
+
elif sel == opt_remove:
|
|
369
|
+
confirm = questionary.confirm(
|
|
370
|
+
i18n.get("settings.confirm_remove"),
|
|
371
|
+
default=False
|
|
372
|
+
).ask()
|
|
373
|
+
if confirm:
|
|
374
|
+
local_library.remove_external_drive(drive["path"])
|
|
375
|
+
console.print(f"[green]{i18n.get('settings.drive_removed')}[/green]")
|
|
376
|
+
time.sleep(0.5)
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
except KeyboardInterrupt:
|
|
380
|
+
return
|
weeb_cli/config.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
5
4
|
APP_NAME = "weeb-cli"
|
|
6
5
|
CONFIG_DIR = Path.home() / f".{APP_NAME}"
|
|
7
|
-
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
8
6
|
|
|
9
7
|
DEFAULT_CONFIG = {
|
|
10
8
|
"language": None,
|
|
@@ -15,36 +13,28 @@ DEFAULT_CONFIG = {
|
|
|
15
13
|
"download_dir": os.path.join(os.getcwd(), "weeb-downloads"),
|
|
16
14
|
"ytdlp_format": "bestvideo+bestaudio/best",
|
|
17
15
|
"scraping_source": "animecix",
|
|
18
|
-
"show_description": True
|
|
16
|
+
"show_description": True,
|
|
17
|
+
"debug_mode": False
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
class Config:
|
|
22
21
|
def __init__(self):
|
|
23
|
-
self.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _load(self):
|
|
33
|
-
try:
|
|
34
|
-
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
35
|
-
return json.load(f)
|
|
36
|
-
except (json.JSONDecodeError, FileNotFoundError):
|
|
37
|
-
return DEFAULT_CONFIG.copy()
|
|
38
|
-
|
|
39
|
-
def _save(self, data):
|
|
40
|
-
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
41
|
-
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
42
|
-
|
|
22
|
+
self._db = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def db(self):
|
|
26
|
+
if self._db is None:
|
|
27
|
+
from weeb_cli.services.database import db
|
|
28
|
+
self._db = db
|
|
29
|
+
return self._db
|
|
30
|
+
|
|
43
31
|
def get(self, key, default=None):
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
val = self.db.get_config(key)
|
|
33
|
+
if val is None:
|
|
34
|
+
return DEFAULT_CONFIG.get(key, default)
|
|
35
|
+
return val
|
|
36
|
+
|
|
46
37
|
def set(self, key, value):
|
|
47
|
-
self.
|
|
48
|
-
self._save(self.data)
|
|
38
|
+
self.db.set_config(key, value)
|
|
49
39
|
|
|
50
40
|
config = Config()
|
weeb_cli/locales/en.json
CHANGED
|
@@ -48,7 +48,18 @@
|
|
|
48
48
|
"source_changed": "Source changed to {source}.",
|
|
49
49
|
"no_sources": "No sources available for this language.",
|
|
50
50
|
"toggle_on": "{tool} enabled.",
|
|
51
|
-
"toggle_off": "{tool} disabled."
|
|
51
|
+
"toggle_off": "{tool} disabled.",
|
|
52
|
+
"external_drives": "External Drives",
|
|
53
|
+
"add_drive": "Add Drive",
|
|
54
|
+
"enter_drive_path": "Enter drive path (e.g. D:\\Anime)",
|
|
55
|
+
"enter_drive_name": "Nickname for drive",
|
|
56
|
+
"drive_not_found": "Specified path not found.",
|
|
57
|
+
"drive_added": "Drive added.",
|
|
58
|
+
"rename_drive": "Rename",
|
|
59
|
+
"remove_drive": "Remove Drive",
|
|
60
|
+
"confirm_remove": "Are you sure you want to remove this drive?",
|
|
61
|
+
"drive_renamed": "Drive renamed.",
|
|
62
|
+
"drive_removed": "Drive removed."
|
|
52
63
|
},
|
|
53
64
|
"setup": {
|
|
54
65
|
"welcome": "Welcome to Weeb CLI!",
|
|
@@ -139,7 +150,7 @@
|
|
|
139
150
|
"no_streams": "No streams found"
|
|
140
151
|
},
|
|
141
152
|
"downloads": {
|
|
142
|
-
"title": "
|
|
153
|
+
"title": "Downloads",
|
|
143
154
|
"empty": "You haven't downloaded anything yet.",
|
|
144
155
|
"status": "Status",
|
|
145
156
|
"progress": "Progress",
|
|
@@ -163,6 +174,23 @@
|
|
|
163
174
|
"queue_stopped": "Queue stopped.",
|
|
164
175
|
"queue_running": "Running",
|
|
165
176
|
"pending_count": "{count} download(s) pending",
|
|
166
|
-
"cleared": "Cleared."
|
|
177
|
+
"cleared": "Cleared.",
|
|
178
|
+
"completed_downloads": "Completed Downloads",
|
|
179
|
+
"active_downloads": "Active Downloads",
|
|
180
|
+
"manage_queue": "Queue Management",
|
|
181
|
+
"select_anime": "Select Anime",
|
|
182
|
+
"search_library": "Search",
|
|
183
|
+
"search_anime": "Enter anime name",
|
|
184
|
+
"search_all": "Search All Sources",
|
|
185
|
+
"offline": "Offline",
|
|
186
|
+
"reindex": "Reindex",
|
|
187
|
+
"indexing": "Indexing...",
|
|
188
|
+
"indexed": "{count} anime indexed",
|
|
189
|
+
"no_indexed": "No indexed anime. Connect a drive and index first.",
|
|
190
|
+
"drive_not_connected": "Drive not connected",
|
|
191
|
+
"connect_drive": "Connect {name} drive",
|
|
192
|
+
"notification_title": "Weeb CLI",
|
|
193
|
+
"notification_complete": "{anime} - Episode {episode} downloaded",
|
|
194
|
+
"episode_short": "Ep"
|
|
167
195
|
}
|
|
168
196
|
}
|