From 6c0a1b44153f21e51ab8e6e1a8455699a5f1830c Mon Sep 17 00:00:00 2001 From: Nose Date: Sat, 23 May 2026 14:20:12 -0700 Subject: [PATCH] update alldebrid plugin --- cmdlet/file/download.py | 17 ++- plugins/alldebrid/__init__.py | 210 ++++++++++++++++--------------- plugins/alldebrid/alldebrid.json | 61 +++------ 3 files changed, 146 insertions(+), 142 deletions(-) diff --git a/cmdlet/file/download.py b/cmdlet/file/download.py index 33e3e03..5f9fb3b 100644 --- a/cmdlet/file/download.py +++ b/cmdlet/file/download.py @@ -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: diff --git a/plugins/alldebrid/__init__.py b/plugins/alldebrid/__init__.py index 0320030..271619b 100644 --- a/plugins/alldebrid/__init__.py +++ b/plugins/alldebrid/__init__.py @@ -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) diff --git a/plugins/alldebrid/alldebrid.json b/plugins/alldebrid/alldebrid.json index a09cadc..7cf902c 100644 --- a/plugins/alldebrid/alldebrid.json +++ b/plugins/alldebrid/alldebrid.json @@ -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",