feat: add rich progress bars to video downloads
Parse yt-dlp progress output and display real-time progress bar with percentage, downloaded size, transfer speed, and ETA using rich library.
This commit is contained in:
parent
3adafe8bbd
commit
7432ad644c
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
# Rich progress bar for downloads
|
||||||
stdout, _ = process.communicate(timeout=None) # No timeout
|
from rich.progress import (
|
||||||
result = subprocess.CompletedProcess(
|
BarColumn,
|
||||||
cmd, process.returncode, stdout, ""
|
DownloadColumn,
|
||||||
|
Progress,
|
||||||
|
TextColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
TransferSpeedColumn,
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
|
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:
|
||||||
|
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, "".join(output_lines), ""
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user