diff --git a/youtube_cli/main.py b/youtube_cli/main.py index c383d33..a7bd22d 100644 --- a/youtube_cli/main.py +++ b/youtube_cli/main.py @@ -825,9 +825,7 @@ class YouTubeCLI: else: logger.info("Using 1080p quality by default") - # Run command and let yt-dlp handle progress natively - # Removed timeout to support long-running downloads in queue - # Use a pipe to capture progress and support cancellation + # Run command with progress bar process = subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -836,12 +834,71 @@ class YouTubeCLI: text=True, ) + # Rich progress bar for downloads + from rich.progress import ( + BarColumn, + DownloadColumn, + Progress, + TextColumn, + TimeRemainingColumn, + TransferSpeedColumn, + ) + + progress = Progress( + TextColumn("[bold blue]{task.description}"), + BarColumn(bar_width=40), + "[progress.percentage]{task.percentage:>3.1f}%", + "•", + DownloadColumn(), + "•", + TransferSpeedColumn(), + "•", + TimeRemainingColumn(), + ) + + downloaded_bytes = 0 + total_bytes = 0 + download_task = None + output_lines = [] + try: - stdout, _ = process.communicate(timeout=None) # No timeout + with progress: + for line in process.stdout: + output_lines.append(line) + + # Parse progress line: "[download] 5.0% of ~ 10.00MiB at 1.23MiB/s ETA 00:08" + import re as re_mod + + progress_match = re_mod.search( + r"\[download\]\s+(\d+\.?\d*)%\s+of\s+[~]?\s*([\d.]+)\s*(B|KiB|MiB|GiB)", + line, + ) + if progress_match: + pct = float(progress_match.group(1)) + size_str = progress_match.group(2) + size_unit = progress_match.group(3) + total_bytes = self._parse_size(size_str, size_unit) + downloaded_bytes = int(total_bytes * pct / 100) + + if download_task is None: + download_task = progress.add_task( + "Downloading...", + total=total_bytes if total_bytes > 0 else None, + ) + progress.update( + download_task, + completed=downloaded_bytes, + total=total_bytes if total_bytes > 0 else None, + ) + elif "[download]" in line and "[" not in line.split("[download]")[1].split()[0]: + pass + elif "[download] Deprecated commandline" in line: + pass + result = subprocess.CompletedProcess( - cmd, process.returncode, stdout, "" + cmd, process.returncode, "".join(output_lines), "" ) - except subprocess.TimeoutExpired: + except KeyboardInterrupt: process.kill() logger.warning("Download cancelled by user") return False @@ -855,7 +912,6 @@ class YouTubeCLI: # Add video to archive for tracking try: - # Extract video ID from URL for archive import re video_id = None @@ -880,8 +936,6 @@ class YouTubeCLI: if result.stdout: logger.error(f"Error details: {result.stdout}") - # Try to check if we have a different problem - # Check for specific JavaScript challenge errors and recommend solutions if ( "Solving JS challenges" in result.stdout or "challenge solving failed" in result.stdout @@ -895,6 +949,11 @@ class YouTubeCLI: except Exception as e: logger.error(f"Error during download: {str(e)}") + def _parse_size(self, size_str: str, unit: str) -> int: + """Parse size string with unit to bytes.""" + multipliers = {"B": 1, "KiB": 1024, "MiB": 1024**2, "GiB": 1024**3} + return int(float(size_str) * multipliers.get(unit, 1)) + def download_playlist( self, url,