dfdf
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
102
SYS/progress.py
102
SYS/progress.py
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user