Merge pull request 'Fix #3: Add progress bars to downloads' (#14) from fix/issue-3 into main

Reviewed-on: https://git.example.com/jarianc/youtube-cli/pulls/14
This commit is contained in:
Jarian Cottingham 2026-07-05 01:51:29 -05:00
commit 6ae8f3befc

View File

@ -825,9 +825,7 @@ class YouTubeCLI:
else: else:
logger.info("Using 1080p quality by default") logger.info("Using 1080p quality by default")
# Run command and let yt-dlp handle progress natively # Run command with progress bar
# Removed timeout to support long-running downloads in queue
# Use a pipe to capture progress and support cancellation
process = subprocess.Popen( process = subprocess.Popen(
cmd, cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -836,12 +834,71 @@ class YouTubeCLI:
text=True, 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: 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( result = subprocess.CompletedProcess(
cmd, process.returncode, stdout, "" cmd, process.returncode, "".join(output_lines), ""
) )
except subprocess.TimeoutExpired: except KeyboardInterrupt:
process.kill() process.kill()
logger.warning("Download cancelled by user") logger.warning("Download cancelled by user")
return False return False
@ -855,7 +912,6 @@ class YouTubeCLI:
# Add video to archive for tracking # Add video to archive for tracking
try: try:
# Extract video ID from URL for archive
import re import re
video_id = None video_id = None
@ -880,8 +936,6 @@ class YouTubeCLI:
if result.stdout: if result.stdout:
logger.error(f"Error details: {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 ( if (
"Solving JS challenges" in result.stdout "Solving JS challenges" in result.stdout
or "challenge solving failed" in result.stdout or "challenge solving failed" in result.stdout
@ -895,6 +949,11 @@ class YouTubeCLI:
except Exception as e: except Exception as e:
logger.error(f"Error during download: {str(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( def download_playlist(
self, self,
url, url,