v
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled

This commit is contained in:
2025-12-28 04:13:11 -08:00
parent 6acb52dfa0
commit edde074cc7
3 changed files with 186 additions and 29 deletions

View File

@@ -113,11 +113,19 @@ class AllDebrid(Provider):
try: try:
from SYS.download import _download_direct_file from SYS.download import _download_direct_file
pipe_progress = None
try:
if isinstance(self.config, dict):
pipe_progress = self.config.get("_pipeline_progress")
except Exception:
pipe_progress = None
dl_res = _download_direct_file( dl_res = _download_direct_file(
unlocked_url, unlocked_url,
Path(output_dir), Path(output_dir),
quiet=quiet, quiet=quiet,
suggested_filename=suggested_name, suggested_filename=suggested_name,
pipeline_progress=pipe_progress,
) )
downloaded_path = getattr(dl_res, "path", None) downloaded_path = getattr(dl_res, "path", None)
if downloaded_path is None: if downloaded_path is None:

View File

@@ -514,6 +514,7 @@ def _download_direct_file(
debug_logger: Optional[DebugLogger] = None, debug_logger: Optional[DebugLogger] = None,
quiet: bool = False, quiet: bool = False,
suggested_filename: Optional[str] = None, suggested_filename: Optional[str] = None,
pipeline_progress: Optional[Any] = None,
) -> DownloadMediaResult: ) -> DownloadMediaResult:
"""Download a direct file (PDF, image, document, etc.) without yt-dlp.""" """Download a direct file (PDF, image, document, etc.) without yt-dlp."""
ensure_directory(output_dir) ensure_directory(output_dir)
@@ -685,6 +686,20 @@ def _download_direct_file(
raise DownloadError("Could not determine filename for URL (no Content-Disposition and no path filename)") raise DownloadError("Could not determine filename for URL (no Content-Disposition and no path filename)")
file_path = _unique_path(output_dir / filename) file_path = _unique_path(output_dir / filename)
# Prefer pipeline transfer bars when a Live UI is active.
use_pipeline_transfer = False
try:
if pipeline_progress is not None and hasattr(pipeline_progress, "update_transfer"):
ui = None
if hasattr(pipeline_progress, "ui_and_pipe_index"):
ui, _ = pipeline_progress.ui_and_pipe_index() # type: ignore[attr-defined]
use_pipeline_transfer = ui is not None
except Exception:
use_pipeline_transfer = False
progress_bar: Optional[ProgressBar] = None
if (not quiet) and (not use_pipeline_transfer):
progress_bar = ProgressBar() progress_bar = ProgressBar()
if not quiet: if not quiet:
@@ -696,11 +711,41 @@ def _download_direct_file(
total_bytes = [0] total_bytes = [0]
last_progress_time = [start_time] last_progress_time = [start_time]
rendered_once = [False] rendered_once = [False]
transfer_started = [False]
def _maybe_begin_transfer(content_length: int) -> None:
if pipeline_progress is None:
return
if transfer_started[0]:
return
try:
total_val: Optional[int] = int(content_length) if isinstance(content_length, int) and content_length > 0 else None
except Exception:
total_val = None
try:
if hasattr(pipeline_progress, "begin_transfer"):
pipeline_progress.begin_transfer(label=str(filename or "download"), total=total_val)
transfer_started[0] = True
except Exception:
return
def progress_callback(bytes_downloaded: int, content_length: int) -> None: def progress_callback(bytes_downloaded: int, content_length: int) -> None:
downloaded_bytes[0] = bytes_downloaded downloaded_bytes[0] = bytes_downloaded
total_bytes[0] = content_length total_bytes[0] = content_length
# Update pipeline transfer bar when present.
try:
if pipeline_progress is not None and hasattr(pipeline_progress, "update_transfer"):
_maybe_begin_transfer(content_length)
total_val: Optional[int] = int(content_length) if isinstance(content_length, int) and content_length > 0 else None
pipeline_progress.update_transfer(
label=str(filename or "download"),
completed=int(bytes_downloaded) if bytes_downloaded is not None else None,
total=total_val,
)
except Exception:
pass
now = time.time() now = time.time()
is_final = bool(content_length > 0 and bytes_downloaded >= content_length) is_final = bool(content_length > 0 and bytes_downloaded >= content_length)
if (not rendered_once[0]) or is_final: if (not rendered_once[0]) or is_final:
@@ -721,6 +766,7 @@ def _download_direct_file(
except Exception: except Exception:
eta_str = None eta_str = None
if progress_bar is not None:
progress_bar.update( progress_bar.update(
downloaded=bytes_downloaded, downloaded=bytes_downloaded,
total=content_length if content_length > 0 else None, total=content_length if content_length > 0 else None,
@@ -736,8 +782,26 @@ def _download_direct_file(
client.download(url, str(file_path), progress_callback=progress_callback) client.download(url, str(file_path), progress_callback=progress_callback)
elapsed = time.time() - start_time elapsed = time.time() - start_time
try:
if progress_bar is not None:
progress_bar.finish() progress_bar.finish()
except Exception:
pass
try:
if pipeline_progress is not None and transfer_started[0] and hasattr(pipeline_progress, "finish_transfer"):
pipeline_progress.finish_transfer(label=str(filename or "download"))
except Exception:
pass
try:
if progress_bar is not None:
avg_speed_str = progress_bar.format_bytes(downloaded_bytes[0] / elapsed if elapsed > 0 else 0) + "/s" avg_speed_str = progress_bar.format_bytes(downloaded_bytes[0] / elapsed if elapsed > 0 else 0) + "/s"
else:
avg_speed_str = f"{(downloaded_bytes[0] / elapsed if elapsed > 0 else 0):.1f} B/s"
except Exception:
avg_speed_str = ""
if not quiet: if not quiet:
debug(f"✓ Downloaded in {elapsed:.1f}s at {avg_speed_str}") debug(f"✓ Downloaded in {elapsed:.1f}s at {avg_speed_str}")
@@ -797,9 +861,15 @@ def _download_direct_file(
except (httpx.HTTPError, httpx.RequestError) as exc: except (httpx.HTTPError, httpx.RequestError) as exc:
try: try:
if progress_bar is not None:
progress_bar.finish() progress_bar.finish()
except Exception: except Exception:
pass pass
try:
if pipeline_progress is not None and transfer_started[0] and hasattr(pipeline_progress, "finish_transfer"):
pipeline_progress.finish_transfer(label=str(filename or "download"))
except Exception:
pass
log(f"Download error: {exc}", file=sys.stderr) log(f"Download error: {exc}", file=sys.stderr)
if debug_logger is not None: if debug_logger is not None:
debug_logger.write_record( debug_logger.write_record(
@@ -809,9 +879,15 @@ def _download_direct_file(
raise DownloadError(f"Failed to download {url}: {exc}") from exc raise DownloadError(f"Failed to download {url}: {exc}") from exc
except Exception as exc: except Exception as exc:
try: try:
if progress_bar is not None:
progress_bar.finish() progress_bar.finish()
except Exception: except Exception:
pass pass
try:
if pipeline_progress is not None and transfer_started[0] and hasattr(pipeline_progress, "finish_transfer"):
pipeline_progress.finish_transfer(label=str(filename or "download"))
except Exception:
pass
log(f"Error downloading file: {exc}", file=sys.stderr) log(f"Error downloading file: {exc}", file=sys.stderr)
if debug_logger is not None: if debug_logger is not None:
debug_logger.write_record( debug_logger.write_record(

View File

@@ -38,7 +38,7 @@ class Download_File(Cmdlet):
super().__init__( super().__init__(
name="download-file", name="download-file",
summary="Download files via HTTP or provider handlers", summary="Download files via HTTP or provider handlers",
usage="download-file <url> [-path DIR] [options] OR @N | download-file [-path DIR] [options]", usage="download-file <url> [-path DIR] [options] OR @N | download-file [-path DIR|DIR] [options]",
alias=["dl-file", "download-http"], alias=["dl-file", "download-http"],
arg=[ arg=[
SharedArgs.URL, SharedArgs.URL,
@@ -690,7 +690,12 @@ class Download_File(Cmdlet):
downloaded_count += 1 downloaded_count += 1
continue continue
result_obj = _download_direct_file(str(url), final_output_dir, quiet=quiet_mode) result_obj = _download_direct_file(
str(url),
final_output_dir,
quiet=quiet_mode,
pipeline_progress=progress,
)
downloaded_path = self._path_from_download_result(result_obj) downloaded_path = self._path_from_download_result(result_obj)
self._emit_local_file( self._emit_local_file(
@@ -829,8 +834,19 @@ class Download_File(Cmdlet):
if not magnet_name: if not magnet_name:
magnet_name = str(get_field(item, "detail") or "").strip() or None magnet_name = str(get_field(item, "detail") or "").strip() or None
if magnet_name: magnet_dir_name = _sf(str(magnet_name)) if magnet_name else ""
output_dir = Path(output_dir) / _sf(str(magnet_name))
# If user already chose -path that ends with the magnet folder name,
# don't create a duplicate nested folder.
try:
base_tail = str(Path(output_dir).name or "")
except Exception:
base_tail = ""
base_tail_norm = _sf(base_tail).lower() if base_tail.strip() else ""
magnet_dir_norm = magnet_dir_name.lower() if magnet_dir_name else ""
if magnet_dir_name and (not base_tail_norm or base_tail_norm != magnet_dir_norm):
output_dir = Path(output_dir) / magnet_dir_name
relpath = None relpath = None
if isinstance(md, dict): if isinstance(md, dict):
@@ -840,6 +856,16 @@ class Download_File(Cmdlet):
if relpath: if relpath:
parts = [p for p in str(relpath).replace("\\", "/").split("/") if p and p not in {".", ".."}] parts = [p for p in str(relpath).replace("\\", "/").split("/") if p and p not in {".", ".."}]
# If the provider relpath already includes the magnet folder name as a
# root directory (common), strip it to prevent double nesting.
if magnet_dir_name and parts:
try:
if _sf(parts[0]).lower() == magnet_dir_norm:
parts = parts[1:]
except Exception:
pass
# relpath includes the filename; only join parent directories. # relpath includes the filename; only join parent directories.
for part in parts[:-1]: for part in parts[:-1]:
output_dir = Path(output_dir) / _sf(part) output_dir = Path(output_dir) / _sf(part)
@@ -927,6 +953,7 @@ class Download_File(Cmdlet):
final_output_dir, final_output_dir,
quiet=quiet_mode, quiet=quiet_mode,
suggested_filename=suggested_name, suggested_filename=suggested_name,
pipeline_progress=progress,
) )
downloaded_path = self._path_from_download_result(result_obj) downloaded_path = self._path_from_download_result(result_obj)
@@ -980,15 +1007,53 @@ class Download_File(Cmdlet):
def _run_impl(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: def _run_impl(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Main download implementation for direct HTTP files.""" """Main download implementation for direct HTTP files."""
progress = PipelineProgress(pipeline_context) progress = PipelineProgress(pipeline_context)
prev_progress = None
had_progress_key = False
try: try:
debug("Starting download-file") debug("Starting download-file")
# Allow providers to tap into the active PipelineProgress (optional).
try:
if isinstance(config, dict):
had_progress_key = "_pipeline_progress" in config
prev_progress = config.get("_pipeline_progress")
config["_pipeline_progress"] = progress
except Exception:
pass
# Parse arguments # Parse arguments
parsed = parse_cmdlet_args(args, self) parsed = parse_cmdlet_args(args, self)
raw_url = self._normalize_urls(parsed) raw_url = self._normalize_urls(parsed)
piped_items = self._collect_piped_items_if_no_urls(result, raw_url) piped_items = self._collect_piped_items_if_no_urls(result, raw_url)
had_piped_input = False
try:
if isinstance(result, list):
had_piped_input = bool(result)
else:
had_piped_input = bool(result)
except Exception:
had_piped_input = False
# UX: In piped mode, allow a single positional arg to be the destination directory.
# Example: @1-4 | download-file "C:\\Users\\Me\\Downloads\\yoyo"
if had_piped_input and raw_url and len(raw_url) == 1 and (not parsed.get("path")) and (not parsed.get("output")):
candidate = str(raw_url[0] or "").strip()
low = candidate.lower()
looks_like_url = low.startswith(("http://", "https://", "ftp://"))
looks_like_provider = low.startswith(("magnet:", "alldebrid:", "hydrus:", "ia:", "internetarchive:"))
looks_like_windows_path = (
(len(candidate) >= 2 and candidate[1] == ":")
or candidate.startswith("\\\\")
or candidate.startswith("\\")
or candidate.endswith(("\\", "/"))
)
if (not looks_like_url) and (not looks_like_provider) and looks_like_windows_path:
parsed["path"] = candidate
raw_url = []
piped_items = self._collect_piped_items_if_no_urls(result, raw_url)
if not raw_url and not piped_items: if not raw_url and not piped_items:
log("No url or piped items to download", file=sys.stderr) log("No url or piped items to download", file=sys.stderr)
return 1 return 1
@@ -1055,6 +1120,14 @@ class Download_File(Cmdlet):
return 1 return 1
finally: finally:
try:
if isinstance(config, dict):
if had_progress_key:
config["_pipeline_progress"] = prev_progress
else:
config.pop("_pipeline_progress", None)
except Exception:
pass
progress.close_local_ui(force_complete=True) progress.close_local_ui(force_complete=True)
def _resolve_output_dir(self, parsed: Dict[str, Any], config: Dict[str, Any]) -> Optional[Path]: def _resolve_output_dir(self, parsed: Dict[str, Any], config: Dict[str, Any]) -> Optional[Path]: