From edde074cc77ef9543e354cd9fefd284d02351ed2 Mon Sep 17 00:00:00 2001 From: Nose Date: Sun, 28 Dec 2025 04:13:11 -0800 Subject: [PATCH] v --- Provider/alldebrid.py | 8 +++ SYS/download.py | 98 ++++++++++++++++++++++++++++++++---- cmdlet/download_file.py | 109 +++++++++++++++++++++++++++++++++------- 3 files changed, 186 insertions(+), 29 deletions(-) diff --git a/Provider/alldebrid.py b/Provider/alldebrid.py index d1c05fa..e588cc5 100644 --- a/Provider/alldebrid.py +++ b/Provider/alldebrid.py @@ -113,11 +113,19 @@ class AllDebrid(Provider): try: 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( unlocked_url, Path(output_dir), quiet=quiet, suggested_filename=suggested_name, + pipeline_progress=pipe_progress, ) downloaded_path = getattr(dl_res, "path", None) if downloaded_path is None: diff --git a/SYS/download.py b/SYS/download.py index f567d64..ae9bb14 100644 --- a/SYS/download.py +++ b/SYS/download.py @@ -514,6 +514,7 @@ def _download_direct_file( debug_logger: Optional[DebugLogger] = None, quiet: bool = False, suggested_filename: Optional[str] = None, + pipeline_progress: Optional[Any] = None, ) -> DownloadMediaResult: """Download a direct file (PDF, image, document, etc.) without yt-dlp.""" ensure_directory(output_dir) @@ -685,7 +686,21 @@ def _download_direct_file( raise DownloadError("Could not determine filename for URL (no Content-Disposition and no path filename)") file_path = _unique_path(output_dir / filename) - progress_bar = ProgressBar() + + # 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() if not quiet: debug(f"Direct download: {filename}") @@ -696,11 +711,41 @@ def _download_direct_file( total_bytes = [0] last_progress_time = [start_time] 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: downloaded_bytes[0] = bytes_downloaded 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() is_final = bool(content_length > 0 and bytes_downloaded >= content_length) if (not rendered_once[0]) or is_final: @@ -721,12 +766,13 @@ def _download_direct_file( except Exception: eta_str = None - progress_bar.update( - downloaded=bytes_downloaded, - total=content_length if content_length > 0 else None, - label=str(filename or "download"), - file=sys.stderr, - ) + if progress_bar is not None: + progress_bar.update( + downloaded=bytes_downloaded, + total=content_length if content_length > 0 else None, + label=str(filename or "download"), + file=sys.stderr, + ) rendered_once[0] = True @@ -736,8 +782,26 @@ def _download_direct_file( client.download(url, str(file_path), progress_callback=progress_callback) elapsed = time.time() - start_time - progress_bar.finish() - avg_speed_str = progress_bar.format_bytes(downloaded_bytes[0] / elapsed if elapsed > 0 else 0) + "/s" + + try: + if progress_bar is not None: + 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" + 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: debug(f"✓ Downloaded in {elapsed:.1f}s at {avg_speed_str}") @@ -797,7 +861,13 @@ def _download_direct_file( except (httpx.HTTPError, httpx.RequestError) as exc: try: - progress_bar.finish() + if progress_bar is not None: + 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 log(f"Download error: {exc}", file=sys.stderr) @@ -809,7 +879,13 @@ def _download_direct_file( raise DownloadError(f"Failed to download {url}: {exc}") from exc except Exception as exc: try: - progress_bar.finish() + if progress_bar is not None: + 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 log(f"Error downloading file: {exc}", file=sys.stderr) diff --git a/cmdlet/download_file.py b/cmdlet/download_file.py index 04bb00a..7fa3230 100644 --- a/cmdlet/download_file.py +++ b/cmdlet/download_file.py @@ -38,7 +38,7 @@ class Download_File(Cmdlet): super().__init__( name="download-file", summary="Download files via HTTP or provider handlers", - usage="download-file [-path DIR] [options] OR @N | download-file [-path DIR] [options]", + usage="download-file [-path DIR] [options] OR @N | download-file [-path DIR|DIR] [options]", alias=["dl-file", "download-http"], arg=[ SharedArgs.URL, @@ -690,7 +690,12 @@ class Download_File(Cmdlet): downloaded_count += 1 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) self._emit_local_file( @@ -829,25 +834,46 @@ class Download_File(Cmdlet): if not magnet_name: magnet_name = str(get_field(item, "detail") or "").strip() or None - if magnet_name: - output_dir = Path(output_dir) / _sf(str(magnet_name)) + magnet_dir_name = _sf(str(magnet_name)) if magnet_name else "" - relpath = None - if isinstance(md, dict): - relpath = md.get("relpath") - if not relpath and isinstance(md.get("file"), dict): - relpath = md["file"].get("_relpath") + # 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 relpath: - parts = [p for p in str(relpath).replace("\\", "/").split("/") if p and p not in {".", ".."}] - # relpath includes the filename; only join parent directories. - for part in parts[:-1]: - output_dir = Path(output_dir) / _sf(part) + if magnet_dir_name and (not base_tail_norm or base_tail_norm != magnet_dir_norm): + output_dir = Path(output_dir) / magnet_dir_name - try: - Path(output_dir).mkdir(parents=True, exist_ok=True) - except Exception: - output_dir = final_output_dir + relpath = None + if isinstance(md, dict): + relpath = md.get("relpath") + if not relpath and isinstance(md.get("file"), dict): + relpath = md["file"].get("_relpath") + + if relpath: + 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. + for part in parts[:-1]: + output_dir = Path(output_dir) / _sf(part) + + try: + Path(output_dir).mkdir(parents=True, exist_ok=True) + except Exception: + output_dir = final_output_dir except Exception: output_dir = final_output_dir @@ -927,6 +953,7 @@ class Download_File(Cmdlet): final_output_dir, quiet=quiet_mode, suggested_filename=suggested_name, + pipeline_progress=progress, ) 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: """Main download implementation for direct HTTP files.""" progress = PipelineProgress(pipeline_context) + prev_progress = None + had_progress_key = False try: 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 parsed = parse_cmdlet_args(args, self) raw_url = self._normalize_urls(parsed) 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: log("No url or piped items to download", file=sys.stderr) return 1 @@ -1055,6 +1120,14 @@ class Download_File(Cmdlet): return 1 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) def _resolve_output_dir(self, parsed: Dict[str, Any], config: Dict[str, Any]) -> Optional[Path]: