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.
Files changed (59) hide show
  1. analytics/__init__.py +1 -0
  2. analytics/reporter.py +497 -0
  3. cli/__init__.py +1 -0
  4. cli/app.py +147 -0
  5. cli/audio.py +129 -0
  6. cli/cli_analytics.py +320 -0
  7. cli/cli_utils.py +282 -0
  8. cli/error_handlers.py +122 -0
  9. cli/files.py +299 -0
  10. cli/update.py +325 -0
  11. cli/video.py +823 -0
  12. cli/worker.py +615 -0
  13. core/__init__.py +1 -0
  14. core/analytics_dashboard.py +368 -0
  15. core/base.py +303 -0
  16. core/base_service.py +69 -0
  17. core/config.py +345 -0
  18. core/database_service.py +116 -0
  19. core/decorators.py +263 -0
  20. core/error_handler.py +210 -0
  21. core/file_tracker.py +254 -0
  22. core/interactive_cli.py +366 -0
  23. core/interfaces.py +166 -0
  24. core/job_queue.py +437 -0
  25. core/logger.py +79 -0
  26. core/package_updater.py +469 -0
  27. core/progress.py +228 -0
  28. core/service_factory.py +295 -0
  29. core/streaming.py +299 -0
  30. core/worker.py +765 -0
  31. database/__init__.py +1 -0
  32. database/connection.py +265 -0
  33. database/metadata.py +516 -0
  34. database/models.py +288 -0
  35. database/repository.py +592 -0
  36. database/transcription_storage.py +219 -0
  37. modules/__init__.py +1 -0
  38. modules/audio/__init__.py +5 -0
  39. modules/audio/converter.py +197 -0
  40. modules/video/__init__.py +16 -0
  41. modules/video/converter.py +191 -0
  42. modules/video/fallback_extractor.py +334 -0
  43. modules/video/services/__init__.py +18 -0
  44. modules/video/services/audio_extraction_service.py +274 -0
  45. modules/video/services/download_service.py +852 -0
  46. modules/video/services/metadata_service.py +190 -0
  47. modules/video/services/playlist_service.py +445 -0
  48. modules/video/services/transcription_service.py +491 -0
  49. modules/video/transcription_service.py +385 -0
  50. modules/video/youtube_api.py +397 -0
  51. spatelier/__init__.py +33 -0
  52. spatelier-0.3.0.dist-info/METADATA +260 -0
  53. spatelier-0.3.0.dist-info/RECORD +59 -0
  54. spatelier-0.3.0.dist-info/WHEEL +5 -0
  55. spatelier-0.3.0.dist-info/entry_points.txt +2 -0
  56. spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
  57. spatelier-0.3.0.dist-info/top_level.txt +7 -0
  58. utils/__init__.py +1 -0
  59. utils/helpers.py +250 -0
@@ -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)