update alldebrid plugin

This commit is contained in:
2026-05-23 14:20:12 -07:00
parent a8cdd09e1e
commit 6c0a1b4415
3 changed files with 146 additions and 142 deletions
+111 -99
View File
@@ -5,7 +5,6 @@ import json
import re
import sys
import time
import shutil
import tempfile
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Callable, Tuple
@@ -17,10 +16,12 @@ from PluginCore.base import Provider, SearchResult
from SYS.plugin_helpers import TablePluginMixin
from SYS.item_accessors import get_field as _extract_value
from SYS.utils import sanitize_filename
from SYS.logger import log, debug, debug_panel
from SYS.logger import log
from SYS.models import DownloadError, PipeObject
_HOSTS_CACHE_TTL_SECONDS = 24 * 60 * 60
_RUNTIME_HOSTS_PAYLOAD: Optional[Dict[str, Any]] = None
_RUNTIME_HOSTS_FETCHED_AT: float = 0.0
def _plugin_dir() -> Path:
@@ -61,18 +62,39 @@ def _resolve_hosts_cache_path() -> Path:
for legacy in _legacy_hosts_cache_paths():
try:
if not legacy.exists() or not legacy.is_file():
continue
path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(legacy, path)
return path
if legacy.exists() and legacy.is_file():
return legacy
except Exception:
continue
return path
def _runtime_hosts_payload_is_fresh() -> bool:
if not isinstance(_RUNTIME_HOSTS_PAYLOAD, dict) or not _RUNTIME_HOSTS_PAYLOAD:
return False
try:
return (time.time() - float(_RUNTIME_HOSTS_FETCHED_AT)) < _HOSTS_CACHE_TTL_SECONDS
except Exception:
return False
def _load_hosts_payload() -> Optional[Dict[str, Any]]:
if _runtime_hosts_payload_is_fresh():
return _RUNTIME_HOSTS_PAYLOAD
path = _resolve_hosts_cache_path()
try:
if not path.exists() or not path.is_file():
return None
payload = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
return payload if isinstance(payload, dict) else None
def _load_cached_domains(category: str) -> List[str]:
"""Load cached domain list from the plugin-local alldebrid.json cache.
"""Load domain list from the runtime cache or bundled alldebrid.json snapshot.
category: "hosts" | "streams" | "redirectors"
"""
@@ -81,14 +103,7 @@ def _load_cached_domains(category: str) -> List[str]:
if wanted not in {"hosts", "streams", "redirectors"}:
return []
path = _resolve_hosts_cache_path()
try:
if not path.exists() or not path.is_file():
return []
payload = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return []
payload = _load_hosts_payload()
if not isinstance(payload, dict):
return []
@@ -149,29 +164,6 @@ def _load_cached_hoster_domains() -> List[str]:
return _load_cached_domains("hosts")
def _save_cached_hosts_payload(payload: Dict[str, Any]) -> None:
path = _hosts_cache_path()
try:
path.parent.mkdir(parents=True, exist_ok=True)
except Exception:
return
try:
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception:
return
def _cache_is_fresh() -> bool:
path = _resolve_hosts_cache_path()
try:
if not path.exists() or not path.is_file():
return False
mtime = float(path.stat().st_mtime)
return (time.time() - mtime) < _HOSTS_CACHE_TTL_SECONDS
except Exception:
return False
def _fetch_hosts_payload_v4_hosts() -> Optional[Dict[str, Any]]:
"""Fetch the public AllDebrid hosts payload.
@@ -192,13 +184,16 @@ def _fetch_hosts_payload_v4_hosts() -> Optional[Dict[str, Any]]:
def refresh_alldebrid_hoster_cache(*, force: bool = False) -> None:
"""Refresh the on-disk cache of host domains (best-effort)."""
if (not force) and _cache_is_fresh():
"""Refresh the in-memory host-domain cache for the current process."""
global _RUNTIME_HOSTS_PAYLOAD, _RUNTIME_HOSTS_FETCHED_AT
if (not force) and _runtime_hosts_payload_is_fresh():
return
payload = _fetch_hosts_payload_v4_hosts()
if isinstance(payload, dict) and payload:
_save_cached_hosts_payload(payload)
_RUNTIME_HOSTS_PAYLOAD = payload
_RUNTIME_HOSTS_FETCHED_AT = time.time()
def _get_debrid_api_key(config: Dict[str, Any]) -> Optional[str]:
@@ -387,39 +382,69 @@ def _looks_like_torrent_source(candidate: str) -> bool:
return False
def _dispatch_alldebrid_magnet_search(
def _build_queued_magnet_item(
*,
magnet_spec: str,
magnet_id: int,
config: Dict[str, Any],
) -> None:
try:
from cmdlet.file.search import CMDLET as _SEARCH_FILE_CMDLET
magnet_info: Dict[str, Any],
) -> Dict[str, Any]:
title = str(
magnet_info.get("filename")
or magnet_info.get("name")
or magnet_info.get("hash")
or f"magnet-{magnet_id}"
).strip() or f"magnet-{magnet_id}"
status_label = str(magnet_info.get("status") or "queued").strip() or "queued"
status_tag = re.sub(r"[^a-z0-9]+", "-", status_label.lower()).strip("-") or "queued"
exec_fn = getattr(_SEARCH_FILE_CMDLET, "exec", None)
if callable(exec_fn):
exec_fn(
None,
["-plugin", "alldebrid", f"ID={magnet_id}"],
config,
)
except Exception:
pass
debug(f"[alldebrid] Sent magnet {magnet_id} to AllDebrid for download")
metadata: Dict[str, Any] = {
"magnet_id": magnet_id,
"provider": "alldebrid",
"provider_view": "files",
"magnet_spec": magnet_spec,
"source_url": magnet_spec,
"status": status_label,
}
for key in ("filename", "name", "hash", "size", "statusCode", "ready"):
value = magnet_info.get(key)
if value is not None:
metadata[key] = value
return {
"table": "alldebrid",
"provider": "alldebrid",
"plugin": "alldebrid",
"path": f"{_ALD_MAGNET_PREFIX}{magnet_id}",
"title": title,
"detail": f"Queued in AllDebrid ({status_label})",
"media_kind": "folder",
"tag": ["folder", f"status:{status_tag}", "provider:alldebrid"],
"columns": [
("Title", title),
("Status", status_label),
("Provider", "alldebrid"),
("Magnet ID", magnet_id),
],
"full_metadata": metadata,
"source_url": magnet_spec,
"_selection_action": ["search-file", "-plugin", "alldebrid", f"ID={magnet_id}"],
}
def prepare_magnet(
magnet_spec: str,
config: Dict[str, Any],
) -> tuple[Optional[AllDebridClient], Optional[int]]:
) -> tuple[Optional[AllDebridClient], Optional[int], Dict[str, Any]]:
api_key = _get_debrid_api_key(config or {})
if not api_key:
log("AllDebrid API key not configured. Use .config to set it.", file=sys.stderr)
return None, None
return None, None, {}
try:
client = AllDebridClient(api_key)
except Exception as exc:
log(f"Failed to initialize AllDebrid client: {exc}", file=sys.stderr)
return None, None
return None, None, {}
try:
magnet_info = client.magnet_add(magnet_spec)
@@ -427,13 +452,12 @@ def prepare_magnet(
magnet_id = int(magnet_id_val)
if magnet_id <= 0:
log(f"AllDebrid magnet submission failed: {magnet_info}", file=sys.stderr)
return None, None
return None, None, {}
except Exception as exc:
log(f"Failed to submit magnet to AllDebrid: {exc}", file=sys.stderr)
return None, None
return None, None, {}
_dispatch_alldebrid_magnet_search(magnet_id, config)
return client, magnet_id
return client, magnet_id, magnet_info if isinstance(magnet_info, dict) else {}
def _flatten_files_with_relpath(items: Any) -> Iterable[Dict[str, Any]]:
@@ -457,7 +481,7 @@ def download_magnet(
path_from_result: Callable[[Any], Path],
on_emit: Callable[[Path, str, str, Dict[str, Any]], None],
) -> tuple[int, Optional[int]]:
client, magnet_id = prepare_magnet(magnet_spec, config)
client, magnet_id, _magnet_info = prepare_magnet(magnet_spec, config)
if client is None or magnet_id is None:
return 0, None
@@ -789,8 +813,20 @@ class AllDebrid(TablePluginMixin, Provider):
cfg = self.config if isinstance(self.config, dict) else {}
try:
prepare_magnet(spec, cfg)
return True, None
_client, magnet_id, magnet_info = prepare_magnet(spec, cfg)
if magnet_id is None:
return False, None
return True, {
"action": "emit_items",
"items": [
_build_queued_magnet_item(
magnet_spec=str(url),
magnet_id=int(magnet_id),
magnet_info=magnet_info,
)
],
"exit_code": 0,
}
except Exception:
return False, None
@@ -804,14 +840,6 @@ class AllDebrid(TablePluginMixin, Provider):
dom = str(d or "").strip().lower()
if dom and dom not in patterns:
patterns.append(dom)
debug_panel(
"AllDebrid host cache",
[
("cached_domains", len(cached)),
("total_patterns", len(patterns)),
],
border_style="magenta",
)
except Exception:
pass
return tuple(patterns)
@@ -866,8 +894,6 @@ class AllDebrid(TablePluginMixin, Provider):
log(f"[alldebrid] Failed to init client: {exc}", file=sys.stderr)
return None
log(f"[alldebrid] download routing target={target}", file=sys.stderr)
# Prefer provider title as the output filename; later we may override if unlocked URL has a better basename.
suggested = sanitize_filename(str(getattr(result, "title", "") or "").strip())
suggested_name = suggested if suggested else None
@@ -914,8 +940,7 @@ class AllDebrid(TablePluginMixin, Provider):
continue
fh.write(chunk)
return dest if dest.exists() else None
except Exception as exc2:
log(f"[alldebrid] raw stream (unlocked) failed: {exc2}", file=sys.stderr)
except Exception:
return None
# Otherwise, use standard downloader with guardrails.
@@ -961,9 +986,8 @@ class AllDebrid(TablePluginMixin, Provider):
unlocked = client.resolve_unlock_link(target, poll=True, max_wait_seconds=45, poll_interval_seconds=5)
if isinstance(unlocked, str) and unlocked.strip().startswith(("http://", "https://")):
unlocked_url = unlocked.strip()
log(f"[alldebrid] unlock -> {unlocked_url}", file=sys.stderr)
except Exception as exc:
log(f"[alldebrid] Failed to unlock link: {exc}", file=sys.stderr)
except Exception:
pass
if unlocked_url != target:
# Prefer filename from unlocked URL path.
@@ -976,20 +1000,14 @@ class AllDebrid(TablePluginMixin, Provider):
# When using an unlocked URL different from the original hoster, stream it directly and do NOT fall back to the public URL.
allow_html = unlocked_url != target
log(
f"[alldebrid] downloading from {unlocked_url} (allow_html={allow_html})",
file=sys.stderr,
)
downloaded = _download_unlocked(unlocked_url, allow_html=allow_html)
if downloaded:
log(f"[alldebrid] downloaded -> {downloaded}", file=sys.stderr)
return downloaded
# If unlock failed entirely and we never changed URL, allow a single attempt on the original target.
if unlocked_url == target:
downloaded = _download_unlocked(target, allow_html=False)
if downloaded:
log(f"[alldebrid] downloaded (original target) -> {downloaded}", file=sys.stderr)
return downloaded
return None
@@ -1172,7 +1190,6 @@ class AllDebrid(TablePluginMixin, Provider):
if active_id is not None:
try:
magnet_id = int(active_id)
debug(f"[download_items] Found magnet_id {magnet_id}, downloading files directly")
cfg = config if isinstance(config, dict) else (self.config or {})
count = self._download_magnet_by_id(
magnet_id,
@@ -1183,10 +1200,9 @@ class AllDebrid(TablePluginMixin, Provider):
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}")
except Exception:
pass
spec = self._resolve_magnet_spec_from_result(result)
if not spec:
@@ -1329,11 +1345,8 @@ class AllDebrid(TablePluginMixin, Provider):
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")
except Exception:
pass
rel_path_obj = Path(relpath)
target_path = adjust_output_dir_for_alldebrid(
@@ -1354,8 +1367,7 @@ class AllDebrid(TablePluginMixin, Provider):
suggested_filename=suggested_name,
pipeline_progress=progress,
)
except Exception as exc:
debug(f"Failed to download {file_url}: {exc}")
except Exception:
continue
downloaded_path = path_from_result(result_obj)