This commit is contained in:
nose
2025-12-20 23:57:44 -08:00
parent b75faa49a2
commit 8ca5783970
39 changed files with 4294 additions and 1722 deletions

View File

@@ -44,6 +44,7 @@ except ImportError:
extract_ytdlp_tags = None
_EXTRACTOR_CACHE: List[Any] | None = None
_YTDLP_PROGRESS = ProgressBar()
def _ensure_yt_dlp_ready() -> None:
@@ -58,14 +59,16 @@ def _progress_callback(status: Dict[str, Any]) -> None:
"""Simple progress callback using logger."""
event = status.get("status")
if event == "downloading":
percent = status.get("_percent_str", "?")
speed = status.get("_speed_str", "?")
eta = status.get("_eta_str", "?")
sys.stdout.write(f"\r[download] {percent} at {speed} ETA {eta} ")
sys.stdout.flush()
downloaded = status.get("downloaded_bytes")
total = status.get("total_bytes") or status.get("total_bytes_estimate")
_YTDLP_PROGRESS.update(
downloaded=int(downloaded or 0),
total=int(total) if total else None,
label="download",
file=sys.stderr,
)
elif event == "finished":
sys.stdout.write("\r" + " " * 70 + "\r")
sys.stdout.flush()
_YTDLP_PROGRESS.finish()
debug(f"✓ Download finished: {status.get('filename')}")
elif event in ("postprocessing", "processing"):
debug(f"Post-processing: {status.get('postprocessor')}")
@@ -632,13 +635,17 @@ def _download_direct_file(
downloaded_bytes = [0]
total_bytes = [0]
last_progress_time = [start_time]
rendered_once = [False]
def progress_callback(bytes_downloaded: int, content_length: int) -> None:
downloaded_bytes[0] = bytes_downloaded
total_bytes[0] = content_length
now = time.time()
if now - last_progress_time[0] < 0.5:
is_final = bool(content_length > 0 and bytes_downloaded >= content_length)
if (not rendered_once[0]) or is_final:
pass
elif now - last_progress_time[0] < 0.5:
return
elapsed = now - start_time
@@ -654,26 +661,14 @@ def _download_direct_file(
except Exception:
eta_str = None
speed_str = progress_bar.format_bytes(speed) + "/s"
progress_line = progress_bar.format_progress(
percent_str=f"{percent:.1f}%",
progress_bar.update(
downloaded=bytes_downloaded,
total=content_length if content_length > 0 else None,
speed_str=speed_str,
eta_str=eta_str,
label=str(filename or "download"),
file=sys.stderr,
)
if not quiet:
try:
if getattr(sys.stderr, "isatty", lambda: False)():
sys.stderr.write("\r" + progress_line + " ")
sys.stderr.flush()
else:
# Non-interactive: print occasional progress lines.
log(progress_line, file=sys.stderr)
except Exception:
pass
rendered_once[0] = True
last_progress_time[0] = now
@@ -681,14 +676,7 @@ def _download_direct_file(
client.download(url, str(file_path), progress_callback=progress_callback)
elapsed = time.time() - start_time
# Clear in-place progress bar.
if not quiet:
try:
if getattr(sys.stderr, "isatty", lambda: False)():
sys.stderr.write("\r" + (" " * 140) + "\r")
sys.stderr.flush()
except Exception:
pass
progress_bar.finish()
avg_speed_str = progress_bar.format_bytes(downloaded_bytes[0] / elapsed if elapsed > 0 else 0) + "/s"
if not quiet:
debug(f"✓ Downloaded in {elapsed:.1f}s at {avg_speed_str}")
@@ -742,6 +730,10 @@ def _download_direct_file(
)
except (httpx.HTTPError, httpx.RequestError) as exc:
try:
progress_bar.finish()
except Exception:
pass
log(f"Download error: {exc}", file=sys.stderr)
if debug_logger is not None:
debug_logger.write_record(
@@ -750,6 +742,10 @@ def _download_direct_file(
)
raise DownloadError(f"Failed to download {url}: {exc}") from exc
except Exception as exc:
try:
progress_bar.finish()
except Exception:
pass
log(f"Error downloading file: {exc}", file=sys.stderr)
if debug_logger is not None:
debug_logger.write_record(

View File

@@ -5,6 +5,8 @@ import inspect
import threading
from pathlib import Path
from rich_display import console_for
_DEBUG_ENABLED = False
_thread_local = threading.local()
@@ -56,6 +58,80 @@ def debug(*args, **kwargs) -> None:
# Use the same logic as log()
log(*args, **kwargs)
def debug_inspect(
obj,
*,
title: str | None = None,
file=None,
methods: bool = False,
docs: bool = False,
private: bool = False,
dunder: bool = False,
sort: bool = True,
all: bool = False,
value: bool = True,
) -> None:
"""Rich-inspect an object when debug logging is enabled.
Uses the same stream / quiet-mode behavior as `debug()` and prepends a
`[file.function]` prefix when debug is enabled.
"""
if not _DEBUG_ENABLED:
return
# Mirror debug() quiet-mode guard.
try:
stderr_name = getattr(sys.stderr, "name", "")
if "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name):
return
except Exception:
pass
# Resolve destination stream.
stream = get_thread_stream()
if stream is not None:
file = stream
elif file is None:
file = sys.stderr
# Compute caller prefix (same as log()).
prefix = None
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None:
caller_frame = frame.f_back
try:
file_name = Path(caller_frame.f_code.co_filename).stem
func_name = caller_frame.f_code.co_name
prefix = f"[{file_name}.{func_name}]"
finally:
del caller_frame
if frame is not None:
del frame
# Render.
from rich import inspect as rich_inspect
console = console_for(file)
# If the caller provides a title, treat it as authoritative.
# Only fall back to the automatic [file.func] prefix when no title is supplied.
effective_title = title
if not effective_title and prefix:
effective_title = prefix
rich_inspect(
obj,
console=console,
title=effective_title,
methods=methods,
docs=docs,
private=private,
dunder=dunder,
sort=sort,
all=all,
value=value,
)
def log(*args, **kwargs) -> None:
"""Print with automatic file.function prefix.
@@ -71,12 +147,18 @@ def log(*args, **kwargs) -> None:
# Get the calling frame
frame = inspect.currentframe()
if frame is None:
print(*args, **kwargs)
file = kwargs.pop("file", sys.stdout)
sep = kwargs.pop("sep", " ")
end = kwargs.pop("end", "\n")
console_for(file).print(*args, sep=sep, end=end)
return
caller_frame = frame.f_back
if caller_frame is None:
print(*args, **kwargs)
file = kwargs.pop("file", sys.stdout)
sep = kwargs.pop("sep", " ")
end = kwargs.pop("end", "\n")
console_for(file).print(*args, sep=sep, end=end)
return
try:
@@ -93,12 +175,15 @@ def log(*args, **kwargs) -> None:
# Set default to stdout if not specified
elif 'file' not in kwargs:
kwargs['file'] = sys.stdout
file = kwargs.pop("file", sys.stdout)
sep = kwargs.pop("sep", " ")
end = kwargs.pop("end", "\n")
if add_prefix:
prefix = f"[{file_name}.{func_name}]"
print(prefix, *args, **kwargs)
console_for(file).print(prefix, *args, sep=sep, end=end)
else:
print(*args, **kwargs)
console_for(file).print(*args, sep=sep, end=end)
finally:
del frame
del caller_frame

View File

@@ -1,102 +1,22 @@
#!/usr/bin/env python3
"""Text-based progress bar utilities for consistent display across all downloads."""
"""Rich-only progress helpers.
These functions preserve the legacy call signatures used around the codebase,
but all rendering is performed via Rich (no ASCII progress bars).
"""
from __future__ import annotations
import sys
from SYS.logger import log
from models import ProgressBar
def format_progress_bar(current: int, total: int, width: int = 40, label: str = "") -> str:
"""Create a text-based progress bar.
Args:
current: Current progress (bytes/items)
total: Total to complete (bytes/items)
width: Width of the bar in characters (default 40)
label: Optional label prefix
Returns:
Formatted progress bar string
Examples:
format_progress_bar(50, 100)
# Returns: "[████████████████░░░░░░░░░░░░░░░░░░░░] 50.0%"
format_progress_bar(256*1024*1024, 1024*1024*1024, label="download.zip")
# Returns: "download.zip: [████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 25.0%"
"""
if total <= 0:
percentage = 0
filled = 0
else:
percentage = (current / total) * 100
filled = int((current / total) * width)
bar = "" * filled + "" * (width - filled)
pct_str = f"{percentage:.1f}%"
if label:
result = f"{label}: [{bar}] {pct_str}"
else:
result = f"[{bar}] {pct_str}"
return result
def format_size(bytes_val: float) -> str:
"""Format bytes to human-readable size."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_val < 1024:
return f"{bytes_val:.2f} {unit}"
bytes_val /= 1024
return f"{bytes_val:.2f} PB"
def format_download_status(filename: str, current: int, total: int, speed: float = 0) -> str:
"""Format download status with progress bar and details."""
bar = format_progress_bar(current, total, width=30)
size_current = format_size(current)
size_total = format_size(total)
if speed > 0:
speed_str = f" @ {format_size(speed)}/s"
else:
speed_str = ""
return f"{bar} ({size_current} / {size_total}{speed_str})"
_BAR = ProgressBar()
def print_progress(filename: str, current: int, total: int, speed: float = 0, end: str = "\r") -> None:
"""Print download progress to stderr (doesn't interfere with piped output)."""
status = format_download_status(filename, current, total, speed)
print(status, file=sys.stderr, end=end, flush=True)
_BAR.update(downloaded=int(current), total=int(total) if total else None, label=str(filename or "progress"), file=sys.stderr)
def print_final_progress(filename: str, total: int, elapsed: float) -> None:
"""Print final progress line (100%) with time elapsed."""
bar = format_progress_bar(total, total, width=30)
size_str = format_size(total)
if elapsed < 60:
time_str = f"{elapsed:.1f}s"
elif elapsed < 3600:
minutes = elapsed / 60
time_str = f"{minutes:.1f}m"
else:
hours = elapsed / 3600
time_str = f"{hours:.2f}h"
print(f"{bar} ({size_str}) - {time_str}", file=sys.stderr, flush=True)
if __name__ == "__main__":
import time
log("Progress Bar Demo:", file=sys.stderr)
for i in range(101):
print_progress("demo.bin", i * 10 * 1024 * 1024, 1024 * 1024 * 1024)
time.sleep(0.02)
print_final_progress("demo.bin", 1024 * 1024 * 1024, 2.0)
log()
_BAR.finish()

View File

@@ -124,7 +124,7 @@ def create_tags_sidecar(file_path: Path, tags: set) -> None:
try:
with open(tags_path, 'w', encoding='utf-8') as f:
for tag in sorted(tags):
f.write(f"{tag}\n")
f.write(f"{str(tag).strip().lower()}\n")
except Exception as e:
raise RuntimeError(f"Failed to create tags sidecar {tags_path}: {e}") from e