spatelier 0.3.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.
- analytics/__init__.py +1 -0
- analytics/reporter.py +497 -0
- cli/__init__.py +1 -0
- cli/app.py +147 -0
- cli/audio.py +129 -0
- cli/cli_analytics.py +320 -0
- cli/cli_utils.py +282 -0
- cli/error_handlers.py +122 -0
- cli/files.py +299 -0
- cli/update.py +325 -0
- cli/video.py +823 -0
- cli/worker.py +615 -0
- core/__init__.py +1 -0
- core/analytics_dashboard.py +368 -0
- core/base.py +303 -0
- core/base_service.py +69 -0
- core/config.py +345 -0
- core/database_service.py +116 -0
- core/decorators.py +263 -0
- core/error_handler.py +210 -0
- core/file_tracker.py +254 -0
- core/interactive_cli.py +366 -0
- core/interfaces.py +166 -0
- core/job_queue.py +437 -0
- core/logger.py +79 -0
- core/package_updater.py +469 -0
- core/progress.py +228 -0
- core/service_factory.py +295 -0
- core/streaming.py +299 -0
- core/worker.py +765 -0
- database/__init__.py +1 -0
- database/connection.py +265 -0
- database/metadata.py +516 -0
- database/models.py +288 -0
- database/repository.py +592 -0
- database/transcription_storage.py +219 -0
- modules/__init__.py +1 -0
- modules/audio/__init__.py +5 -0
- modules/audio/converter.py +197 -0
- modules/video/__init__.py +16 -0
- modules/video/converter.py +191 -0
- modules/video/fallback_extractor.py +334 -0
- modules/video/services/__init__.py +18 -0
- modules/video/services/audio_extraction_service.py +274 -0
- modules/video/services/download_service.py +852 -0
- modules/video/services/metadata_service.py +190 -0
- modules/video/services/playlist_service.py +445 -0
- modules/video/services/transcription_service.py +491 -0
- modules/video/transcription_service.py +385 -0
- modules/video/youtube_api.py +397 -0
- spatelier/__init__.py +33 -0
- spatelier-0.3.0.dist-info/METADATA +260 -0
- spatelier-0.3.0.dist-info/RECORD +59 -0
- spatelier-0.3.0.dist-info/WHEEL +5 -0
- spatelier-0.3.0.dist-info/entry_points.txt +2 -0
- spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
- spatelier-0.3.0.dist-info/top_level.txt +7 -0
- utils/__init__.py +1 -0
- utils/helpers.py +250 -0
core/package_updater.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified package updater for automatic dependency updates.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to check and update critical packages,
|
|
5
|
+
supporting both automatic background updates and manual update operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from core.config import Config
|
|
17
|
+
from core.logger import get_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PackageUpdater:
|
|
21
|
+
"""
|
|
22
|
+
Unified package updater supporting both automatic and manual updates.
|
|
23
|
+
|
|
24
|
+
Consolidates functionality from AutoUpdater and PackageUpdater.
|
|
25
|
+
Supports background automatic updates and manual update checks/operations.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
config: Config,
|
|
31
|
+
verbose: bool = False,
|
|
32
|
+
auto_update: bool = False,
|
|
33
|
+
check_frequency_hours: int = 24,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Initialize package updater.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config: Configuration instance
|
|
40
|
+
verbose: Enable verbose logging
|
|
41
|
+
auto_update: Enable automatic background updates
|
|
42
|
+
check_frequency_hours: Hours between update checks (default: 24)
|
|
43
|
+
"""
|
|
44
|
+
self.config = config
|
|
45
|
+
self.verbose = verbose
|
|
46
|
+
self.auto_update = auto_update
|
|
47
|
+
self.check_frequency_hours = check_frequency_hours
|
|
48
|
+
self.logger = get_logger("PackageUpdater", verbose=verbose)
|
|
49
|
+
|
|
50
|
+
# Critical packages that should be kept up-to-date
|
|
51
|
+
self.critical_packages = {
|
|
52
|
+
"yt-dlp": {
|
|
53
|
+
"description": "YouTube downloader",
|
|
54
|
+
"check_command": [sys.executable, "-m", "yt_dlp", "--version"],
|
|
55
|
+
"update_command": [
|
|
56
|
+
sys.executable,
|
|
57
|
+
"-m",
|
|
58
|
+
"pip",
|
|
59
|
+
"install",
|
|
60
|
+
"--upgrade",
|
|
61
|
+
"yt-dlp",
|
|
62
|
+
"--quiet",
|
|
63
|
+
],
|
|
64
|
+
"version_file": "yt-dlp_version.json",
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Lock for thread safety
|
|
69
|
+
self._update_lock = threading.Lock()
|
|
70
|
+
self._last_check_file = (
|
|
71
|
+
Path(config.database.sqlite_path.parent) / "auto_update_last_check.json"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def should_check_updates(self, package_name: Optional[str] = None) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Check if enough time has passed since last update check.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
package_name: Optional package name (for per-package checking, kept for compatibility)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if should check for updates
|
|
83
|
+
"""
|
|
84
|
+
if not self._last_check_file.exists():
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
with open(self._last_check_file, "r") as f:
|
|
89
|
+
data = json.load(f)
|
|
90
|
+
|
|
91
|
+
last_check = datetime.fromisoformat(data.get("last_check", "1970-01-01"))
|
|
92
|
+
hours_since_check = (datetime.now() - last_check).total_seconds() / 3600
|
|
93
|
+
|
|
94
|
+
return hours_since_check >= self.check_frequency_hours
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.debug(f"Failed to check last update time: {e}")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def _save_check_time(self):
|
|
101
|
+
"""Save the last check time."""
|
|
102
|
+
try:
|
|
103
|
+
self._last_check_file.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
data = {
|
|
106
|
+
"last_check": datetime.now().isoformat(),
|
|
107
|
+
"check_frequency_hours": self.check_frequency_hours,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
with open(self._last_check_file, "w") as f:
|
|
111
|
+
json.dump(data, f, indent=2)
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
self.logger.debug(f"Failed to save check time: {e}")
|
|
115
|
+
|
|
116
|
+
def _get_current_version(self, package_name: str) -> str:
|
|
117
|
+
"""Get current installed version of package."""
|
|
118
|
+
try:
|
|
119
|
+
if package_name == "yt-dlp":
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
[sys.executable, "-m", "yt_dlp", "--version"],
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
check=True,
|
|
125
|
+
timeout=10,
|
|
126
|
+
)
|
|
127
|
+
return result.stdout.strip()
|
|
128
|
+
else:
|
|
129
|
+
# Fallback to pip show
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
[sys.executable, "-m", "pip", "show", package_name],
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
check=True,
|
|
135
|
+
timeout=10,
|
|
136
|
+
)
|
|
137
|
+
for line in result.stdout.split("\n"):
|
|
138
|
+
if line.startswith("Version:"):
|
|
139
|
+
return line.split(":", 1)[1].strip()
|
|
140
|
+
return "unknown"
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self.logger.debug(f"Failed to get current version for {package_name}: {e}")
|
|
143
|
+
return "unknown"
|
|
144
|
+
|
|
145
|
+
def _get_latest_version(self, package_name: str) -> str:
|
|
146
|
+
"""Get latest available version of package."""
|
|
147
|
+
try:
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
[sys.executable, "-m", "pip", "index", "versions", package_name],
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
check=True,
|
|
153
|
+
timeout=15,
|
|
154
|
+
)
|
|
155
|
+
# Parse the output to get latest version
|
|
156
|
+
lines = result.stdout.split("\n")
|
|
157
|
+
for line in lines:
|
|
158
|
+
if "Available versions:" in line:
|
|
159
|
+
versions = line.split("Available versions:")[1].strip()
|
|
160
|
+
# Get the first (latest) version)
|
|
161
|
+
latest = versions.split(",")[0].strip()
|
|
162
|
+
return latest
|
|
163
|
+
return "unknown"
|
|
164
|
+
except Exception as e:
|
|
165
|
+
self.logger.debug(f"Failed to get latest version for {package_name}: {e}")
|
|
166
|
+
return "unknown"
|
|
167
|
+
|
|
168
|
+
def _compare_versions(self, current: str, latest: str) -> bool:
|
|
169
|
+
"""Compare version strings to determine if update is needed."""
|
|
170
|
+
if current == "unknown" or latest == "unknown":
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Simple string comparison for now
|
|
174
|
+
# In production, you'd want proper semantic version comparison
|
|
175
|
+
return current != latest
|
|
176
|
+
|
|
177
|
+
def check_package_updates(self, package_name: str) -> Dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Check if a package has updates available.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
package_name: Name of package to check
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Dictionary with update information
|
|
186
|
+
"""
|
|
187
|
+
if package_name not in self.critical_packages:
|
|
188
|
+
return {"error": f"Package {package_name} not in critical packages list"}
|
|
189
|
+
|
|
190
|
+
package_info = self.critical_packages[package_name]
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# Get current version
|
|
194
|
+
current_version = self._get_current_version(package_name)
|
|
195
|
+
|
|
196
|
+
# Get latest version
|
|
197
|
+
latest_version = self._get_latest_version(package_name)
|
|
198
|
+
|
|
199
|
+
# Check if update is needed
|
|
200
|
+
needs_update = self._compare_versions(current_version, latest_version)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"package": package_name,
|
|
204
|
+
"current_version": current_version,
|
|
205
|
+
"latest_version": latest_version,
|
|
206
|
+
"needs_update": needs_update,
|
|
207
|
+
"last_checked": datetime.now().isoformat(),
|
|
208
|
+
"description": package_info["description"],
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
self.logger.error(f"Failed to check updates for {package_name}: {e}")
|
|
213
|
+
return {"error": str(e)}
|
|
214
|
+
|
|
215
|
+
def update_package(
|
|
216
|
+
self, package_name: str, auto_confirm: bool = False, silent: bool = False
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Update a package to the latest version.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
package_name: Name of package to update
|
|
223
|
+
auto_confirm: Whether to update without user confirmation (deprecated, kept for compatibility)
|
|
224
|
+
silent: Whether to run update silently (for background updates)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dictionary with update result
|
|
228
|
+
"""
|
|
229
|
+
if package_name not in self.critical_packages:
|
|
230
|
+
return {"error": f"Package {package_name} not in critical packages list"}
|
|
231
|
+
|
|
232
|
+
package_info = self.critical_packages[package_name]
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if not silent:
|
|
236
|
+
self.logger.info(f"Updating {package_name}...")
|
|
237
|
+
else:
|
|
238
|
+
self.logger.debug(f"Silently updating {package_name}...")
|
|
239
|
+
|
|
240
|
+
# Run update command with timeout
|
|
241
|
+
result = subprocess.run(
|
|
242
|
+
package_info["update_command"],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
check=True,
|
|
246
|
+
timeout=60, # 1 minute timeout
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Get new version
|
|
250
|
+
new_version = self._get_current_version(package_name)
|
|
251
|
+
|
|
252
|
+
# Save update info
|
|
253
|
+
self._save_update_info(package_name, new_version)
|
|
254
|
+
|
|
255
|
+
if not silent:
|
|
256
|
+
self.logger.info(f"Updated {package_name} to {new_version}")
|
|
257
|
+
else:
|
|
258
|
+
self.logger.info(f"Auto-updated {package_name} to {new_version}")
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"success": True,
|
|
262
|
+
"package": package_name,
|
|
263
|
+
"new_version": new_version,
|
|
264
|
+
"output": result.stdout,
|
|
265
|
+
"updated_at": datetime.now().isoformat(),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
except subprocess.TimeoutExpired:
|
|
269
|
+
error_msg = f"Update timeout for {package_name}"
|
|
270
|
+
self.logger.warning(error_msg)
|
|
271
|
+
return {"success": False, "package": package_name, "error": error_msg}
|
|
272
|
+
except subprocess.CalledProcessError as e:
|
|
273
|
+
error_msg = f"Update failed for {package_name}: {e}"
|
|
274
|
+
if not silent:
|
|
275
|
+
self.logger.error(error_msg)
|
|
276
|
+
else:
|
|
277
|
+
self.logger.debug(error_msg)
|
|
278
|
+
return {
|
|
279
|
+
"success": False,
|
|
280
|
+
"package": package_name,
|
|
281
|
+
"error": str(e),
|
|
282
|
+
"output": e.stdout,
|
|
283
|
+
"stderr": e.stderr,
|
|
284
|
+
}
|
|
285
|
+
except Exception as e:
|
|
286
|
+
error_msg = f"Unexpected error updating {package_name}: {e}"
|
|
287
|
+
if not silent:
|
|
288
|
+
self.logger.error(error_msg)
|
|
289
|
+
else:
|
|
290
|
+
self.logger.debug(error_msg)
|
|
291
|
+
return {"success": False, "package": package_name, "error": str(e)}
|
|
292
|
+
|
|
293
|
+
def _save_update_info(self, package_name: str, version: str):
|
|
294
|
+
"""Save update information to file."""
|
|
295
|
+
try:
|
|
296
|
+
version_file = (
|
|
297
|
+
Path(self.config.database.sqlite_path.parent)
|
|
298
|
+
/ self.critical_packages[package_name]["version_file"]
|
|
299
|
+
)
|
|
300
|
+
version_file.parent.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
|
|
302
|
+
data = {
|
|
303
|
+
"package": package_name,
|
|
304
|
+
"version": version,
|
|
305
|
+
"last_checked": datetime.now().isoformat(),
|
|
306
|
+
"last_updated": datetime.now().isoformat(),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
with open(version_file, "w") as f:
|
|
310
|
+
json.dump(data, f, indent=2)
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
self.logger.debug(f"Failed to save update info: {e}")
|
|
314
|
+
|
|
315
|
+
def _check_and_update_package(self, package_name: str) -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Check if package needs update and update if necessary (for auto-update).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
package_name: Name of package to check
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if package was updated
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
current_version = self._get_current_version(package_name)
|
|
327
|
+
latest_version = self._get_latest_version(package_name)
|
|
328
|
+
|
|
329
|
+
if current_version == "unknown" or latest_version == "unknown":
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
# Simple version comparison
|
|
333
|
+
if current_version != latest_version:
|
|
334
|
+
self.logger.debug(
|
|
335
|
+
f"Package {package_name} needs update: {current_version} -> {latest_version}"
|
|
336
|
+
)
|
|
337
|
+
result = self.update_package(package_name, silent=True)
|
|
338
|
+
return result.get("success", False)
|
|
339
|
+
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
self.logger.debug(f"Error checking/updating {package_name}: {e}")
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
def run_background_update_check(self):
|
|
347
|
+
"""
|
|
348
|
+
Run background update check for all critical packages.
|
|
349
|
+
|
|
350
|
+
This method is designed to be called in a separate thread
|
|
351
|
+
and will not block the main application.
|
|
352
|
+
"""
|
|
353
|
+
# Use lock to prevent multiple simultaneous update checks
|
|
354
|
+
if not self._update_lock.acquire(blocking=False):
|
|
355
|
+
self.logger.debug("Update check already in progress, skipping")
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
if not self.should_check_updates():
|
|
360
|
+
self.logger.debug("Skipping update check - checked recently")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
self.logger.debug("Starting background update check...")
|
|
364
|
+
|
|
365
|
+
# Check and update each critical package
|
|
366
|
+
updated_packages = []
|
|
367
|
+
for package_name in self.critical_packages:
|
|
368
|
+
if self._check_and_update_package(package_name):
|
|
369
|
+
updated_packages.append(package_name)
|
|
370
|
+
|
|
371
|
+
# Save check time
|
|
372
|
+
self._save_check_time()
|
|
373
|
+
|
|
374
|
+
if updated_packages:
|
|
375
|
+
self.logger.info(
|
|
376
|
+
f"Auto-updated packages: {', '.join(updated_packages)}"
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
self.logger.debug("All packages up to date")
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
self.logger.error(f"Error in background update check: {e}")
|
|
383
|
+
finally:
|
|
384
|
+
self._update_lock.release()
|
|
385
|
+
|
|
386
|
+
def start_background_update(self):
|
|
387
|
+
"""
|
|
388
|
+
Start background update check in a separate thread.
|
|
389
|
+
|
|
390
|
+
This is the main method to call when you want to trigger
|
|
391
|
+
automatic updates without blocking the main thread.
|
|
392
|
+
"""
|
|
393
|
+
if not self.auto_update:
|
|
394
|
+
self.logger.debug("Auto-update disabled, skipping background update")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
def update_worker():
|
|
398
|
+
try:
|
|
399
|
+
self.run_background_update_check()
|
|
400
|
+
except Exception as e:
|
|
401
|
+
self.logger.error(f"Background update worker error: {e}")
|
|
402
|
+
|
|
403
|
+
# Start update check in background thread
|
|
404
|
+
update_thread = threading.Thread(
|
|
405
|
+
target=update_worker,
|
|
406
|
+
name="PackageUpdater",
|
|
407
|
+
daemon=True, # Dies when main thread dies
|
|
408
|
+
)
|
|
409
|
+
update_thread.start()
|
|
410
|
+
|
|
411
|
+
self.logger.debug("Started background update check")
|
|
412
|
+
|
|
413
|
+
def force_update_check(self):
|
|
414
|
+
"""
|
|
415
|
+
Force an immediate update check, bypassing time restrictions.
|
|
416
|
+
|
|
417
|
+
Useful for testing or when you want to ensure packages are updated.
|
|
418
|
+
"""
|
|
419
|
+
self.logger.debug("Forcing immediate update check...")
|
|
420
|
+
|
|
421
|
+
# Temporarily override the check frequency
|
|
422
|
+
original_frequency = self.check_frequency_hours
|
|
423
|
+
self.check_frequency_hours = 0
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
self.run_background_update_check()
|
|
427
|
+
finally:
|
|
428
|
+
self.check_frequency_hours = original_frequency
|
|
429
|
+
|
|
430
|
+
def check_all_critical_packages(self) -> List[Dict[str, Any]]:
|
|
431
|
+
"""
|
|
432
|
+
Check all critical packages for updates.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
List of update information for all packages
|
|
436
|
+
"""
|
|
437
|
+
results = []
|
|
438
|
+
|
|
439
|
+
for package_name in self.critical_packages:
|
|
440
|
+
if self.should_check_updates(package_name):
|
|
441
|
+
result = self.check_package_updates(package_name)
|
|
442
|
+
results.append(result)
|
|
443
|
+
else:
|
|
444
|
+
self.logger.debug(f"Skipping {package_name} - checked recently")
|
|
445
|
+
|
|
446
|
+
return results
|
|
447
|
+
|
|
448
|
+
def get_update_summary(self) -> Dict[str, Any]:
|
|
449
|
+
"""
|
|
450
|
+
Get a summary of all package update statuses.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Dictionary with update summary
|
|
454
|
+
"""
|
|
455
|
+
results = self.check_all_critical_packages()
|
|
456
|
+
|
|
457
|
+
total_packages = len(self.critical_packages)
|
|
458
|
+
packages_needing_update = sum(
|
|
459
|
+
1 for r in results if r.get("needs_update", False)
|
|
460
|
+
)
|
|
461
|
+
packages_with_errors = sum(1 for r in results if "error" in r)
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
"total_packages": total_packages,
|
|
465
|
+
"packages_needing_update": packages_needing_update,
|
|
466
|
+
"packages_with_errors": packages_with_errors,
|
|
467
|
+
"last_check": datetime.now().isoformat(),
|
|
468
|
+
"results": results,
|
|
469
|
+
}
|
core/progress.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress tracking utilities for long-running operations.
|
|
3
|
+
|
|
4
|
+
This module provides progress bars and progress tracking for video processing,
|
|
5
|
+
downloads, and other long-running operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.progress import (
|
|
14
|
+
BarColumn,
|
|
15
|
+
Progress,
|
|
16
|
+
SpinnerColumn,
|
|
17
|
+
TextColumn,
|
|
18
|
+
TimeElapsedColumn,
|
|
19
|
+
TimeRemainingColumn,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from core.logger import get_logger
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProgressTracker:
|
|
28
|
+
"""Track progress for long-running operations."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self, description: str, total: Optional[int] = None, verbose: bool = False
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize progress tracker.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
description: Description of the operation
|
|
38
|
+
total: Total number of steps (None for indeterminate)
|
|
39
|
+
verbose: Enable verbose logging
|
|
40
|
+
"""
|
|
41
|
+
self.description = description
|
|
42
|
+
self.total = total
|
|
43
|
+
self.verbose = verbose
|
|
44
|
+
self.logger = get_logger("ProgressTracker", verbose=verbose)
|
|
45
|
+
self.progress = None
|
|
46
|
+
self.task_id = None
|
|
47
|
+
|
|
48
|
+
def __enter__(self):
|
|
49
|
+
"""Start progress tracking."""
|
|
50
|
+
self.progress = Progress(
|
|
51
|
+
SpinnerColumn(),
|
|
52
|
+
TextColumn("[progress.description]{task.description}"),
|
|
53
|
+
BarColumn(),
|
|
54
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
55
|
+
TimeElapsedColumn(),
|
|
56
|
+
TimeRemainingColumn(),
|
|
57
|
+
console=console,
|
|
58
|
+
transient=False,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.progress.start()
|
|
62
|
+
|
|
63
|
+
if self.total:
|
|
64
|
+
self.task_id = self.progress.add_task(self.description, total=self.total)
|
|
65
|
+
else:
|
|
66
|
+
self.task_id = self.progress.add_task(self.description, total=None)
|
|
67
|
+
|
|
68
|
+
self.logger.info(f"Started progress tracking: {self.description}")
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
72
|
+
"""Stop progress tracking."""
|
|
73
|
+
if self.progress:
|
|
74
|
+
self.progress.stop()
|
|
75
|
+
self.logger.info(f"Completed progress tracking: {self.description}")
|
|
76
|
+
|
|
77
|
+
def update(self, advance: int = 1, description: Optional[str] = None):
|
|
78
|
+
"""Update progress."""
|
|
79
|
+
if self.progress and self.task_id is not None:
|
|
80
|
+
if description:
|
|
81
|
+
self.progress.update(self.task_id, description=description)
|
|
82
|
+
self.progress.advance(self.task_id, advance)
|
|
83
|
+
|
|
84
|
+
def set_total(self, total: int):
|
|
85
|
+
"""Set total progress."""
|
|
86
|
+
if self.progress and self.task_id is not None:
|
|
87
|
+
self.progress.update(self.task_id, total=total)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@contextmanager
|
|
91
|
+
def track_progress(
|
|
92
|
+
description: str, total: Optional[int] = None, verbose: bool = False
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Context manager for progress tracking.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
description: Description of the operation
|
|
99
|
+
total: Total number of steps (None for indeterminate)
|
|
100
|
+
verbose: Enable verbose logging
|
|
101
|
+
|
|
102
|
+
Usage:
|
|
103
|
+
with track_progress("Downloading video", total=100) as progress:
|
|
104
|
+
for i in range(100):
|
|
105
|
+
# Do work
|
|
106
|
+
progress.update(1)
|
|
107
|
+
"""
|
|
108
|
+
tracker = ProgressTracker(description, total, verbose)
|
|
109
|
+
with tracker:
|
|
110
|
+
yield tracker
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def progress_decorator(description: str, total_param: Optional[str] = None):
|
|
114
|
+
"""
|
|
115
|
+
Decorator to add progress tracking to functions.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
description: Description of the operation
|
|
119
|
+
total_param: Parameter name that contains the total count
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
@progress_decorator("Processing videos", "video_count")
|
|
123
|
+
def process_videos(self, videos, video_count):
|
|
124
|
+
# Function implementation
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def decorator(func: Callable) -> Callable:
|
|
128
|
+
def wrapper(*args, **kwargs):
|
|
129
|
+
# Get total from parameter if specified
|
|
130
|
+
total = None
|
|
131
|
+
if total_param and total_param in kwargs:
|
|
132
|
+
total = kwargs[total_param]
|
|
133
|
+
elif total_param and len(args) > 0:
|
|
134
|
+
# Try to get from first argument if it's a dict or object
|
|
135
|
+
first_arg = args[0]
|
|
136
|
+
if hasattr(first_arg, total_param):
|
|
137
|
+
total = getattr(first_arg, total_param)
|
|
138
|
+
elif isinstance(first_arg, dict) and total_param in first_arg:
|
|
139
|
+
total = first_arg[total_param]
|
|
140
|
+
|
|
141
|
+
with track_progress(description, total, verbose=True) as progress:
|
|
142
|
+
# Pass progress tracker to the function
|
|
143
|
+
if "progress" not in kwargs:
|
|
144
|
+
kwargs["progress"] = progress
|
|
145
|
+
return func(*args, **kwargs)
|
|
146
|
+
|
|
147
|
+
return wrapper
|
|
148
|
+
|
|
149
|
+
return decorator
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DownloadProgress:
|
|
153
|
+
"""Progress tracking for video downloads."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, total_videos: int, verbose: bool = False):
|
|
156
|
+
"""
|
|
157
|
+
Initialize download progress tracker.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
total_videos: Total number of videos to download
|
|
161
|
+
verbose: Enable verbose logging
|
|
162
|
+
"""
|
|
163
|
+
self.total_videos = total_videos
|
|
164
|
+
self.verbose = verbose
|
|
165
|
+
self.logger = get_logger("DownloadProgress", verbose=verbose)
|
|
166
|
+
self.progress = None
|
|
167
|
+
self.task_id = None
|
|
168
|
+
self.current_video = 0
|
|
169
|
+
|
|
170
|
+
def __enter__(self):
|
|
171
|
+
"""Start download progress tracking."""
|
|
172
|
+
self.progress = Progress(
|
|
173
|
+
SpinnerColumn(),
|
|
174
|
+
TextColumn("[progress.description]{task.description}"),
|
|
175
|
+
BarColumn(),
|
|
176
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
177
|
+
TimeElapsedColumn(),
|
|
178
|
+
TimeRemainingColumn(),
|
|
179
|
+
console=console,
|
|
180
|
+
transient=False,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self.progress.start()
|
|
184
|
+
self.task_id = self.progress.add_task(
|
|
185
|
+
f"Downloading {self.total_videos} videos", total=self.total_videos
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self.logger.info(
|
|
189
|
+
f"Started download progress tracking for {self.total_videos} videos"
|
|
190
|
+
)
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
194
|
+
"""Stop download progress tracking."""
|
|
195
|
+
if self.progress:
|
|
196
|
+
self.progress.stop()
|
|
197
|
+
self.logger.info("Completed download progress tracking")
|
|
198
|
+
|
|
199
|
+
def update_video(self, video_name: str, status: str = "downloading"):
|
|
200
|
+
"""Update progress for current video."""
|
|
201
|
+
self.current_video += 1
|
|
202
|
+
description = (
|
|
203
|
+
f"Downloading video {self.current_video}/{self.total_videos}: {video_name}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if self.progress and self.task_id is not None:
|
|
207
|
+
self.progress.update(self.task_id, advance=1, description=description)
|
|
208
|
+
|
|
209
|
+
self.logger.info(
|
|
210
|
+
f"Video {self.current_video}/{self.total_videos}: {video_name} - {status}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def show_download_progress(total_videos: int, verbose: bool = False):
|
|
215
|
+
"""
|
|
216
|
+
Context manager for download progress tracking.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
total_videos: Total number of videos to download
|
|
220
|
+
verbose: Enable verbose logging
|
|
221
|
+
|
|
222
|
+
Usage:
|
|
223
|
+
with show_download_progress(10) as progress:
|
|
224
|
+
for video in videos:
|
|
225
|
+
# Download video
|
|
226
|
+
progress.update_video(video.name)
|
|
227
|
+
"""
|
|
228
|
+
return DownloadProgress(total_videos, verbose)
|