h
This commit is contained in:
@@ -3,3 +3,9 @@
|
|||||||
Concrete provider implementations live in this package.
|
Concrete provider implementations live in this package.
|
||||||
The public entrypoint/registry is ProviderCore.registry.
|
The public entrypoint/registry is ProviderCore.registry.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Register providers with the strict ResultTable adapter system
|
||||||
|
try:
|
||||||
|
from . import alldebrid
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -544,6 +544,7 @@ def adjust_output_dir_for_alldebrid(
|
|||||||
class AllDebrid(Provider):
|
class AllDebrid(Provider):
|
||||||
# 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
|
||||||
URL = ("magnet:",)
|
URL = ("magnet:",)
|
||||||
URL_DOMAINS = ()
|
URL_DOMAINS = ()
|
||||||
|
|
||||||
@@ -818,6 +819,29 @@ class AllDebrid(Provider):
|
|||||||
path_from_result: Callable[[Any], Path],
|
path_from_result: Callable[[Any], Path],
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[Dict[str, Any]] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
|
# Check if this is a direct magnet_id from the account (e.g., from selector)
|
||||||
|
full_metadata = getattr(result, "full_metadata", None) or {}
|
||||||
|
if isinstance(full_metadata, dict):
|
||||||
|
magnet_id_direct = full_metadata.get("magnet_id")
|
||||||
|
if magnet_id_direct is not None:
|
||||||
|
try:
|
||||||
|
magnet_id = int(magnet_id_direct)
|
||||||
|
debug(f"[download_items] Found magnet_id {magnet_id} in metadata, downloading files directly")
|
||||||
|
cfg = config if isinstance(config, dict) else (self.config or {})
|
||||||
|
count = self._download_magnet_by_id(
|
||||||
|
magnet_id,
|
||||||
|
output_dir,
|
||||||
|
cfg,
|
||||||
|
emit,
|
||||||
|
progress,
|
||||||
|
quiet_mode,
|
||||||
|
path_from_result,
|
||||||
|
)
|
||||||
|
debug(f"[download_items] _download_magnet_by_id returned {count}")
|
||||||
|
return count
|
||||||
|
except Exception as e:
|
||||||
|
debug(f"[download_items] Failed to download by magnet_id: {e}")
|
||||||
|
|
||||||
spec = self._resolve_magnet_spec_from_result(result)
|
spec = self._resolve_magnet_spec_from_result(result)
|
||||||
if not spec:
|
if not spec:
|
||||||
return 0
|
return 0
|
||||||
@@ -885,6 +909,97 @@ class AllDebrid(Provider):
|
|||||||
enriched["_relpath"] = relpath
|
enriched["_relpath"] = relpath
|
||||||
yield enriched
|
yield enriched
|
||||||
|
|
||||||
|
def _download_magnet_by_id(
|
||||||
|
self,
|
||||||
|
magnet_id: int,
|
||||||
|
output_dir: Path,
|
||||||
|
config: Dict[str, Any],
|
||||||
|
emit: Callable[[Path, str, str, Dict[str, Any]], None],
|
||||||
|
progress: Any,
|
||||||
|
quiet_mode: bool,
|
||||||
|
path_from_result: Callable[[Any], Path],
|
||||||
|
) -> int:
|
||||||
|
"""Download files from an existing magnet ID (already in account)."""
|
||||||
|
api_key = _get_debrid_api_key(config or {})
|
||||||
|
if not api_key:
|
||||||
|
log("AllDebrid API key not configured", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = AllDebridClient(api_key)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Failed to init AllDebrid client: {exc}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
files_result = client.magnet_links([magnet_id])
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Failed to list files for magnet {magnet_id}: {exc}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
magnet_files = files_result.get(str(magnet_id), {}) if isinstance(files_result, dict) else {}
|
||||||
|
file_nodes = magnet_files.get("files") if isinstance(magnet_files, dict) else []
|
||||||
|
if not file_nodes:
|
||||||
|
log(f"AllDebrid magnet {magnet_id} has no files", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
downloaded = 0
|
||||||
|
for node in self._flatten_files(file_nodes):
|
||||||
|
locked_url = str(node.get("l") or node.get("link") or "").strip()
|
||||||
|
file_name = str(node.get("n") or node.get("name") or "").strip()
|
||||||
|
relpath = str(node.get("_relpath") or file_name or "").strip()
|
||||||
|
|
||||||
|
if not locked_url or not relpath:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Unlock the URL if it's restricted (contains /f/)
|
||||||
|
file_url = locked_url
|
||||||
|
if "/f/" in locked_url:
|
||||||
|
try:
|
||||||
|
unlocked = client.unlock_link(locked_url)
|
||||||
|
if unlocked:
|
||||||
|
file_url = unlocked
|
||||||
|
debug(f"[alldebrid] Unlocked restricted link for {file_name}")
|
||||||
|
else:
|
||||||
|
debug(f"[alldebrid] Failed to unlock {locked_url}, trying locked URL")
|
||||||
|
except Exception as exc:
|
||||||
|
debug(f"[alldebrid] unlock_link failed: {exc}, trying locked URL")
|
||||||
|
|
||||||
|
target_path = output_dir
|
||||||
|
rel_path_obj = Path(relpath)
|
||||||
|
if rel_path_obj.parent:
|
||||||
|
target_path = output_dir / rel_path_obj.parent
|
||||||
|
try:
|
||||||
|
target_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
target_path = output_dir
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_obj = _download_direct_file(
|
||||||
|
file_url,
|
||||||
|
target_path,
|
||||||
|
quiet=quiet_mode,
|
||||||
|
suggested_filename=rel_path_obj.name,
|
||||||
|
pipeline_progress=progress,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
debug(f"Failed to download {file_url}: {exc}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
downloaded_path = path_from_result(result_obj)
|
||||||
|
metadata = {
|
||||||
|
"magnet_id": magnet_id,
|
||||||
|
"relpath": relpath,
|
||||||
|
"name": file_name,
|
||||||
|
}
|
||||||
|
emit(downloaded_path, file_url, relpath, metadata)
|
||||||
|
downloaded += 1
|
||||||
|
|
||||||
|
if downloaded == 0:
|
||||||
|
log(f"AllDebrid magnet {magnet_id} produced no downloads", file=sys.stderr)
|
||||||
|
|
||||||
|
return downloaded
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
@@ -1032,10 +1147,9 @@ class AllDebrid(Provider):
|
|||||||
"file": file_node,
|
"file": file_node,
|
||||||
"provider": "alldebrid",
|
"provider": "alldebrid",
|
||||||
"provider_view": "files",
|
"provider_view": "files",
|
||||||
|
"_selection_args": ["-magnet-id", str(magnet_id)],
|
||||||
|
"_selection_action": ["download-file", "-provider", "alldebrid", "-magnet-id", str(magnet_id)],
|
||||||
}
|
}
|
||||||
if file_url:
|
|
||||||
metadata["_selection_args"] = ["-url", file_url]
|
|
||||||
metadata["_selection_action"] = ["download-file", "-url", file_url]
|
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
SearchResult(
|
SearchResult(
|
||||||
@@ -1147,6 +1261,9 @@ class AllDebrid(Provider):
|
|||||||
"provider": "alldebrid",
|
"provider": "alldebrid",
|
||||||
"provider_view": "folders",
|
"provider_view": "folders",
|
||||||
"magnet_name": magnet_name,
|
"magnet_name": magnet_name,
|
||||||
|
# Selection metadata: allow @N expansion to drive downloads directly
|
||||||
|
"_selection_args": ["-magnet-id", str(magnet_id)],
|
||||||
|
"_selection_action": ["download-file", "-provider", "alldebrid", "-magnet-id", str(magnet_id)],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1247,10 +1364,7 @@ class AllDebrid(Provider):
|
|||||||
table.set_table_metadata({"provider": "alldebrid", "view": "files", "magnet_id": magnet_id})
|
table.set_table_metadata({"provider": "alldebrid", "view": "files", "magnet_id": magnet_id})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
table.set_source_command(
|
table.set_source_command("download-file", ["-provider", "alldebrid"])
|
||||||
"search-file",
|
|
||||||
["-provider", "alldebrid", "-open", str(magnet_id), "-query", "*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
results_payload: List[Dict[str, Any]] = []
|
results_payload: List[Dict[str, Any]] = []
|
||||||
for r in files or []:
|
for r in files or []:
|
||||||
@@ -1280,3 +1394,177 @@ class AllDebrid(Provider):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from SYS.result_table_adapters import register_provider
|
||||||
|
from SYS.result_table_api import ColumnSpec, ResultModel, metadata_column, title_column
|
||||||
|
|
||||||
|
def _as_payload(item: Any) -> Dict[str, Any]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
return dict(item)
|
||||||
|
try:
|
||||||
|
if hasattr(item, "to_dict"):
|
||||||
|
result = item.to_dict() # type: ignore[attr-defined]
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return result
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
payload: Dict[str, Any] = {}
|
||||||
|
for attr in ("title", "path", "columns", "full_metadata", "table", "source", "size_bytes", "size", "ext"):
|
||||||
|
try:
|
||||||
|
val = getattr(item, attr, None)
|
||||||
|
except Exception:
|
||||||
|
val = None
|
||||||
|
if val is not None:
|
||||||
|
payload.setdefault(attr, val)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_size(value: Any) -> Optional[int]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(float(str(value).strip()))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_columns(columns: Any, metadata: Dict[str, Any]) -> None:
|
||||||
|
if not isinstance(columns, list):
|
||||||
|
return
|
||||||
|
for entry in columns:
|
||||||
|
if not isinstance(entry, (list, tuple)) or len(entry) < 2:
|
||||||
|
continue
|
||||||
|
key, value = entry[0], entry[1]
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
normalized = str(key).replace(" ", "_").strip().lower()
|
||||||
|
if not normalized:
|
||||||
|
continue
|
||||||
|
metadata.setdefault(normalized, value)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_to_model(item: Any) -> ResultModel:
|
||||||
|
payload = _as_payload(item)
|
||||||
|
title = str(payload.get("title") or payload.get("name") or "").strip()
|
||||||
|
if not title:
|
||||||
|
candidate = payload.get("path") or payload.get("detail") or payload.get("magnet_name")
|
||||||
|
title = str(candidate or "").strip()
|
||||||
|
if not title:
|
||||||
|
title = "alldebrid"
|
||||||
|
|
||||||
|
path_val = payload.get("path")
|
||||||
|
if path_val is not None and not isinstance(path_val, str):
|
||||||
|
try:
|
||||||
|
path_val = str(path_val)
|
||||||
|
except Exception:
|
||||||
|
path_val = None
|
||||||
|
|
||||||
|
size_bytes = _coerce_size(payload.get("size_bytes") or payload.get("size") or payload.get("file_size"))
|
||||||
|
metadata: Dict[str, Any] = {}
|
||||||
|
full_metadata = payload.get("full_metadata")
|
||||||
|
if isinstance(full_metadata, dict):
|
||||||
|
metadata.update(full_metadata)
|
||||||
|
_normalize_columns(payload.get("columns"), metadata)
|
||||||
|
|
||||||
|
table_name = str(payload.get("table") or payload.get("source") or "alldebrid").strip().lower()
|
||||||
|
if table_name:
|
||||||
|
metadata.setdefault("table", table_name)
|
||||||
|
metadata.setdefault("source", table_name)
|
||||||
|
metadata.setdefault("provider", table_name)
|
||||||
|
|
||||||
|
ext = payload.get("ext")
|
||||||
|
if not ext and isinstance(path_val, str):
|
||||||
|
try:
|
||||||
|
suffix = Path(path_val).suffix
|
||||||
|
if suffix:
|
||||||
|
ext = suffix.lstrip(".")
|
||||||
|
except Exception:
|
||||||
|
ext = None
|
||||||
|
|
||||||
|
return ResultModel(
|
||||||
|
title=title,
|
||||||
|
path=path_val,
|
||||||
|
ext=str(ext) if ext is not None else None,
|
||||||
|
size_bytes=size_bytes,
|
||||||
|
metadata=metadata,
|
||||||
|
source="alldebrid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _adapter(items: Iterable[Any]) -> Iterable[ResultModel]:
|
||||||
|
for item in items or []:
|
||||||
|
try:
|
||||||
|
model = _convert_to_model(item)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
yield model
|
||||||
|
|
||||||
|
|
||||||
|
def _has_metadata(rows: List[ResultModel], key: str) -> bool:
|
||||||
|
for row in rows:
|
||||||
|
md = row.metadata or {}
|
||||||
|
if key in md:
|
||||||
|
val = md[key]
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
if isinstance(val, str) and not val.strip():
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _columns_factory(rows: List[ResultModel]) -> List[ColumnSpec]:
|
||||||
|
cols = [title_column()]
|
||||||
|
if _has_metadata(rows, "magnet_name"):
|
||||||
|
cols.append(metadata_column("magnet_name", "Magnet"))
|
||||||
|
if _has_metadata(rows, "magnet_id"):
|
||||||
|
cols.append(metadata_column("magnet_id", "Magnet ID"))
|
||||||
|
if _has_metadata(rows, "status"):
|
||||||
|
cols.append(metadata_column("status", "Status"))
|
||||||
|
if _has_metadata(rows, "ready"):
|
||||||
|
cols.append(metadata_column("ready", "Ready"))
|
||||||
|
if _has_metadata(rows, "relpath"):
|
||||||
|
cols.append(metadata_column("relpath", "Relpath"))
|
||||||
|
if _has_metadata(rows, "provider_view"):
|
||||||
|
cols.append(metadata_column("provider_view", "View"))
|
||||||
|
if _has_metadata(rows, "size"):
|
||||||
|
cols.append(metadata_column("size", "Size"))
|
||||||
|
return cols
|
||||||
|
|
||||||
|
|
||||||
|
def _selection_fn(row: ResultModel) -> List[str]:
|
||||||
|
metadata = row.metadata or {}
|
||||||
|
action = metadata.get("_selection_action") or metadata.get("selection_action")
|
||||||
|
if isinstance(action, (list, tuple)) and action:
|
||||||
|
return [str(x) for x in action if x is not None]
|
||||||
|
args = metadata.get("_selection_args") or metadata.get("selection_args")
|
||||||
|
if isinstance(args, (list, tuple)) and args:
|
||||||
|
return [str(x) for x in args if x is not None]
|
||||||
|
view = metadata.get("provider_view") or metadata.get("view") or ""
|
||||||
|
if view == "files":
|
||||||
|
if row.path:
|
||||||
|
return ["-url", row.path]
|
||||||
|
magnet_id = metadata.get("magnet_id")
|
||||||
|
if magnet_id is not None:
|
||||||
|
return ["-magnet-id", str(magnet_id)]
|
||||||
|
if row.path:
|
||||||
|
return ["-url", row.path]
|
||||||
|
return ["-title", row.title or ""]
|
||||||
|
|
||||||
|
|
||||||
|
register_provider(
|
||||||
|
"alldebrid",
|
||||||
|
_adapter,
|
||||||
|
columns=_columns_factory,
|
||||||
|
selection_fn=_selection_fn,
|
||||||
|
metadata={"description": "AllDebrid account provider"},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ class Download_File(Cmdlet):
|
|||||||
alias="a",
|
alias="a",
|
||||||
description="Download audio only (yt-dlp)",
|
description="Download audio only (yt-dlp)",
|
||||||
),
|
),
|
||||||
|
CmdletArg(
|
||||||
|
name="-magnet-id",
|
||||||
|
type="string",
|
||||||
|
description="(internal) AllDebrid magnet id used by provider selection hooks",
|
||||||
|
),
|
||||||
CmdletArg(
|
CmdletArg(
|
||||||
name="format",
|
name="format",
|
||||||
type="string",
|
type="string",
|
||||||
@@ -3789,6 +3794,94 @@ class Download_File(Cmdlet):
|
|||||||
registry = self._load_provider_registry()
|
registry = self._load_provider_registry()
|
||||||
|
|
||||||
downloaded_count = 0
|
downloaded_count = 0
|
||||||
|
|
||||||
|
# Special-case: support selection-inserted magnet-id arg to drive provider downloads
|
||||||
|
magnet_id_raw = parsed.get("magnet-id")
|
||||||
|
if magnet_id_raw:
|
||||||
|
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=self._path_from_download_result,
|
||||||
|
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=self._path_from_download_result,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user