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/services/downloader.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import json
|
|
3
2
|
import re
|
|
4
3
|
import threading
|
|
5
4
|
import time
|
|
@@ -13,27 +12,21 @@ console = Console()
|
|
|
13
12
|
|
|
14
13
|
class QueueManager:
|
|
15
14
|
def __init__(self):
|
|
16
|
-
self.
|
|
17
|
-
self.queue_file = self.config_dir / "download_queue.json"
|
|
18
|
-
self.queue = []
|
|
19
|
-
self.active_downloads = 0
|
|
15
|
+
self._db = None
|
|
20
16
|
self.lock = threading.Lock()
|
|
21
17
|
self.running = False
|
|
22
18
|
self.worker_thread = None
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def
|
|
26
|
-
if self.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
with self.lock:
|
|
35
|
-
with open(self.queue_file, 'w', encoding='utf-8') as f:
|
|
36
|
-
json.dump(self.queue, f, indent=2, ensure_ascii=False)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def db(self):
|
|
22
|
+
if self._db is None:
|
|
23
|
+
from weeb_cli.services.database import db
|
|
24
|
+
self._db = db
|
|
25
|
+
return self._db
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def queue(self):
|
|
29
|
+
return self.db.get_queue()
|
|
37
30
|
|
|
38
31
|
def start_queue(self):
|
|
39
32
|
if self.running:
|
|
@@ -61,13 +54,14 @@ class QueueManager:
|
|
|
61
54
|
def resume_incomplete(self):
|
|
62
55
|
for item in self.queue:
|
|
63
56
|
if item["status"] == "processing":
|
|
64
|
-
item["
|
|
65
|
-
self._save_queue()
|
|
57
|
+
self.db.update_queue_item(item["episode_id"], status="pending")
|
|
66
58
|
self.start_queue()
|
|
67
59
|
|
|
68
60
|
def cancel_incomplete(self):
|
|
69
|
-
self.
|
|
70
|
-
self.
|
|
61
|
+
self.db.clear_completed_queue()
|
|
62
|
+
for item in self.queue:
|
|
63
|
+
if item["status"] in ["pending", "processing"]:
|
|
64
|
+
self.db.update_queue_item(item["episode_id"], status="cancelled")
|
|
71
65
|
|
|
72
66
|
def is_downloading(self, slug, episode_id=None):
|
|
73
67
|
for item in self.queue:
|
|
@@ -83,7 +77,7 @@ class QueueManager:
|
|
|
83
77
|
ep_id = ep.get("id")
|
|
84
78
|
if self.is_downloading(slug, ep_id):
|
|
85
79
|
continue
|
|
86
|
-
|
|
80
|
+
|
|
87
81
|
item = {
|
|
88
82
|
"anime_title": anime_title,
|
|
89
83
|
"episode_number": ep.get("number") or ep.get("ep_num"),
|
|
@@ -94,10 +88,8 @@ class QueueManager:
|
|
|
94
88
|
"progress": 0,
|
|
95
89
|
"eta": "?"
|
|
96
90
|
}
|
|
97
|
-
if
|
|
98
|
-
self.queue.append(item)
|
|
91
|
+
if self.db.add_to_queue(item):
|
|
99
92
|
added += 1
|
|
100
|
-
self._save_queue()
|
|
101
93
|
return added
|
|
102
94
|
|
|
103
95
|
def _sanitize_filename(self, name):
|
|
@@ -107,14 +99,13 @@ class QueueManager:
|
|
|
107
99
|
while self.running:
|
|
108
100
|
max_workers = config.get("max_concurrent_downloads", 3)
|
|
109
101
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
queue = self.queue
|
|
103
|
+
active_count = len([x for x in queue if x["status"] == "processing"])
|
|
104
|
+
pending = [x for x in queue if x["status"] == "pending"]
|
|
113
105
|
|
|
114
106
|
if active_count < max_workers and pending:
|
|
115
107
|
to_start = pending[0]
|
|
116
|
-
to_start["
|
|
117
|
-
self._save_queue()
|
|
108
|
+
self.db.update_queue_item(to_start["episode_id"], status="processing")
|
|
118
109
|
|
|
119
110
|
t = threading.Thread(target=self._run_task, args=(to_start,))
|
|
120
111
|
t.start()
|
|
@@ -126,16 +117,25 @@ class QueueManager:
|
|
|
126
117
|
time.sleep(1)
|
|
127
118
|
|
|
128
119
|
def _run_task(self, item):
|
|
120
|
+
from weeb_cli.services.notifier import send_notification
|
|
121
|
+
from weeb_cli.services.logger import debug, error
|
|
122
|
+
from weeb_cli.i18n import i18n
|
|
123
|
+
|
|
124
|
+
debug(f"Starting download: {item['anime_title']} - Ep {item['episode_number']}")
|
|
125
|
+
|
|
129
126
|
try:
|
|
130
127
|
self._download_item(item)
|
|
131
|
-
item["
|
|
132
|
-
|
|
133
|
-
item[
|
|
128
|
+
self.db.update_queue_item(item["episode_id"], status="completed", progress=100, eta="-")
|
|
129
|
+
|
|
130
|
+
debug(f"Download completed: {item['anime_title']} - Ep {item['episode_number']}")
|
|
131
|
+
|
|
132
|
+
title = i18n.get("downloads.notification_title", "Weeb CLI")
|
|
133
|
+
msg = i18n.t("downloads.notification_complete", anime=item['anime_title'], episode=item['episode_number'])
|
|
134
|
+
send_notification(title, msg)
|
|
135
|
+
|
|
134
136
|
except Exception as e:
|
|
135
|
-
item["
|
|
136
|
-
item[
|
|
137
|
-
item["eta"] = ""
|
|
138
|
-
self._save_queue()
|
|
137
|
+
self.db.update_queue_item(item["episode_id"], status="failed", error=str(e), eta="")
|
|
138
|
+
error(f"Download failed: {item['anime_title']} - {str(e)}")
|
|
139
139
|
|
|
140
140
|
def _download_item(self, item):
|
|
141
141
|
from weeb_cli.services.watch import get_streams
|
|
@@ -200,6 +200,15 @@ class QueueManager:
|
|
|
200
200
|
return node["url"]
|
|
201
201
|
return None
|
|
202
202
|
|
|
203
|
+
def _update_progress(self, item, progress=None, eta=None):
|
|
204
|
+
updates = {}
|
|
205
|
+
if progress is not None:
|
|
206
|
+
updates["progress"] = progress
|
|
207
|
+
if eta is not None:
|
|
208
|
+
updates["eta"] = eta
|
|
209
|
+
if updates:
|
|
210
|
+
self.db.update_queue_item(item["episode_id"], **updates)
|
|
211
|
+
|
|
203
212
|
def _download_aria2(self, url, path, item):
|
|
204
213
|
aria2 = dependency_manager.check_dependency("aria2")
|
|
205
214
|
conn = config.get("aria2_max_connections", 16)
|
|
@@ -227,11 +236,10 @@ class QueueManager:
|
|
|
227
236
|
try:
|
|
228
237
|
parts = line.split("ETA:")
|
|
229
238
|
eta_part = parts[1].split("]")[0]
|
|
230
|
-
item["eta"] = eta_part.strip()
|
|
231
239
|
|
|
232
240
|
match = re.search(r'\((\d+)%\)', line)
|
|
233
|
-
if match
|
|
234
|
-
|
|
241
|
+
progress = int(match.group(1)) if match else None
|
|
242
|
+
self._update_progress(item, progress=progress, eta=eta_part.strip())
|
|
235
243
|
except:
|
|
236
244
|
pass
|
|
237
245
|
|
|
@@ -258,16 +266,16 @@ class QueueManager:
|
|
|
258
266
|
if "[download]" in line and "%" in line:
|
|
259
267
|
try:
|
|
260
268
|
p_str = line.split("%")[0].split()[-1]
|
|
261
|
-
|
|
262
|
-
if "ETA" in line
|
|
263
|
-
|
|
269
|
+
progress = float(p_str)
|
|
270
|
+
eta = line.split("ETA")[-1].strip() if "ETA" in line else None
|
|
271
|
+
self._update_progress(item, progress=progress, eta=eta)
|
|
264
272
|
except:
|
|
265
273
|
pass
|
|
266
274
|
if process.returncode != 0:
|
|
267
275
|
raise Exception("yt-dlp failed")
|
|
268
276
|
|
|
269
277
|
def _download_ffmpeg(self, url, path, item):
|
|
270
|
-
item
|
|
278
|
+
self._update_progress(item, eta="")
|
|
271
279
|
ffmpeg = dependency_manager.check_dependency("ffmpeg")
|
|
272
280
|
cmd = [
|
|
273
281
|
ffmpeg,
|
|
@@ -281,7 +289,7 @@ class QueueManager:
|
|
|
281
289
|
|
|
282
290
|
def _download_generic(self, url, path, item):
|
|
283
291
|
import requests
|
|
284
|
-
item
|
|
292
|
+
self._update_progress(item, eta="...")
|
|
285
293
|
with requests.get(url, stream=True) as r:
|
|
286
294
|
r.raise_for_status()
|
|
287
295
|
total = int(r.headers.get('content-length', 0))
|
|
@@ -292,17 +300,20 @@ class QueueManager:
|
|
|
292
300
|
for chunk in r.iter_content(chunk_size=8192):
|
|
293
301
|
f.write(chunk)
|
|
294
302
|
downloaded += len(chunk)
|
|
295
|
-
|
|
303
|
+
progress = int((downloaded / total) * 100)
|
|
296
304
|
|
|
297
305
|
elapsed = time.time() - start_time
|
|
298
306
|
if elapsed > 0:
|
|
299
307
|
speed = downloaded / elapsed
|
|
300
308
|
remaining = total - downloaded
|
|
301
309
|
eta_s = remaining / speed
|
|
302
|
-
item
|
|
310
|
+
self._update_progress(item, progress=progress, eta=f"{int(eta_s)}s")
|
|
303
311
|
else:
|
|
304
312
|
with open(path, 'wb') as f:
|
|
305
313
|
for chunk in r.iter_content(chunk_size=8192):
|
|
306
314
|
f.write(chunk)
|
|
307
315
|
|
|
316
|
+
def clear_completed(self):
|
|
317
|
+
self.db.clear_completed_queue()
|
|
318
|
+
|
|
308
319
|
queue_manager = QueueManager()
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Dict, Optional
|
|
5
|
+
from weeb_cli.config import config
|
|
6
|
+
from weeb_cli.services.progress import progress_tracker
|
|
7
|
+
|
|
8
|
+
class LocalLibrary:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._db = None
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def db(self):
|
|
14
|
+
if self._db is None:
|
|
15
|
+
from weeb_cli.services.database import db
|
|
16
|
+
self._db = db
|
|
17
|
+
return self._db
|
|
18
|
+
|
|
19
|
+
def get_all_sources(self) -> List[Dict]:
|
|
20
|
+
sources = []
|
|
21
|
+
|
|
22
|
+
download_dir = Path(config.get("download_dir"))
|
|
23
|
+
if download_dir.exists():
|
|
24
|
+
sources.append({
|
|
25
|
+
"path": str(download_dir),
|
|
26
|
+
"name": "İndirilenler",
|
|
27
|
+
"type": "local",
|
|
28
|
+
"available": True
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
for drive in self.db.get_external_drives():
|
|
32
|
+
path = Path(drive["path"])
|
|
33
|
+
sources.append({
|
|
34
|
+
"path": drive["path"],
|
|
35
|
+
"name": drive["name"],
|
|
36
|
+
"type": "external",
|
|
37
|
+
"available": path.exists()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return sources
|
|
41
|
+
|
|
42
|
+
def scan_library(self, source_path: str = None) -> List[Dict]:
|
|
43
|
+
if source_path:
|
|
44
|
+
return self._scan_folder(Path(source_path))
|
|
45
|
+
|
|
46
|
+
download_dir = Path(config.get("download_dir"))
|
|
47
|
+
return self._scan_folder(download_dir)
|
|
48
|
+
|
|
49
|
+
def scan_all_sources(self) -> List[Dict]:
|
|
50
|
+
all_anime = []
|
|
51
|
+
seen_titles = set()
|
|
52
|
+
|
|
53
|
+
for source in self.get_all_sources():
|
|
54
|
+
if not source["available"]:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
anime_list = self._scan_folder(Path(source["path"]))
|
|
58
|
+
for anime in anime_list:
|
|
59
|
+
anime["source"] = source["name"]
|
|
60
|
+
anime["source_path"] = source["path"]
|
|
61
|
+
|
|
62
|
+
key = anime["title"].lower()
|
|
63
|
+
if key not in seen_titles:
|
|
64
|
+
seen_titles.add(key)
|
|
65
|
+
all_anime.append(anime)
|
|
66
|
+
|
|
67
|
+
return sorted(all_anime, key=lambda x: x["title"].lower())
|
|
68
|
+
|
|
69
|
+
def _scan_folder(self, folder: Path) -> List[Dict]:
|
|
70
|
+
if not folder.exists():
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
anime_list = []
|
|
74
|
+
|
|
75
|
+
for anime_folder in folder.iterdir():
|
|
76
|
+
if not anime_folder.is_dir():
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
episodes = self._scan_anime_folder(anime_folder)
|
|
80
|
+
if episodes:
|
|
81
|
+
anime_list.append({
|
|
82
|
+
"title": anime_folder.name,
|
|
83
|
+
"path": str(anime_folder),
|
|
84
|
+
"episodes": episodes,
|
|
85
|
+
"episode_count": len(episodes)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return sorted(anime_list, key=lambda x: x["title"].lower())
|
|
89
|
+
|
|
90
|
+
def _scan_anime_folder(self, folder: Path) -> List[Dict]:
|
|
91
|
+
episodes = []
|
|
92
|
+
video_extensions = {'.mp4', '.mkv', '.avi', '.webm', '.m4v'}
|
|
93
|
+
|
|
94
|
+
for file in folder.iterdir():
|
|
95
|
+
if file.is_file() and file.suffix.lower() in video_extensions:
|
|
96
|
+
ep_num = self._extract_episode_number(file.name)
|
|
97
|
+
episodes.append({
|
|
98
|
+
"filename": file.name,
|
|
99
|
+
"path": str(file),
|
|
100
|
+
"number": ep_num,
|
|
101
|
+
"size": file.stat().st_size
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return sorted(episodes, key=lambda x: x["number"])
|
|
105
|
+
|
|
106
|
+
def _extract_episode_number(self, filename: str) -> int:
|
|
107
|
+
patterns = [
|
|
108
|
+
r'S\d+B(\d+)',
|
|
109
|
+
r'[Ee]p?(\d+)',
|
|
110
|
+
r'[Bb]ölüm\s*(\d+)',
|
|
111
|
+
r'[Ee]pisode\s*(\d+)',
|
|
112
|
+
r'- (\d+)',
|
|
113
|
+
r'\[(\d+)\]',
|
|
114
|
+
r'(\d+)\.',
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
for pattern in patterns:
|
|
118
|
+
match = re.search(pattern, filename, re.IGNORECASE)
|
|
119
|
+
if match:
|
|
120
|
+
return int(match.group(1))
|
|
121
|
+
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
def get_anime_progress(self, anime_title: str) -> Dict:
|
|
125
|
+
slug = self._title_to_slug(anime_title)
|
|
126
|
+
return progress_tracker.get_anime_progress(slug)
|
|
127
|
+
|
|
128
|
+
def mark_episode_watched(self, anime_title: str, ep_number: int, total_episodes: int):
|
|
129
|
+
slug = self._title_to_slug(anime_title)
|
|
130
|
+
progress_tracker.mark_watched(slug, ep_number, title=anime_title, total_episodes=total_episodes)
|
|
131
|
+
|
|
132
|
+
def _title_to_slug(self, title: str) -> str:
|
|
133
|
+
slug = title.lower()
|
|
134
|
+
slug = re.sub(r'[^a-z0-9\s-]', '', slug)
|
|
135
|
+
slug = re.sub(r'\s+', '-', slug)
|
|
136
|
+
return slug
|
|
137
|
+
|
|
138
|
+
def get_next_episode(self, anime_title: str, episodes: List[Dict]) -> Optional[Dict]:
|
|
139
|
+
progress = self.get_anime_progress(anime_title)
|
|
140
|
+
last_watched = progress.get("last_watched", 0)
|
|
141
|
+
|
|
142
|
+
for ep in episodes:
|
|
143
|
+
if ep["number"] > last_watched:
|
|
144
|
+
return ep
|
|
145
|
+
|
|
146
|
+
return episodes[0] if episodes else None
|
|
147
|
+
|
|
148
|
+
def format_size(self, size_bytes: int) -> str:
|
|
149
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
150
|
+
if size_bytes < 1024:
|
|
151
|
+
return f"{size_bytes:.1f} {unit}"
|
|
152
|
+
size_bytes /= 1024
|
|
153
|
+
return f"{size_bytes:.1f} TB"
|
|
154
|
+
|
|
155
|
+
def add_external_drive(self, path: str, name: str = None):
|
|
156
|
+
self.db.add_external_drive(path, name)
|
|
157
|
+
|
|
158
|
+
def remove_external_drive(self, path: str):
|
|
159
|
+
self.db.remove_external_drive(path)
|
|
160
|
+
|
|
161
|
+
def rename_external_drive(self, path: str, name: str):
|
|
162
|
+
self.db.update_drive_name(path, name)
|
|
163
|
+
|
|
164
|
+
def get_external_drives(self):
|
|
165
|
+
return self.db.get_external_drives()
|
|
166
|
+
|
|
167
|
+
def index_source(self, source_path: str, source_name: str):
|
|
168
|
+
path = Path(source_path)
|
|
169
|
+
if not path.exists():
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
self.db.clear_source_index(source_path)
|
|
173
|
+
|
|
174
|
+
count = 0
|
|
175
|
+
for anime_folder in path.iterdir():
|
|
176
|
+
if not anime_folder.is_dir():
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
episodes = self._scan_anime_folder(anime_folder)
|
|
180
|
+
if episodes:
|
|
181
|
+
self.db.index_anime(
|
|
182
|
+
title=anime_folder.name,
|
|
183
|
+
source_path=source_path,
|
|
184
|
+
source_name=source_name,
|
|
185
|
+
folder_path=str(anime_folder),
|
|
186
|
+
episode_count=len(episodes)
|
|
187
|
+
)
|
|
188
|
+
count += 1
|
|
189
|
+
|
|
190
|
+
return count
|
|
191
|
+
|
|
192
|
+
def smart_index_source(self, source_path: str, source_name: str):
|
|
193
|
+
path = Path(source_path)
|
|
194
|
+
if not path.exists():
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
indexed = {a["folder_path"]: a for a in self.db.get_all_indexed_anime() if a["source_path"] == source_path}
|
|
198
|
+
|
|
199
|
+
current_folders = set()
|
|
200
|
+
added = 0
|
|
201
|
+
|
|
202
|
+
for anime_folder in path.iterdir():
|
|
203
|
+
if not anime_folder.is_dir():
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
folder_path = str(anime_folder)
|
|
207
|
+
current_folders.add(folder_path)
|
|
208
|
+
|
|
209
|
+
episodes = self._scan_anime_folder(anime_folder)
|
|
210
|
+
if not episodes:
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if folder_path in indexed:
|
|
214
|
+
if indexed[folder_path]["episode_count"] != len(episodes):
|
|
215
|
+
self.db.index_anime(
|
|
216
|
+
title=anime_folder.name,
|
|
217
|
+
source_path=source_path,
|
|
218
|
+
source_name=source_name,
|
|
219
|
+
folder_path=folder_path,
|
|
220
|
+
episode_count=len(episodes)
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
self.db.index_anime(
|
|
224
|
+
title=anime_folder.name,
|
|
225
|
+
source_path=source_path,
|
|
226
|
+
source_name=source_name,
|
|
227
|
+
folder_path=folder_path,
|
|
228
|
+
episode_count=len(episodes)
|
|
229
|
+
)
|
|
230
|
+
added += 1
|
|
231
|
+
|
|
232
|
+
for folder_path in indexed:
|
|
233
|
+
if folder_path not in current_folders:
|
|
234
|
+
self.db.remove_indexed_anime(folder_path)
|
|
235
|
+
|
|
236
|
+
return added
|
|
237
|
+
|
|
238
|
+
def smart_index_all(self):
|
|
239
|
+
total = 0
|
|
240
|
+
for source in self.get_all_sources():
|
|
241
|
+
if source["available"]:
|
|
242
|
+
total += self.smart_index_source(source["path"], source["name"])
|
|
243
|
+
return total
|
|
244
|
+
|
|
245
|
+
def index_all_sources(self):
|
|
246
|
+
total = 0
|
|
247
|
+
for source in self.get_all_sources():
|
|
248
|
+
if source["available"]:
|
|
249
|
+
total += self.index_source(source["path"], source["name"])
|
|
250
|
+
return total
|
|
251
|
+
|
|
252
|
+
def get_indexed_anime(self) -> List[Dict]:
|
|
253
|
+
return self.db.get_all_indexed_anime()
|
|
254
|
+
|
|
255
|
+
def search_all_indexed(self, query: str) -> List[Dict]:
|
|
256
|
+
if not query:
|
|
257
|
+
return self.db.get_all_indexed_anime()
|
|
258
|
+
return self.db.search_indexed_anime(query)
|
|
259
|
+
|
|
260
|
+
def is_source_available(self, source_path: str) -> bool:
|
|
261
|
+
return Path(source_path).exists()
|
|
262
|
+
|
|
263
|
+
local_library = LocalLibrary()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from weeb_cli.config import config
|
|
6
|
+
|
|
7
|
+
_logger = None
|
|
8
|
+
|
|
9
|
+
def get_logger():
|
|
10
|
+
global _logger
|
|
11
|
+
if _logger is None:
|
|
12
|
+
_logger = _setup_logger()
|
|
13
|
+
return _logger
|
|
14
|
+
|
|
15
|
+
def _setup_logger():
|
|
16
|
+
logger = logging.getLogger("weeb-cli")
|
|
17
|
+
logger.setLevel(logging.DEBUG)
|
|
18
|
+
logger.handlers = []
|
|
19
|
+
|
|
20
|
+
if config.get("debug_mode", False):
|
|
21
|
+
log_dir = Path.home() / ".weeb-cli" / "logs"
|
|
22
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
log_file = log_dir / f"debug_{datetime.now().strftime('%Y%m%d')}.log"
|
|
25
|
+
|
|
26
|
+
handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
27
|
+
handler.setLevel(logging.DEBUG)
|
|
28
|
+
|
|
29
|
+
formatter = logging.Formatter(
|
|
30
|
+
'%(asctime)s | %(levelname)s | %(module)s:%(lineno)d | %(message)s',
|
|
31
|
+
datefmt='%H:%M:%S'
|
|
32
|
+
)
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
logger.addHandler(handler)
|
|
35
|
+
else:
|
|
36
|
+
logger.addHandler(logging.NullHandler())
|
|
37
|
+
|
|
38
|
+
return logger
|
|
39
|
+
|
|
40
|
+
def debug(msg, *args):
|
|
41
|
+
get_logger().debug(msg, *args)
|
|
42
|
+
|
|
43
|
+
def info(msg, *args):
|
|
44
|
+
get_logger().info(msg, *args)
|
|
45
|
+
|
|
46
|
+
def warning(msg, *args):
|
|
47
|
+
get_logger().warning(msg, *args)
|
|
48
|
+
|
|
49
|
+
def error(msg, *args):
|
|
50
|
+
get_logger().error(msg, *args)
|
|
51
|
+
|
|
52
|
+
def reload():
|
|
53
|
+
global _logger
|
|
54
|
+
_logger = None
|
|
55
|
+
get_logger()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import subprocess
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
def send_notification(title: str, message: str):
|
|
6
|
+
threading.Thread(target=_send_notification_sync, args=(title, message), daemon=True).start()
|
|
7
|
+
|
|
8
|
+
def _send_notification_sync(title: str, message: str):
|
|
9
|
+
system = platform.system()
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
if system == "Windows":
|
|
13
|
+
_notify_windows(title, message)
|
|
14
|
+
elif system == "Darwin":
|
|
15
|
+
_notify_macos(title, message)
|
|
16
|
+
else:
|
|
17
|
+
_notify_linux(title, message)
|
|
18
|
+
except:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def _notify_windows(title: str, message: str):
|
|
22
|
+
try:
|
|
23
|
+
from win10toast import ToastNotifier
|
|
24
|
+
toaster = ToastNotifier()
|
|
25
|
+
toaster.show_toast(title, message, duration=5, threaded=True)
|
|
26
|
+
return
|
|
27
|
+
except ImportError:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import ctypes
|
|
32
|
+
ctypes.windll.user32.MessageBoxW(0, message, title, 0x40)
|
|
33
|
+
except:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def _notify_macos(title: str, message: str):
|
|
37
|
+
script = f'display notification "{message}" with title "{title}"'
|
|
38
|
+
subprocess.run(["osascript", "-e", script], capture_output=True)
|
|
39
|
+
|
|
40
|
+
def _notify_linux(title: str, message: str):
|
|
41
|
+
subprocess.run(["notify-send", title, message], capture_output=True)
|