This commit is contained in:
2026-03-25 22:39:30 -07:00
parent c31402c8f1
commit 562acd809c
46 changed files with 2367 additions and 1868 deletions

View File

@@ -19,11 +19,17 @@ from contextlib import AbstractContextManager, nullcontext
from API.HTTP import _download_direct_file
from SYS.models import DownloadError, DownloadOptions, DownloadMediaResult
from SYS.logger import log, debug, is_debug_enabled
from SYS.payload_builders import build_file_result_payload, build_table_result_payload
from SYS.pipeline_progress import PipelineProgress
from SYS.result_table import Table
from SYS.rich_display import stderr_console as get_stderr_console
from SYS import pipeline as pipeline_context
from SYS.metadata import normalize_urls as normalize_url_list
from SYS.selection_builder import (
extract_selection_fields,
extract_urls_from_selection_args,
selection_args_have_url,
)
from SYS.utils import sha256_file
from tool.ytdlp import (
@@ -57,6 +63,7 @@ build_pipeline_preview = sh.build_pipeline_preview
# URI scheme prefixes owned by AllDebrid (magic-link and emoji shorthand).
# Defined once here so every method in this file references the same constant.
_ALLDEBRID_PREFIXES: tuple[str, ...] = ("alldebrid:", "alldebrid🧲")
_FORMAT_INDEX_RE = re.compile(r"^\s*#?\d+\s*$")
class Download_File(Cmdlet):
@@ -1008,9 +1015,7 @@ class Download_File(Cmdlet):
formats_cache: Dict[str, Optional[List[Dict[str, Any]]]],
ytdlp_tool: YtDlpTool,
) -> Optional[str]:
import re
if not query_format or not re.match(r"^\s*#?\d+\s*$", str(query_format)):
if not query_format or not _FORMAT_INDEX_RE.match(str(query_format)):
return None
try:
@@ -1221,22 +1226,24 @@ class Download_File(Cmdlet):
except Exception:
pass
row: Dict[str, Any] = {
"table": "download-file",
"title": str(title or f"Item {idx}"),
"detail": str(uploader or ""),
"media_kind": "playlist-item",
"playlist_index": idx,
"_selection_args": (["-url", str(entry_url)] if entry_url else ["-url", str(url), "-item", str(idx)]),
"url": entry_url,
"target": entry_url,
"columns": [
row = build_table_result_payload(
table="download-file",
title=str(title or f"Item {idx}"),
detail=str(uploader or ""),
columns=[
("#", str(idx)),
("Title", str(title or "")),
("Duration", str(duration or "")),
("Uploader", str(uploader or "")),
],
}
selection_args=(
["-url", str(entry_url)] if entry_url else ["-url", str(url), "-item", str(idx)]
),
media_kind="playlist-item",
playlist_index=idx,
url=entry_url,
target=entry_url,
)
results_list.append(row)
table.add_result(row)
@@ -1782,14 +1789,11 @@ class Download_File(Cmdlet):
desc_parts.append(size_str)
format_desc = " | ".join(desc_parts)
format_dict: Dict[str, Any] = {
"table": "download-file",
"title": f"Format {format_id}",
"url": url,
"target": url,
"detail": format_desc,
"media_kind": "format",
"columns": [
format_dict = build_table_result_payload(
table="download-file",
title=f"Format {format_id}",
detail=format_desc,
columns=[
("ID", format_id),
("Resolution", resolution or "N/A"),
("Ext", ext),
@@ -1797,13 +1801,16 @@ class Download_File(Cmdlet):
("Video", vcodec),
("Audio", acodec),
],
"full_metadata": {
selection_args=["-query", f"format:{selection_format_id}"],
url=url,
target=url,
media_kind="format",
full_metadata={
"format_id": format_id,
"url": url,
"item_selector": selection_format_id,
},
"_selection_args": ["-query", f"format:{selection_format_id}"],
}
)
results_list.append(format_dict)
table.add_result(format_dict)
@@ -2379,18 +2386,18 @@ class Download_File(Cmdlet):
if not final_url and url:
final_url = str(url)
return {
"path": str(media_path),
"hash": hash_value,
"title": title,
"url": final_url,
"tag": tag,
"action": "cmdlet:download-file",
"is_temp": True,
"ytdl_format": getattr(opts, "ytdl_format", None),
"store": getattr(opts, "storage_name", None) or getattr(opts, "storage_location", None) or "PATH",
"media_kind": "video" if opts.mode == "video" else "audio",
}
return build_file_result_payload(
title=title,
path=str(media_path),
hash_value=hash_value,
url=final_url,
tag=tag,
store=getattr(opts, "storage_name", None) or getattr(opts, "storage_location", None) or "PATH",
action="cmdlet:download-file",
is_temp=True,
ytdl_format=getattr(opts, "ytdl_format", None),
media_kind="video" if opts.mode == "video" else "audio",
)
@staticmethod
def download_streaming_url_as_pipe_objects(
@@ -2609,22 +2616,13 @@ class Download_File(Cmdlet):
return out
@staticmethod
def _normalize_hash_hex(value: Optional[str]) -> Optional[str]:
if not value or not isinstance(value, str):
return None
candidate = value.strip().lower()
if len(candidate) == 64 and all(c in "0123456789abcdef" for c in candidate):
return candidate
return None
@classmethod
def _extract_hash_from_search_hit(cls, hit: Any) -> Optional[str]:
if not isinstance(hit, dict):
return None
for key in ("hash", "hash_hex", "file_hash", "hydrus_hash"):
v = hit.get(key)
normalized = cls._normalize_hash_hex(str(v) if v is not None else None)
normalized = sh.normalize_hash(str(v) if v is not None else None)
if normalized:
return normalized
return None
@@ -2717,10 +2715,10 @@ class Download_File(Cmdlet):
hashes: List[str] = []
for po in pipe_objects:
h_val = cls._normalize_hash_hex(str(po.get("hash") or ""))
h_val = sh.normalize_hash(str(po.get("hash") or ""))
hashes.append(h_val or "")
king_hash = cls._normalize_hash_hex(source_king_hash) if source_king_hash else None
king_hash = sh.normalize_hash(source_king_hash) if source_king_hash else None
if not king_hash:
king_hash = hashes[0] if hashes and hashes[0] else None
if not king_hash:
@@ -2774,10 +2772,10 @@ class Download_File(Cmdlet):
# Fallback to piped items if no explicit URLs provided
piped_items = []
if not raw_url:
if isinstance(result, list):
piped_items = list(result)
elif result is not None:
piped_items = [result]
piped_items = sh.normalize_result_items(
result,
include_falsey_single=True,
)
# Handle TABLE_AUTO_STAGES routing: if a piped item has _selection_args,
# re-invoke download-file with those args instead of processing the PipeObject itself.
@@ -2785,68 +2783,18 @@ class Download_File(Cmdlet):
selection_runs: List[List[str]] = []
residual_items: List[Any] = []
def _looks_like_url(value: Any) -> bool:
try:
s_val = str(value or "").strip().lower()
except Exception:
return False
return s_val.startswith(
("http://", "https://", "magnet:", "torrent:") + _ALLDEBRID_PREFIXES
)
def _extract_selection_args(item: Any) -> tuple[Optional[List[str]], Optional[str]]:
selection_args: Optional[List[str]] = None
item_url: Optional[str] = None
if isinstance(item, dict):
selection_args = item.get("_selection_args") or item.get("selection_args")
item_url = item.get("url") or item.get("path") or item.get("target")
md = item.get("metadata") or item.get("full_metadata")
if isinstance(md, dict):
selection_args = selection_args or md.get("_selection_args") or md.get("selection_args")
item_url = item_url or md.get("url") or md.get("source_url")
extra = item.get("extra")
if isinstance(extra, dict):
selection_args = selection_args or extra.get("_selection_args") or extra.get("selection_args")
item_url = item_url or extra.get("url") or extra.get("source_url")
else:
item_url = getattr(item, "url", None) or getattr(item, "path", None) or getattr(item, "target", None)
md = getattr(item, "metadata", None)
if isinstance(md, dict):
selection_args = md.get("_selection_args") or md.get("selection_args")
item_url = item_url or md.get("url") or md.get("source_url")
extra = getattr(item, "extra", None)
if isinstance(extra, dict):
selection_args = selection_args or extra.get("_selection_args") or extra.get("selection_args")
item_url = item_url or extra.get("url") or extra.get("source_url")
if isinstance(selection_args, (list, tuple)):
normalized_args = [str(arg) for arg in selection_args if arg is not None]
elif selection_args is not None:
normalized_args = [str(selection_args)]
else:
normalized_args = None
if item_url and not _looks_like_url(item_url):
item_url = None
return normalized_args, item_url
def _selection_args_have_url(args_list: Sequence[str]) -> bool:
for idx, arg in enumerate(args_list):
low = str(arg or "").strip().lower()
if low in {"-url", "--url"}:
return True
if _looks_like_url(arg):
return True
return False
for item in piped_items:
handled = False
try:
normalized_args, item_url = _extract_selection_args(item)
normalized_args, _normalized_action, item_url = extract_selection_fields(
item,
extra_url_prefixes=_ALLDEBRID_PREFIXES,
)
if normalized_args:
if _selection_args_have_url(normalized_args):
if selection_args_have_url(
normalized_args,
extra_url_prefixes=_ALLDEBRID_PREFIXES,
):
selection_runs.append(list(normalized_args))
handled = True
elif item_url:
@@ -2860,25 +2808,11 @@ class Download_File(Cmdlet):
if selection_runs:
selection_urls: List[str] = []
def _extract_urls_from_args(args_list: Sequence[str]) -> List[str]:
urls: List[str] = []
idx = 0
while idx < len(args_list):
token = str(args_list[idx] or "")
low = token.strip().lower()
if low in {"-url", "--url"} and idx + 1 < len(args_list):
candidate = str(args_list[idx + 1] or "").strip()
if _looks_like_url(candidate):
urls.append(candidate)
idx += 2
continue
if _looks_like_url(token):
urls.append(token.strip())
idx += 1
return urls
for run_args in selection_runs:
for u in _extract_urls_from_args(run_args):
for u in extract_urls_from_selection_args(
run_args,
extra_url_prefixes=_ALLDEBRID_PREFIXES,
):
if u not in selection_urls:
selection_urls.append(u)