h
This commit is contained in:
@@ -3,3 +3,9 @@
|
||||
Concrete provider implementations live in this package.
|
||||
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):
|
||||
# Magnet URIs should be routed through this provider.
|
||||
TABLE_AUTO_STAGES = {"alldebrid": ["download-file"]}
|
||||
AUTO_STAGE_USE_SELECTION_ARGS = True
|
||||
URL = ("magnet:",)
|
||||
URL_DOMAINS = ()
|
||||
|
||||
@@ -818,6 +819,29 @@ class AllDebrid(Provider):
|
||||
path_from_result: Callable[[Any], Path],
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
) -> 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)
|
||||
if not spec:
|
||||
return 0
|
||||
@@ -885,6 +909,97 @@ class AllDebrid(Provider):
|
||||
enriched["_relpath"] = relpath
|
||||
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(
|
||||
self,
|
||||
query: str,
|
||||
@@ -1032,10 +1147,9 @@ class AllDebrid(Provider):
|
||||
"file": file_node,
|
||||
"provider": "alldebrid",
|
||||
"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(
|
||||
SearchResult(
|
||||
@@ -1147,6 +1261,9 @@ class AllDebrid(Provider):
|
||||
"provider": "alldebrid",
|
||||
"provider_view": "folders",
|
||||
"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})
|
||||
except Exception:
|
||||
pass
|
||||
table.set_source_command(
|
||||
"search-file",
|
||||
["-provider", "alldebrid", "-open", str(magnet_id), "-query", "*"],
|
||||
)
|
||||
table.set_source_command("download-file", ["-provider", "alldebrid"])
|
||||
|
||||
results_payload: List[Dict[str, Any]] = []
|
||||
for r in files or []:
|
||||
@@ -1280,3 +1394,177 @@ class AllDebrid(Provider):
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user