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
+16 -1
View File
@@ -1460,6 +1460,8 @@ class Download_File(Cmdlet):
) -> List[Dict[str, Any]]:
if not canonical_url:
return []
if not cls._supports_storage_duplicate_lookup(canonical_url):
return []
config_dict = config if isinstance(config, dict) else {}
refs: List[Dict[str, Any]] = []
@@ -1691,6 +1693,20 @@ class Download_File(Cmdlet):
return storage, hydrus_available
@staticmethod
def _supports_storage_duplicate_lookup(raw_url: str) -> bool:
text = str(raw_url or "").strip()
if not text:
return False
try:
parsed = urlparse(text)
except Exception:
parsed = None
scheme = str(getattr(parsed, "scheme", "") or "").strip().lower()
return scheme in {"http", "https", "ftp", "ftps"}
@staticmethod
def _filter_supported_urls(raw_urls: Sequence[str]) -> tuple[List[str], List[str]]:
"""Split explicit URLs into supported and unsupported buckets."""
@@ -2690,7 +2706,6 @@ class Download_File(Cmdlet):
self._maybe_render_download_details(config=config)
return 0
log("No downloads completed", file=sys.stderr)
return 1
except Exception as e:
+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)
+19 -42
View File
@@ -71,7 +71,7 @@
"(wayupload\\.com/[a-z0-9]{12}\\.html)"
],
"regexp": "(turbobit5?a?\\.(net|cc|com)/([a-z0-9]{12}))|(turbobif\\.(net|cc|com)/([a-z0-9]{12}))|(turb[o]?\\.(to|cc|pw)\\/([a-z0-9]{12}))|(turbobit\\.(net|cc)/download/free/([a-z0-9]{12}))|((trbbt|tourbobit|torbobit|tbit|turbobita|trbt)\\.(net|cc|com|to)/([a-z0-9]{12}))|((turbobit\\.cloud/turbo/[a-z0-9]+))|((wayupload\\.com/[a-z0-9]{12}\\.html))",
"status": false
"status": true
},
"hitfile": {
"name": "hitfile",
@@ -92,7 +92,7 @@
"(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": false
"status": true
},
"mega": {
"name": "mega",
@@ -165,40 +165,6 @@
"alldebrid\\.com/f/([a-zA-Z0-9\\_\\-]+)"
]
},
"clicknupload": {
"name": "clicknupload",
"type": "premium",
"domains": [
"clicknupload.click",
"clickndownload.cc",
"clickndownload.click",
"clickndownload.link",
"clickndownload.name",
"clickndownload.org",
"clickndownload.space",
"clickndownload.xyz",
"clicknupload.cc",
"clicknupload.club",
"clicknupload.co",
"clicknupload.download",
"clicknupload.link",
"clicknupload.name",
"clicknupload.one",
"clicknupload.online",
"clicknupload.org",
"clicknupload.red",
"clicknupload.site",
"clicknupload.space",
"clicknupload.to",
"clicknupload.vip",
"clicknupload.xyz"
],
"regexps": [
"clicknupload\\.(link|org|red|co|cc|vip|to|club|click|xyz|online|download|site|space|one|name)/([a-zA-Z0-9]+)",
"clickndownload\\.(org|space|link|click|link|xyz|name|cc)/([a-zA-Z0-9]+)"
],
"regexp": "(clicknupload\\.(link|org|red|co|cc|vip|to|club|click|xyz|online|download|site|space|one|name)/([a-zA-Z0-9]+))|(clickndownload\\.(org|space|link|click|link|xyz|name|cc)/([a-zA-Z0-9]+))"
},
"clipwatching": {
"name": "clipwatching",
"type": "premium",
@@ -213,7 +179,7 @@
},
"dailyuploads": {
"name": "dailyuploads",
"type": "free",
"type": "premium",
"domains": [
"dailyuploads.net"
],
@@ -375,7 +341,7 @@
"(filespace\\.com/[a-zA-Z0-9]{12})"
],
"regexp": "(filespace\\.com/fd/([a-zA-Z0-9]{12}))|((filespace\\.com/[a-zA-Z0-9]{12}))",
"status": false
"status": true
},
"filezip": {
"name": "filezip",
@@ -463,7 +429,7 @@
"isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12})"
],
"regexp": "((isra\\.cloud/[0-9a-zA-Z]{12}))|(isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12}))",
"status": false,
"status": true,
"hardRedirect": [
"isra\\.cloud/([0-9a-zA-Z]{12})"
]
@@ -482,7 +448,7 @@
"(katfile\\.com/[0-9a-zA-Z]{12})"
],
"regexp": "(katfile\\.(cloud|online|vip|ws|space)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
"status": false
"status": true
},
"mediafire": {
"name": "mediafire",
@@ -695,6 +661,17 @@
"uploadrar.com/([0-9a-zA-Z]{12})"
]
},
"uploady": {
"name": "uploady",
"type": "premium",
"domains": [
"uploady.io"
],
"regexps": [
"(uploady\\.io/[0-9a-zA-Z]{12})"
],
"regexp": "(uploady\\.io/[0-9a-zA-Z]{12})"
},
"usersdrive": {
"name": "usersdrive",
"type": "free",
@@ -17929,9 +17906,9 @@
"generic.tld"
],
"regexps": [
"((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|clicknupload.click|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
"((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|uploady.io|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
],
"regexp": "((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|clicknupload.click|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
"regexp": "((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|uploady.io|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
},
"google": {
"name": "google",