f
This commit is contained in:
@@ -92,7 +92,7 @@
|
|||||||
"(hitfile\\.net/[a-z0-9A-Z]{4,9})"
|
"(hitfile\\.net/[a-z0-9A-Z]{4,9})"
|
||||||
],
|
],
|
||||||
"regexp": "(hitf\\.(to|cc)/([a-z0-9A-Z]{4,9}))|(htfl\\.(net|to|cc)/([a-z0-9A-Z]{4,9}))|(hitfile\\.(net)/download/free/([a-z0-9A-Z]{4,9}))|((hitfile\\.net/[a-z0-9A-Z]{4,9}))",
|
"regexp": "(hitf\\.(to|cc)/([a-z0-9A-Z]{4,9}))|(htfl\\.(net|to|cc)/([a-z0-9A-Z]{4,9}))|(hitfile\\.(net)/download/free/([a-z0-9A-Z]{4,9}))|((hitfile\\.net/[a-z0-9A-Z]{4,9}))",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"mega": {
|
"mega": {
|
||||||
"name": "mega",
|
"name": "mega",
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ class MPV:
|
|||||||
def _q(s: str) -> str:
|
def _q(s: str) -> str:
|
||||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||||
|
|
||||||
pipeline = f"download-file -url {_q(url)} -format {_q(fmt)}"
|
pipeline = f"download-file -url {_q(url)} -query {_q(f'format:{fmt}')}"
|
||||||
if store:
|
if store:
|
||||||
pipeline += f" | add-file -store {_q(store)}"
|
pipeline += f" | add-file -store {_q(store)}"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
|||||||
size = _format_bytes(fmt.get("filesize") or fmt.get("filesize_approx"))
|
size = _format_bytes(fmt.get("filesize") or fmt.get("filesize_approx"))
|
||||||
|
|
||||||
# Build selection args compatible with MPV Lua picker.
|
# Build selection args compatible with MPV Lua picker.
|
||||||
selection_args = ["-format", format_id]
|
selection_args = ["-query", f"format:{format_id}"]
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -266,6 +266,22 @@ def _fetch_torrent_bytes(target: str) -> Optional[bytes]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_ALD_MAGNET_PREFIX = "alldebrid:magnet:"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_alldebrid_magnet_id(target: str) -> Optional[int]:
|
||||||
|
candidate = str(target or "").strip()
|
||||||
|
if not candidate:
|
||||||
|
return None
|
||||||
|
if not candidate.lower().startswith(_ALD_MAGNET_PREFIX):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
magnet_id_raw = candidate[len(_ALD_MAGNET_PREFIX):].strip()
|
||||||
|
return int(magnet_id_raw)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_magnet_spec(target: str) -> Optional[str]:
|
def resolve_magnet_spec(target: str) -> Optional[str]:
|
||||||
"""Resolve a magnet/hash/torrent URL into a magnet/hash string."""
|
"""Resolve a magnet/hash/torrent URL into a magnet/hash string."""
|
||||||
candidate = str(target or "").strip()
|
candidate = str(target or "").strip()
|
||||||
@@ -558,14 +574,14 @@ class AllDebrid(TableProviderMixin, Provider):
|
|||||||
1. User runs: search-file -provider alldebrid "ubuntu"
|
1. User runs: search-file -provider alldebrid "ubuntu"
|
||||||
2. Results show magnet folders and (optionally) files
|
2. Results show magnet folders and (optionally) files
|
||||||
3. User selects a row: @1
|
3. User selects a row: @1
|
||||||
4. Selection metadata routes to download-file with -magnet-id
|
4. Selection metadata routes to download-file with -url alldebrid:magnet:<id>
|
||||||
5. download-file calls provider.download_items() with magnet_id
|
5. download-file invokes provider.download_items() via provider URL handling
|
||||||
6. Provider fetches files, unlocks locked URLs, and downloads
|
6. Provider fetches files, unlocks locked URLs, and downloads
|
||||||
"""
|
"""
|
||||||
# Magnet URIs should be routed through this provider.
|
# Magnet URIs should be routed through this provider.
|
||||||
TABLE_AUTO_STAGES = {"alldebrid": ["download-file"]}
|
TABLE_AUTO_STAGES = {"alldebrid": ["download-file"]}
|
||||||
AUTO_STAGE_USE_SELECTION_ARGS = True
|
AUTO_STAGE_USE_SELECTION_ARGS = True
|
||||||
URL = ("magnet:",)
|
URL = ("magnet:", "alldebrid:magnet:")
|
||||||
URL_DOMAINS = ()
|
URL_DOMAINS = ()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -631,6 +647,19 @@ class AllDebrid(TableProviderMixin, Provider):
|
|||||||
return resolve_magnet_spec(str(target)) if isinstance(target, str) else None
|
return resolve_magnet_spec(str(target)) if isinstance(target, str) else None
|
||||||
|
|
||||||
def handle_url(self, url: str, *, output_dir: Optional[Path] = None) -> Tuple[bool, Optional[Path]]:
|
def handle_url(self, url: str, *, output_dir: Optional[Path] = None) -> Tuple[bool, Optional[Path]]:
|
||||||
|
magnet_id = _parse_alldebrid_magnet_id(url)
|
||||||
|
if magnet_id is not None:
|
||||||
|
return True, {
|
||||||
|
"action": "download_items",
|
||||||
|
"path": f"{_ALD_MAGNET_PREFIX}{magnet_id}",
|
||||||
|
"title": f"magnet-{magnet_id}",
|
||||||
|
"metadata": {
|
||||||
|
"magnet_id": magnet_id,
|
||||||
|
"provider": "alldebrid",
|
||||||
|
"provider_view": "files",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
spec = resolve_magnet_spec(url)
|
spec = resolve_magnet_spec(url)
|
||||||
if not spec:
|
if not spec:
|
||||||
return False, None
|
return False, None
|
||||||
@@ -1180,8 +1209,8 @@ class AllDebrid(TableProviderMixin, Provider):
|
|||||||
"provider": "alldebrid",
|
"provider": "alldebrid",
|
||||||
"provider_view": "files",
|
"provider_view": "files",
|
||||||
# Selection metadata for table system
|
# Selection metadata for table system
|
||||||
"_selection_args": ["-magnet-id", str(magnet_id)],
|
"_selection_args": ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||||
"_selection_action": ["download-file", "-provider", "alldebrid", "-magnet-id", str(magnet_id)],
|
"_selection_action": ["download-file", "-provider", "alldebrid", "-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||||
}
|
}
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
@@ -1295,8 +1324,8 @@ class AllDebrid(TableProviderMixin, Provider):
|
|||||||
"provider_view": "folders",
|
"provider_view": "folders",
|
||||||
"magnet_name": magnet_name,
|
"magnet_name": magnet_name,
|
||||||
# Selection metadata: allow @N expansion to drive downloads directly
|
# Selection metadata: allow @N expansion to drive downloads directly
|
||||||
"_selection_args": ["-magnet-id", str(magnet_id)],
|
"_selection_args": ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||||
"_selection_action": ["download-file", "-provider", "alldebrid", "-magnet-id", str(magnet_id)],
|
"_selection_action": ["download-file", "-provider", "alldebrid", "-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1585,7 +1614,7 @@ try:
|
|||||||
1. Explicit _selection_action (full command args)
|
1. Explicit _selection_action (full command args)
|
||||||
2. Explicit _selection_args (URL-specific args)
|
2. Explicit _selection_args (URL-specific args)
|
||||||
3. Magic routing based on provider_view (files vs folders)
|
3. Magic routing based on provider_view (files vs folders)
|
||||||
4. Magnet ID routing for folder-type rows
|
4. Magnet ID routing for folder-type rows (via alldebrid:magnet:<id>)
|
||||||
5. Direct URL for file rows
|
5. Direct URL for file rows
|
||||||
|
|
||||||
This ensures that selector overrides all pre-codes and gives users full power.
|
This ensures that selector overrides all pre-codes and gives users full power.
|
||||||
@@ -1612,7 +1641,7 @@ try:
|
|||||||
# Folder rows: use magnet_id to fetch and download all files
|
# Folder rows: use magnet_id to fetch and download all files
|
||||||
magnet_id = metadata.get("magnet_id")
|
magnet_id = metadata.get("magnet_id")
|
||||||
if magnet_id is not None:
|
if magnet_id is not None:
|
||||||
return ["-magnet-id", str(magnet_id)]
|
return ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"]
|
||||||
|
|
||||||
# Fallback: try direct URL
|
# Fallback: try direct URL
|
||||||
if row.path:
|
if row.path:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
When a URL is passed through download-file, this provider displays available formats
|
When a URL is passed through download-file, this provider displays available formats
|
||||||
in a table and routes format selection back to download-file with the chosen format
|
in a table and routes format selection back to download-file with the chosen format
|
||||||
already specified via -format, skipping the format table on the second invocation.
|
already specified via -query "format:<id>", skipping the format table on the second invocation.
|
||||||
|
|
||||||
This keeps format selection logic in ytdlp and leaves add-file plug-and-play.
|
This keeps format selection logic in ytdlp and leaves add-file plug-and-play.
|
||||||
"""
|
"""
|
||||||
@@ -31,8 +31,8 @@ class ytdlp(TableProviderMixin, Provider):
|
|||||||
- User runs: download-file "https://example.com/video"
|
- User runs: download-file "https://example.com/video"
|
||||||
- If URL is ytdlp-supported and no format specified, displays format table
|
- If URL is ytdlp-supported and no format specified, displays format table
|
||||||
- User selects @N (e.g., @3 for format index 3)
|
- User selects @N (e.g., @3 for format index 3)
|
||||||
- Selection args include -format <format_id>, re-invoking download-file
|
- Selection args include -query "format:<format_id>", re-invoking download-file
|
||||||
- Second download-file call sees -format and skips table, downloads directly
|
- Second download-file call sees the format query and skips the table, downloads directly
|
||||||
|
|
||||||
SEARCH USAGE:
|
SEARCH USAGE:
|
||||||
- User runs: search-file -provider ytdlp "linux tutorial"
|
- User runs: search-file -provider ytdlp "linux tutorial"
|
||||||
@@ -41,12 +41,12 @@ class ytdlp(TableProviderMixin, Provider):
|
|||||||
- Selection args route to download-file for streaming download
|
- Selection args route to download-file for streaming download
|
||||||
|
|
||||||
SELECTION FLOW (Format):
|
SELECTION FLOW (Format):
|
||||||
1. download-file receives URL without -format
|
1. download-file receives URL without a format query
|
||||||
2. Calls ytdlp to list formats
|
2. Calls ytdlp to list formats
|
||||||
3. Returns formats as ResultTable (from this provider)
|
3. Returns formats as ResultTable (from this provider)
|
||||||
4. User selects @N
|
4. User selects @N
|
||||||
5. Selection args: ["-format", "<format_id>"] route back to download-file
|
5. Selection args: ["-query", "format:<format_id>"] route back to download-file
|
||||||
6. Second download-file invocation with -format skips table
|
6. Second download-file invocation with format query skips table
|
||||||
|
|
||||||
SELECTION FLOW (Search):
|
SELECTION FLOW (Search):
|
||||||
1. search-file lists YouTube videos via yt_dlp
|
1. search-file lists YouTube videos via yt_dlp
|
||||||
@@ -56,7 +56,7 @@ class ytdlp(TableProviderMixin, Provider):
|
|||||||
5. download-file downloads the selected video
|
5. download-file downloads the selected video
|
||||||
|
|
||||||
TABLE AUTO-STAGES:
|
TABLE AUTO-STAGES:
|
||||||
- Format selection: ytdlp.formatlist -> download-file (with -format)
|
- Format selection: ytdlp.formatlist -> download-file (with -query format:<id>)
|
||||||
- Video search: ytdlp.search -> download-file (with -url)
|
- Video search: ytdlp.search -> download-file (with -url)
|
||||||
|
|
||||||
SUPPORTED URLS:
|
SUPPORTED URLS:
|
||||||
@@ -106,7 +106,7 @@ class ytdlp(TableProviderMixin, Provider):
|
|||||||
"ytdlp.formatlist": ["download-file"],
|
"ytdlp.formatlist": ["download-file"],
|
||||||
"ytdlp.search": ["download-file"],
|
"ytdlp.search": ["download-file"],
|
||||||
}
|
}
|
||||||
# Forward selection args (including -format or -url) to the next stage
|
# Forward selection args (including -query format:... or -url) to the next stage
|
||||||
AUTO_STAGE_USE_SELECTION_ARGS = True
|
AUTO_STAGE_USE_SELECTION_ARGS = True
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
@@ -277,7 +277,7 @@ try:
|
|||||||
"""Return selection args for format selection.
|
"""Return selection args for format selection.
|
||||||
|
|
||||||
When user selects @N, these args are passed to download-file which sees
|
When user selects @N, these args are passed to download-file which sees
|
||||||
the -format specifier and skips the format table, downloading directly.
|
the format query and skips the format table, downloading directly.
|
||||||
"""
|
"""
|
||||||
metadata = row.metadata or {}
|
metadata = row.metadata or {}
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ try:
|
|||||||
# Fallback: use format_id
|
# Fallback: use format_id
|
||||||
format_id = metadata.get("format_id") or metadata.get("id")
|
format_id = metadata.get("format_id") or metadata.get("id")
|
||||||
if format_id:
|
if format_id:
|
||||||
result_args = ["-format", str(format_id)]
|
result_args = ["-query", f"format:{format_id}"]
|
||||||
debug(f"[ytdlp] Selection routed with format_id: {format_id}")
|
debug(f"[ytdlp] Selection routed with format_id: {format_id}")
|
||||||
return result_args
|
return result_args
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections.abc import Iterable as IterableABC
|
from collections.abc import Iterable as IterableABC
|
||||||
|
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
from SYS.logger import log, debug
|
from SYS.logger import log, debug
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -3074,6 +3075,19 @@ def check_url_exists_in_storage(
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _load_preflight_cache() -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
existing = pipeline_context.load_value("preflight", default=None)
|
||||||
|
except Exception:
|
||||||
|
existing = None
|
||||||
|
return existing if isinstance(existing, dict) else {}
|
||||||
|
|
||||||
|
def _store_preflight_cache(cache: Dict[str, Any]) -> None:
|
||||||
|
try:
|
||||||
|
pipeline_context.store_value("preflight", cache)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
unique_urls: List[str] = []
|
unique_urls: List[str] = []
|
||||||
for u in urls or []:
|
for u in urls or []:
|
||||||
s = str(u or "").strip()
|
s = str(u or "").strip()
|
||||||
@@ -3093,6 +3107,56 @@ def check_url_exists_in_storage(
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _expand_url_variants(value: str) -> List[str]:
|
||||||
|
if not _httpish(value):
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = urlparse(value)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if parsed.scheme.lower() not in {"http", "https"}:
|
||||||
|
return []
|
||||||
|
|
||||||
|
out: List[str] = []
|
||||||
|
|
||||||
|
def _maybe_add(candidate: str) -> None:
|
||||||
|
if not candidate or candidate == value:
|
||||||
|
return
|
||||||
|
if candidate not in out:
|
||||||
|
out.append(candidate)
|
||||||
|
|
||||||
|
if parsed.fragment:
|
||||||
|
_maybe_add(urlunparse(parsed._replace(fragment="")))
|
||||||
|
|
||||||
|
time_keys = {"t", "start", "time_continue", "timestamp", "time", "begin"}
|
||||||
|
tracking_prefixes = ("utm_",)
|
||||||
|
|
||||||
|
try:
|
||||||
|
query_pairs = parse_qsl(parsed.query, keep_blank_values=True)
|
||||||
|
except Exception:
|
||||||
|
query_pairs = []
|
||||||
|
|
||||||
|
if query_pairs or parsed.fragment:
|
||||||
|
filtered_pairs = []
|
||||||
|
removed = False
|
||||||
|
for key, val in query_pairs:
|
||||||
|
key_norm = str(key or "").lower()
|
||||||
|
if key_norm in time_keys:
|
||||||
|
removed = True
|
||||||
|
continue
|
||||||
|
if key_norm.startswith(tracking_prefixes):
|
||||||
|
removed = True
|
||||||
|
continue
|
||||||
|
filtered_pairs.append((key, val))
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
new_query = urlencode(filtered_pairs, doseq=True) if filtered_pairs else ""
|
||||||
|
_maybe_add(urlunparse(parsed._replace(query=new_query, fragment="")))
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
url_needles: Dict[str, List[str]] = {}
|
url_needles: Dict[str, List[str]] = {}
|
||||||
for u in unique_urls:
|
for u in unique_urls:
|
||||||
needles: List[str] = []
|
needles: List[str] = []
|
||||||
@@ -3112,7 +3176,88 @@ def check_url_exists_in_storage(
|
|||||||
continue
|
continue
|
||||||
if n2 not in filtered:
|
if n2 not in filtered:
|
||||||
filtered.append(n2)
|
filtered.append(n2)
|
||||||
url_needles[u] = filtered if filtered else [u]
|
expanded: List[str] = []
|
||||||
|
for n2 in filtered:
|
||||||
|
for extra in _expand_url_variants(n2):
|
||||||
|
if extra not in expanded and extra not in filtered:
|
||||||
|
expanded.append(extra)
|
||||||
|
|
||||||
|
combined = filtered + expanded
|
||||||
|
url_needles[u] = combined if combined else [u]
|
||||||
|
|
||||||
|
if in_pipeline:
|
||||||
|
preflight_cache = _load_preflight_cache()
|
||||||
|
url_dup_cache = preflight_cache.get("url_duplicates")
|
||||||
|
if not isinstance(url_dup_cache, dict):
|
||||||
|
url_dup_cache = {}
|
||||||
|
cached_urls = url_dup_cache.get("urls")
|
||||||
|
cached_set = {str(u) for u in cached_urls} if isinstance(cached_urls, list) else set()
|
||||||
|
|
||||||
|
if cached_set:
|
||||||
|
all_cached = True
|
||||||
|
for original_url, needles in url_needles.items():
|
||||||
|
if original_url in cached_set:
|
||||||
|
continue
|
||||||
|
if any(n in cached_set for n in (needles or [])):
|
||||||
|
continue
|
||||||
|
all_cached = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if all_cached:
|
||||||
|
debug("Bulk URL preflight: cached for pipeline; skipping duplicate check")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _search_backend_url_hits(
|
||||||
|
backend: Any,
|
||||||
|
backend_name: str,
|
||||||
|
original_url: str,
|
||||||
|
needles: Sequence[str],
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
backend_hits: List[Dict[str, Any]] = []
|
||||||
|
for needle in (needles or [])[:3]:
|
||||||
|
try:
|
||||||
|
backend_hits = backend.search(f"url:{needle}", limit=1) or []
|
||||||
|
if backend_hits:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not backend_hits:
|
||||||
|
return None
|
||||||
|
|
||||||
|
hit = backend_hits[0]
|
||||||
|
title = hit.get("title") or hit.get("name") or hit.get("target") or hit.get("path") or "(exists)"
|
||||||
|
file_hash = hit.get("hash") or hit.get("file_hash") or hit.get("sha256") or ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from SYS.result_table import build_display_row
|
||||||
|
extracted = build_display_row(hit, keys=["title", "store", "hash", "ext", "size"])
|
||||||
|
except Exception:
|
||||||
|
extracted = {}
|
||||||
|
|
||||||
|
extracted["title"] = str(title)
|
||||||
|
extracted["store"] = str(hit.get("store") or backend_name)
|
||||||
|
extracted["hash"] = str(file_hash or "")
|
||||||
|
|
||||||
|
ext = extracted.get("ext")
|
||||||
|
size_val = extracted.get("size")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": str(title),
|
||||||
|
"store": str(hit.get("store") or backend_name),
|
||||||
|
"hash": str(file_hash or ""),
|
||||||
|
"ext": str(ext or ""),
|
||||||
|
"size": size_val,
|
||||||
|
"url": original_url,
|
||||||
|
"columns": [
|
||||||
|
("Title", str(title)),
|
||||||
|
("Store", str(hit.get("store") or backend_name)),
|
||||||
|
("Hash", str(file_hash or "")),
|
||||||
|
("Ext", str(ext or "")),
|
||||||
|
("Size", size_val),
|
||||||
|
("URL", original_url),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
backend_names: List[str] = []
|
backend_names: List[str] = []
|
||||||
try:
|
try:
|
||||||
@@ -3167,12 +3312,11 @@ def check_url_exists_in_storage(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if HydrusNetwork is not None and isinstance(backend, HydrusNetwork):
|
if HydrusNetwork is not None and isinstance(backend, HydrusNetwork):
|
||||||
if not hydrus_available:
|
|
||||||
continue
|
|
||||||
|
|
||||||
client = getattr(backend, "_client", None)
|
client = getattr(backend, "_client", None)
|
||||||
if client is None:
|
if client is None:
|
||||||
continue
|
continue
|
||||||
|
if not hydrus_available:
|
||||||
|
debug("Bulk URL preflight: hydrus availability check failed; attempting best-effort lookup")
|
||||||
|
|
||||||
for original_url, needles in url_needles.items():
|
for original_url, needles in url_needles.items():
|
||||||
if len(match_rows) >= max_rows:
|
if len(match_rows) >= max_rows:
|
||||||
@@ -3214,6 +3358,11 @@ def check_url_exists_in_storage(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not found:
|
if not found:
|
||||||
|
fallback_row = _search_backend_url_hits(backend, str(backend_name), original_url, needles)
|
||||||
|
if fallback_row:
|
||||||
|
seen_pairs.add((original_url, str(backend_name)))
|
||||||
|
matched_urls.add(original_url)
|
||||||
|
match_rows.append(fallback_row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
seen_pairs.add((original_url, str(backend_name)))
|
seen_pairs.add((original_url, str(backend_name)))
|
||||||
@@ -3239,57 +3388,33 @@ def check_url_exists_in_storage(
|
|||||||
if (original_url, str(backend_name)) in seen_pairs:
|
if (original_url, str(backend_name)) in seen_pairs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
backend_hits: List[Dict[str, Any]] = []
|
display_row = _search_backend_url_hits(backend, str(backend_name), original_url, needles)
|
||||||
for needle in (needles or [])[:3]:
|
if not display_row:
|
||||||
try:
|
|
||||||
backend_hits = backend.search(f"url:{needle}", limit=1) or []
|
|
||||||
if backend_hits:
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not backend_hits:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
seen_pairs.add((original_url, str(backend_name)))
|
seen_pairs.add((original_url, str(backend_name)))
|
||||||
matched_urls.add(original_url)
|
matched_urls.add(original_url)
|
||||||
hit = backend_hits[0]
|
|
||||||
title = hit.get("title") or hit.get("name") or hit.get("target") or hit.get("path") or "(exists)"
|
|
||||||
file_hash = hit.get("hash") or hit.get("file_hash") or hit.get("sha256") or ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
from SYS.result_table import build_display_row
|
|
||||||
extracted = build_display_row(hit, keys=["title", "store", "hash", "ext", "size"])
|
|
||||||
except Exception:
|
|
||||||
extracted = {}
|
|
||||||
|
|
||||||
extracted["title"] = str(title)
|
|
||||||
extracted["store"] = str(hit.get("store") or backend_name)
|
|
||||||
extracted["hash"] = str(file_hash or "")
|
|
||||||
|
|
||||||
ext = extracted.get("ext")
|
|
||||||
size_val = extracted.get("size")
|
|
||||||
|
|
||||||
display_row = {
|
|
||||||
"title": str(title),
|
|
||||||
"store": str(hit.get("store") or backend_name),
|
|
||||||
"hash": str(file_hash or ""),
|
|
||||||
"ext": str(ext or ""),
|
|
||||||
"size": size_val,
|
|
||||||
"url": original_url,
|
|
||||||
"columns": [
|
|
||||||
("Title", str(title)),
|
|
||||||
("Store", str(hit.get("store") or backend_name)),
|
|
||||||
("Hash", str(file_hash or "")),
|
|
||||||
("Ext", str(ext or "")),
|
|
||||||
("Size", size_val),
|
|
||||||
("URL", original_url),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
match_rows.append(display_row)
|
match_rows.append(display_row)
|
||||||
|
|
||||||
if not match_rows:
|
if not match_rows:
|
||||||
debug("Bulk URL preflight: no matches")
|
debug("Bulk URL preflight: no matches")
|
||||||
|
if in_pipeline:
|
||||||
|
preflight_cache = _load_preflight_cache()
|
||||||
|
url_dup_cache = preflight_cache.get("url_duplicates")
|
||||||
|
if not isinstance(url_dup_cache, dict):
|
||||||
|
url_dup_cache = {}
|
||||||
|
|
||||||
|
cached_urls = url_dup_cache.get("urls")
|
||||||
|
cached_set = {str(u) for u in cached_urls} if isinstance(cached_urls, list) else set()
|
||||||
|
|
||||||
|
for original_url, needles in url_needles.items():
|
||||||
|
cached_set.add(original_url)
|
||||||
|
for needle in needles or []:
|
||||||
|
cached_set.add(str(needle))
|
||||||
|
|
||||||
|
url_dup_cache["urls"] = sorted(cached_set)
|
||||||
|
preflight_cache["url_duplicates"] = url_dup_cache
|
||||||
|
_store_preflight_cache(preflight_cache)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
table = ResultTable(f"URL already exists ({len(matched_urls)} url(s))", max_columns=10)
|
table = ResultTable(f"URL already exists ({len(matched_urls)} url(s))", max_columns=10)
|
||||||
@@ -3333,6 +3458,13 @@ def check_url_exists_in_storage(
|
|||||||
url_dup_cache = {}
|
url_dup_cache = {}
|
||||||
url_dup_cache["command"] = str(current_cmd_text or "")
|
url_dup_cache["command"] = str(current_cmd_text or "")
|
||||||
url_dup_cache["continue"] = bool(answered_yes)
|
url_dup_cache["continue"] = bool(answered_yes)
|
||||||
|
cached_urls = url_dup_cache.get("urls")
|
||||||
|
cached_set = {str(u) for u in cached_urls} if isinstance(cached_urls, list) else set()
|
||||||
|
for original_url, needles in url_needles.items():
|
||||||
|
cached_set.add(original_url)
|
||||||
|
for needle in needles or []:
|
||||||
|
cached_set.add(str(needle))
|
||||||
|
url_dup_cache["urls"] = sorted(cached_set)
|
||||||
preflight_cache["url_duplicates"] = url_dup_cache
|
preflight_cache["url_duplicates"] = url_dup_cache
|
||||||
try:
|
try:
|
||||||
pipeline_context.store_value("preflight", preflight_cache)
|
pipeline_context.store_value("preflight", preflight_cache)
|
||||||
|
|||||||
@@ -81,23 +81,6 @@ class Download_File(Cmdlet):
|
|||||||
alias="o",
|
alias="o",
|
||||||
description="(deprecated) Output directory (use -path instead)",
|
description="(deprecated) Output directory (use -path instead)",
|
||||||
),
|
),
|
||||||
CmdletArg(
|
|
||||||
name="audio",
|
|
||||||
type="flag",
|
|
||||||
alias="a",
|
|
||||||
description="Download audio only (yt-dlp)",
|
|
||||||
),
|
|
||||||
CmdletArg(
|
|
||||||
name="-magnet-id",
|
|
||||||
type="string",
|
|
||||||
description="(internal) AllDebrid magnet id used by provider selection hooks",
|
|
||||||
),
|
|
||||||
CmdletArg(
|
|
||||||
name="format",
|
|
||||||
type="string",
|
|
||||||
alias="fmt",
|
|
||||||
description="Explicit yt-dlp format selector",
|
|
||||||
),
|
|
||||||
QueryArg(
|
QueryArg(
|
||||||
"clip",
|
"clip",
|
||||||
key="clip",
|
key="clip",
|
||||||
@@ -183,6 +166,42 @@ class Download_File(Cmdlet):
|
|||||||
path_value: Optional[Any] = path
|
path_value: Optional[Any] = path
|
||||||
|
|
||||||
if isinstance(path, dict):
|
if isinstance(path, dict):
|
||||||
|
provider_action = str(
|
||||||
|
path.get("action")
|
||||||
|
or path.get("provider_action")
|
||||||
|
or ""
|
||||||
|
).strip().lower()
|
||||||
|
if provider_action == "download_items" or bool(path.get("download_items")):
|
||||||
|
request_metadata = path.get("metadata") or path.get("full_metadata") or {}
|
||||||
|
if not isinstance(request_metadata, dict):
|
||||||
|
request_metadata = {}
|
||||||
|
magnet_id = path.get("magnet_id") or request_metadata.get("magnet_id")
|
||||||
|
if magnet_id is not None:
|
||||||
|
request_metadata.setdefault("magnet_id", magnet_id)
|
||||||
|
|
||||||
|
if SearchResult is None:
|
||||||
|
debug("Provider download_items requested but SearchResult unavailable")
|
||||||
|
continue
|
||||||
|
|
||||||
|
sr = SearchResult(
|
||||||
|
table=str(provider_name),
|
||||||
|
title=str(path.get("title") or path.get("name") or f"{provider_name} item"),
|
||||||
|
path=str(path.get("path") or path.get("url") or url),
|
||||||
|
full_metadata=request_metadata,
|
||||||
|
)
|
||||||
|
downloaded_extra = self._download_provider_items(
|
||||||
|
provider=provider,
|
||||||
|
provider_name=str(provider_name),
|
||||||
|
search_result=sr,
|
||||||
|
output_dir=final_output_dir,
|
||||||
|
progress=progress,
|
||||||
|
quiet_mode=quiet_mode,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
if downloaded_extra:
|
||||||
|
downloaded_count += int(downloaded_extra)
|
||||||
|
continue
|
||||||
|
|
||||||
path_value = path.get("path") or path.get("file_path")
|
path_value = path.get("path") or path.get("file_path")
|
||||||
extra_meta = path.get("metadata") or path.get("full_metadata")
|
extra_meta = path.get("metadata") or path.get("full_metadata")
|
||||||
title_hint = path.get("title") or path.get("name")
|
title_hint = path.get("title") or path.get("name")
|
||||||
@@ -451,6 +470,24 @@ class Download_File(Cmdlet):
|
|||||||
provider_sr = sr
|
provider_sr = sr
|
||||||
debug(f"[download-file] Provider download result: {downloaded_path}")
|
debug(f"[download-file] Provider download result: {downloaded_path}")
|
||||||
|
|
||||||
|
if downloaded_path is None:
|
||||||
|
try:
|
||||||
|
downloaded_extra = self._download_provider_items(
|
||||||
|
provider=provider_obj,
|
||||||
|
provider_name=str(provider_key),
|
||||||
|
search_result=sr,
|
||||||
|
output_dir=output_dir,
|
||||||
|
progress=progress,
|
||||||
|
quiet_mode=quiet_mode,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
downloaded_extra = 0
|
||||||
|
|
||||||
|
if downloaded_extra:
|
||||||
|
downloaded_count += int(downloaded_extra)
|
||||||
|
continue
|
||||||
|
|
||||||
# Fallback: if we have a direct HTTP URL and no provider successfully handled it
|
# Fallback: if we have a direct HTTP URL and no provider successfully handled it
|
||||||
if (downloaded_path is None and not attempted_provider_download
|
if (downloaded_path is None and not attempted_provider_download
|
||||||
and isinstance(target, str) and target.startswith("http")):
|
and isinstance(target, str) and target.startswith("http")):
|
||||||
@@ -539,6 +576,68 @@ class Download_File(Cmdlet):
|
|||||||
|
|
||||||
return downloaded_count, queued_magnet_submissions
|
return downloaded_count, queued_magnet_submissions
|
||||||
|
|
||||||
|
def _download_provider_items(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
provider: Any,
|
||||||
|
provider_name: str,
|
||||||
|
search_result: Any,
|
||||||
|
output_dir: Path,
|
||||||
|
progress: PipelineProgress,
|
||||||
|
quiet_mode: bool,
|
||||||
|
config: Dict[str, Any],
|
||||||
|
) -> int:
|
||||||
|
if provider is None or not hasattr(provider, "download_items"):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _on_emit(path: Path, file_url: str, relpath: str, metadata: Dict[str, Any]) -> None:
|
||||||
|
title_hint = None
|
||||||
|
try:
|
||||||
|
title_hint = metadata.get("name") or relpath
|
||||||
|
except Exception:
|
||||||
|
title_hint = relpath
|
||||||
|
title_hint = title_hint or (Path(path).name if path else "download")
|
||||||
|
|
||||||
|
self._emit_local_file(
|
||||||
|
downloaded_path=path,
|
||||||
|
source=file_url,
|
||||||
|
title_hint=title_hint,
|
||||||
|
tags_hint=None,
|
||||||
|
media_kind_hint="file",
|
||||||
|
full_metadata=metadata if isinstance(metadata, dict) else None,
|
||||||
|
progress=progress,
|
||||||
|
config=config,
|
||||||
|
provider_hint=provider_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
downloaded_count = provider.download_items(
|
||||||
|
search_result,
|
||||||
|
output_dir,
|
||||||
|
emit=_on_emit,
|
||||||
|
progress=progress,
|
||||||
|
quiet_mode=quiet_mode,
|
||||||
|
path_from_result=coerce_to_path,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
downloaded_count = provider.download_items(
|
||||||
|
search_result,
|
||||||
|
output_dir,
|
||||||
|
emit=_on_emit,
|
||||||
|
progress=progress,
|
||||||
|
quiet_mode=quiet_mode,
|
||||||
|
path_from_result=coerce_to_path,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Provider {provider_name} download_items error: {exc}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(downloaded_count or 0)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
def _emit_local_file(
|
def _emit_local_file(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1221,11 +1320,27 @@ class Download_File(Cmdlet):
|
|||||||
# Add base command for display
|
# Add base command for display
|
||||||
format_dict["cmd"] = base_cmd
|
format_dict["cmd"] = base_cmd
|
||||||
|
|
||||||
|
def _merge_query_args(selection_args: List[str], query_value: str) -> List[str]:
|
||||||
|
if not query_value:
|
||||||
|
return selection_args
|
||||||
|
merged = list(selection_args or [])
|
||||||
|
if "-query" in merged:
|
||||||
|
idx_query = merged.index("-query")
|
||||||
|
if idx_query + 1 < len(merged):
|
||||||
|
existing = str(merged[idx_query + 1] or "").strip()
|
||||||
|
merged[idx_query + 1] = f"{existing},{query_value}" if existing else query_value
|
||||||
|
else:
|
||||||
|
merged.append(query_value)
|
||||||
|
else:
|
||||||
|
merged.extend(["-query", query_value])
|
||||||
|
return merged
|
||||||
|
|
||||||
# Append clip values to selection args if needed
|
# Append clip values to selection args if needed
|
||||||
selection_args: List[str] = format_dict["_selection_args"].copy()
|
selection_args: List[str] = list(format_dict.get("_selection_args") or [])
|
||||||
try:
|
try:
|
||||||
if (not clip_spec) and clip_values:
|
if (not clip_spec) and clip_values:
|
||||||
selection_args.extend(["-query", f"clip:{','.join([v for v in clip_values if v])}"])
|
clip_query = f"clip:{','.join([v for v in clip_values if v])}"
|
||||||
|
selection_args = _merge_query_args(selection_args, clip_query)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
format_dict["_selection_args"] = selection_args
|
format_dict["_selection_args"] = selection_args
|
||||||
@@ -1253,7 +1368,9 @@ class Download_File(Cmdlet):
|
|||||||
pipeline_context.set_last_result_table(table, results_list)
|
pipeline_context.set_last_result_table(table, results_list)
|
||||||
|
|
||||||
debug(f"[ytdlp.formatlist] Format table registered with {len(results_list)} formats")
|
debug(f"[ytdlp.formatlist] Format table registered with {len(results_list)} formats")
|
||||||
debug(f"[ytdlp.formatlist] When user selects @N, will invoke: download-file {url} -format <format_id>")
|
debug(
|
||||||
|
f"[ytdlp.formatlist] When user selects @N, will invoke: download-file {url} -query 'format:<format_id>'"
|
||||||
|
)
|
||||||
|
|
||||||
log(f"", file=sys.stderr)
|
log(f"", file=sys.stderr)
|
||||||
return 0
|
return 0
|
||||||
@@ -1518,7 +1635,7 @@ class Download_File(Cmdlet):
|
|||||||
"url": url,
|
"url": url,
|
||||||
"item_selector": selection_format_id,
|
"item_selector": selection_format_id,
|
||||||
},
|
},
|
||||||
"_selection_args": ["-format", selection_format_id],
|
"_selection_args": ["-query", f"format:{selection_format_id}"],
|
||||||
}
|
}
|
||||||
|
|
||||||
results_list.append(format_dict)
|
results_list.append(format_dict)
|
||||||
@@ -1748,12 +1865,10 @@ class Download_File(Cmdlet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
query_wants_audio = False
|
query_wants_audio = False
|
||||||
|
|
||||||
audio_flag = bool(parsed.get("audio") is True)
|
|
||||||
wants_audio = audio_flag
|
|
||||||
if query_audio is not None:
|
if query_audio is not None:
|
||||||
wants_audio = wants_audio or bool(query_audio)
|
wants_audio = bool(query_audio)
|
||||||
else:
|
else:
|
||||||
wants_audio = wants_audio or bool(query_wants_audio)
|
wants_audio = bool(query_wants_audio)
|
||||||
mode = "audio" if wants_audio else "video"
|
mode = "audio" if wants_audio else "video"
|
||||||
|
|
||||||
clip_ranges, clip_invalid, clip_values = self._parse_clip_ranges_and_apply_items(
|
clip_ranges, clip_invalid, clip_values = self._parse_clip_ranges_and_apply_items(
|
||||||
@@ -1777,8 +1892,8 @@ class Download_File(Cmdlet):
|
|||||||
|
|
||||||
formats_cache: Dict[str, Optional[List[Dict[str, Any]]]] = {}
|
formats_cache: Dict[str, Optional[List[Dict[str, Any]]]] = {}
|
||||||
playlist_items = str(parsed.get("item")) if parsed.get("item") else None
|
playlist_items = str(parsed.get("item")) if parsed.get("item") else None
|
||||||
ytdl_format = parsed.get("format")
|
ytdl_format = None
|
||||||
if not ytdl_format and query_format and not query_wants_audio:
|
if query_format and not query_wants_audio:
|
||||||
try:
|
try:
|
||||||
height_selector = self._format_selector_for_query_height(query_format)
|
height_selector = self._format_selector_for_query_height(query_format)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -1825,7 +1940,7 @@ class Download_File(Cmdlet):
|
|||||||
sample_pipeline = f'download-file "{candidate_url}"'
|
sample_pipeline = f'download-file "{candidate_url}"'
|
||||||
hint = (
|
hint = (
|
||||||
"To select non-interactively, re-run with an explicit format: "
|
"To select non-interactively, re-run with an explicit format: "
|
||||||
"e.g. mm \"{pipeline} -format {fmt} | add-file -store <store>\" or "
|
"e.g. mm \"{pipeline} -query 'format:{fmt}' | add-file -store <store>\" or "
|
||||||
"mm \"{pipeline} -query 'format:{index}' | add-file -store <store>\""
|
"mm \"{pipeline} -query 'format:{index}' | add-file -store <store>\""
|
||||||
).format(
|
).format(
|
||||||
pipeline=sample_pipeline,
|
pipeline=sample_pipeline,
|
||||||
@@ -2735,18 +2850,6 @@ class Download_File(Cmdlet):
|
|||||||
|
|
||||||
downloaded_count = 0
|
downloaded_count = 0
|
||||||
|
|
||||||
# Special-case: support selection-inserted magnet-id arg to drive provider downloads
|
|
||||||
magnet_ret = self._process_magnet_id(
|
|
||||||
parsed=parsed,
|
|
||||||
registry=registry,
|
|
||||||
config=config,
|
|
||||||
final_output_dir=final_output_dir,
|
|
||||||
progress=progress,
|
|
||||||
quiet_mode=quiet_mode
|
|
||||||
)
|
|
||||||
if magnet_ret is not None:
|
|
||||||
return magnet_ret
|
|
||||||
|
|
||||||
urls_downloaded, early_exit = self._process_explicit_urls(
|
urls_downloaded, early_exit = self._process_explicit_urls(
|
||||||
raw_urls=raw_url,
|
raw_urls=raw_url,
|
||||||
final_output_dir=final_output_dir,
|
final_output_dir=final_output_dir,
|
||||||
@@ -2800,104 +2903,6 @@ class Download_File(Cmdlet):
|
|||||||
pass
|
pass
|
||||||
progress.close_local_ui(force_complete=True)
|
progress.close_local_ui(force_complete=True)
|
||||||
|
|
||||||
def _process_magnet_id(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
parsed: Dict[str, Any],
|
|
||||||
registry: Dict[str, Any],
|
|
||||||
config: Dict[str, Any],
|
|
||||||
final_output_dir: Path,
|
|
||||||
progress: PipelineProgress,
|
|
||||||
quiet_mode: bool
|
|
||||||
) -> Optional[int]:
|
|
||||||
magnet_id_raw = parsed.get("magnet-id")
|
|
||||||
if not magnet_id_raw:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
magnet_id = int(str(magnet_id_raw).strip())
|
|
||||||
except Exception:
|
|
||||||
log(f"[download-file] invalid magnet-id: {magnet_id_raw}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
get_provider = registry.get("get_provider")
|
|
||||||
provider_name = str(parsed.get("provider") or "alldebrid").strip().lower()
|
|
||||||
provider_obj = None
|
|
||||||
if get_provider is not None:
|
|
||||||
try:
|
|
||||||
provider_obj = get_provider(provider_name, config)
|
|
||||||
except Exception:
|
|
||||||
provider_obj = None
|
|
||||||
|
|
||||||
if provider_obj is None:
|
|
||||||
log(f"[download-file] provider '{provider_name}' not available", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
SearchResult = registry.get("SearchResult")
|
|
||||||
try:
|
|
||||||
if SearchResult is not None:
|
|
||||||
sr = SearchResult(
|
|
||||||
table=provider_name,
|
|
||||||
title=f"magnet-{magnet_id}",
|
|
||||||
path=f"alldebrid:magnet:{magnet_id}",
|
|
||||||
full_metadata={
|
|
||||||
"magnet_id": magnet_id,
|
|
||||||
"provider": provider_name,
|
|
||||||
"provider_view": "files",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
sr = None
|
|
||||||
except Exception:
|
|
||||||
sr = None
|
|
||||||
|
|
||||||
def _on_emit(path: Path, file_url: str, relpath: str, metadata: Dict[str, Any]) -> None:
|
|
||||||
title_hint = metadata.get("name") or relpath or f"magnet-{magnet_id}"
|
|
||||||
self._emit_local_file(
|
|
||||||
downloaded_path=path,
|
|
||||||
source=file_url or f"alldebrid:magnet:{magnet_id}",
|
|
||||||
title_hint=title_hint,
|
|
||||||
tags_hint=None,
|
|
||||||
media_kind_hint="file",
|
|
||||||
full_metadata=metadata,
|
|
||||||
progress=progress,
|
|
||||||
config=config,
|
|
||||||
provider_hint=provider_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
downloaded_extra = provider_obj.download_items(
|
|
||||||
sr,
|
|
||||||
final_output_dir,
|
|
||||||
emit=_on_emit,
|
|
||||||
progress=progress,
|
|
||||||
quiet_mode=quiet_mode,
|
|
||||||
path_from_result=coerce_to_path,
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
except TypeError:
|
|
||||||
downloaded_extra = provider_obj.download_items(
|
|
||||||
sr,
|
|
||||||
final_output_dir,
|
|
||||||
emit=_on_emit,
|
|
||||||
progress=progress,
|
|
||||||
quiet_mode=quiet_mode,
|
|
||||||
path_from_result=coerce_to_path,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
log(f"[download-file] failed to download magnet {magnet_id}: {exc}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if downloaded_extra:
|
|
||||||
debug(f"[download-file] AllDebrid magnet {magnet_id} emitted {downloaded_extra} files")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
log(
|
|
||||||
f"[download-file] AllDebrid magnet {magnet_id} produced no downloads",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def _maybe_show_provider_picker(
|
def _maybe_show_provider_picker(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -75,6 +75,17 @@ class Get_Url(Cmdlet):
|
|||||||
|
|
||||||
return url.lower()
|
return url.lower()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_url_pattern(value: str) -> bool:
|
||||||
|
v = str(value or "").strip().lower()
|
||||||
|
if not v:
|
||||||
|
return False
|
||||||
|
if "://" in v:
|
||||||
|
return True
|
||||||
|
if v.startswith(("magnet:", "torrent:", "ytdl:", "tidal:", "ftp:", "sftp:", "file:")):
|
||||||
|
return True
|
||||||
|
return "." in v and "/" in v
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _match_url_pattern(url: str, pattern: str) -> bool:
|
def _match_url_pattern(url: str, pattern: str) -> bool:
|
||||||
"""Match URL against pattern with wildcard support.
|
"""Match URL against pattern with wildcard support.
|
||||||
@@ -82,10 +93,14 @@ class Get_Url(Cmdlet):
|
|||||||
Strips protocol/www from both URL and pattern before matching.
|
Strips protocol/www from both URL and pattern before matching.
|
||||||
Supports * and ? wildcards.
|
Supports * and ? wildcards.
|
||||||
"""
|
"""
|
||||||
|
raw_pattern = str(pattern or "").strip()
|
||||||
normalized_url = Get_Url._normalize_url_for_search(url)
|
normalized_url = Get_Url._normalize_url_for_search(url)
|
||||||
normalized_pattern = Get_Url._normalize_url_for_search(pattern)
|
normalized_pattern = Get_Url._normalize_url_for_search(raw_pattern)
|
||||||
|
|
||||||
has_wildcards = any(ch in normalized_pattern for ch in ("*", "?"))
|
looks_like_url = Get_Url._looks_like_url_pattern(raw_pattern)
|
||||||
|
has_wildcards = "*" in normalized_pattern or (
|
||||||
|
not looks_like_url and "?" in normalized_pattern
|
||||||
|
)
|
||||||
if has_wildcards:
|
if has_wildcards:
|
||||||
return fnmatch(normalized_url, normalized_pattern)
|
return fnmatch(normalized_url, normalized_pattern)
|
||||||
|
|
||||||
@@ -324,25 +339,58 @@ class Get_Url(Cmdlet):
|
|||||||
# This avoids the expensive/incorrect "search('*')" scan.
|
# This avoids the expensive/incorrect "search('*')" scan.
|
||||||
try:
|
try:
|
||||||
raw_pattern = str(pattern or "").strip()
|
raw_pattern = str(pattern or "").strip()
|
||||||
has_wildcards = any(ch in raw_pattern for ch in ("*", "?"))
|
looks_like_url = self._looks_like_url_pattern(raw_pattern)
|
||||||
|
has_wildcards = "*" in raw_pattern or (
|
||||||
|
not looks_like_url and "?" in raw_pattern
|
||||||
|
)
|
||||||
|
|
||||||
# If this is a Hydrus backend and the pattern is a single URL,
|
# If this is a Hydrus backend and the pattern is a single URL,
|
||||||
# normalize it through the official API. Skip for bare domains.
|
# normalize it through the official API. Skip for bare domains.
|
||||||
normalized_url = None
|
normalized_url = None
|
||||||
looks_like_url = (
|
normalized_search_pattern = None
|
||||||
"://" in raw_pattern or raw_pattern.startswith("magnet:")
|
if not has_wildcards and looks_like_url:
|
||||||
)
|
normalized_search_pattern = self._normalize_url_for_search(
|
||||||
if not has_wildcards and looks_like_url and hasattr(backend, "get_url_info"):
|
raw_pattern
|
||||||
try:
|
)
|
||||||
info = backend.get_url_info(raw_pattern) # type: ignore[attr-defined]
|
if (
|
||||||
if isinstance(info, dict):
|
normalized_search_pattern
|
||||||
norm = info.get("normalised_url") or info.get("normalized_url")
|
and normalized_search_pattern != raw_pattern
|
||||||
if isinstance(norm, str) and norm.strip():
|
):
|
||||||
normalized_url = norm.strip()
|
debug(
|
||||||
except Exception:
|
"get-url normalized raw pattern: %s -> %s",
|
||||||
normalized_url = None
|
raw_pattern,
|
||||||
|
normalized_search_pattern,
|
||||||
|
)
|
||||||
|
if hasattr(backend, "get_url_info"):
|
||||||
|
try:
|
||||||
|
info = backend.get_url_info(raw_pattern) # type: ignore[attr-defined]
|
||||||
|
if isinstance(info, dict):
|
||||||
|
norm = (
|
||||||
|
info.get("normalised_url")
|
||||||
|
or info.get("normalized_url")
|
||||||
|
)
|
||||||
|
if isinstance(norm, str) and norm.strip():
|
||||||
|
normalized_url = self._normalize_url_for_search(
|
||||||
|
norm.strip()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if (
|
||||||
|
normalized_url
|
||||||
|
and normalized_url != normalized_search_pattern
|
||||||
|
and normalized_url != raw_pattern
|
||||||
|
):
|
||||||
|
debug(
|
||||||
|
"get-url normalized backend result: %s -> %s",
|
||||||
|
raw_pattern,
|
||||||
|
normalized_url,
|
||||||
|
)
|
||||||
|
|
||||||
target_pattern = normalized_url or raw_pattern
|
target_pattern = (
|
||||||
|
normalized_url
|
||||||
|
or normalized_search_pattern
|
||||||
|
or raw_pattern
|
||||||
|
)
|
||||||
if has_wildcards or not target_pattern:
|
if has_wildcards or not target_pattern:
|
||||||
search_query = "url:*"
|
search_query = "url:*"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ def format_for_table_selection(
|
|||||||
|
|
||||||
This helper formats a single format from list_formats() into the shape
|
This helper formats a single format from list_formats() into the shape
|
||||||
expected by the ResultTable system, ready for user selection and routing
|
expected by the ResultTable system, ready for user selection and routing
|
||||||
to download-file with -format argument.
|
to download-file with -query "format:<id>".
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fmt: Format dict from yt-dlp
|
fmt: Format dict from yt-dlp
|
||||||
@@ -403,9 +403,9 @@ def format_for_table_selection(
|
|||||||
"format_id": format_id,
|
"format_id": format_id,
|
||||||
"url": url,
|
"url": url,
|
||||||
"item_selector": selection_format_id,
|
"item_selector": selection_format_id,
|
||||||
"_selection_args": ["-format", selection_format_id],
|
"_selection_args": ["-query", f"format:{selection_format_id}"],
|
||||||
},
|
},
|
||||||
"_selection_args": ["-format", selection_format_id],
|
"_selection_args": ["-query", f"format:{selection_format_id}"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user