From cdae571385d3fedebe18669f65a0fa986341657b Mon Sep 17 00:00:00 2001 From: Nose Date: Tue, 26 May 2026 19:00:04 -0700 Subject: [PATCH] update --- CLI.py | 12 +- PluginCore/registry.py | 223 ++++++---------------------- cmdlet/file/add.py | 59 ++++---- cmdlet/file/search.py | 11 +- plugins/alldebrid/__init__.py | 1 + plugins/bandcamp/__init__.py | 1 + plugins/fileio/__init__.py | 1 + plugins/hello/__init__.py | 1 + plugins/hifi/__init__.py | 1 + plugins/internetarchive/__init__.py | 1 + plugins/libgen/__init__.py | 1 + plugins/loc/__init__.py | 2 + plugins/matrix/__init__.py | 2 +- plugins/mpv/commands.py | 34 ++--- plugins/openlibrary/__init__.py | 1 + plugins/podcastindex/__init__.py | 1 + plugins/scp/__init__.py | 1 + plugins/soulseek/__init__.py | 1 + plugins/telegram/__init__.py | 1 + plugins/tidal/__init__.py | 1 + plugins/torrent/__init__.py | 1 + plugins/vimm/__init__.py | 1 + plugins/ytdlp/__init__.py | 1 + plugins/zeroxzero/__init__.py | 1 + 24 files changed, 119 insertions(+), 241 deletions(-) diff --git a/CLI.py b/CLI.py index 9b0d09d..bf8c07f 100644 --- a/CLI.py +++ b/CLI.py @@ -505,14 +505,8 @@ class CmdletIntrospection: if normalized_arg == "plugin": canonical_cmd = (cmd_name or "").replace("_", "-").lower() try: - from PluginCore.registry import ( - list_configured_plugin_names_with_capability, - list_plugin_names_with_capability, - list_plugin_names_for_cmdlet, - ) + from PluginCore.registry import list_plugin_names_for_cmdlet except Exception: - list_configured_plugin_names_with_capability = None # type: ignore - list_plugin_names_with_capability = None # type: ignore list_plugin_names_for_cmdlet = None # type: ignore plugin_choices: List[str] = [] @@ -569,10 +563,6 @@ class CmdletIntrospection: ) or [] # Prefer configured plugins first, but still show valid plugin options. plugin_choices = _merge_choice_groups(configured, available) - elif canonical_cmd in {"add-file"} and list_configured_plugin_names_with_capability is not None: - plugin_choices = list_configured_plugin_names_with_capability("upload", config) or [] - elif list_configured_plugin_names_with_capability is not None: - plugin_choices = list_configured_plugin_names_with_capability("search", config) or [] if plugin_choices: return plugin_choices diff --git a/PluginCore/registry.py b/PluginCore/registry.py index dbbd421..55a892c 100644 --- a/PluginCore/registry.py +++ b/PluginCore/registry.py @@ -626,100 +626,6 @@ def is_known_plugin_name(name: str) -> bool: return REGISTRY.has_name(name) -def _supports_search(provider: Provider) -> bool: - return _class_supports_method(provider.__class__, "search", Provider.search) - - -def _supports_upload(provider: Provider) -> bool: - try: - exposed = bool(getattr(provider.__class__, "EXPOSE_AS_FILE_PROVIDER", True)) - except Exception: - exposed = True - return exposed and _class_supports_method(provider.__class__, "upload", Provider.upload) - - -def _supports_download(provider: Provider) -> bool: - return ( - _class_supports_method(provider.__class__, "handle_url", Provider.handle_url) - or _class_supports_method(provider.__class__, "download_url", Provider.download_url) - or _class_supports_method(provider.__class__, "download", Provider.download) - ) - - -def _supports_pipe_result_download(provider: Provider) -> bool: - return _class_supports_method( - provider.__class__, - "resolve_pipe_result_download", - Provider.resolve_pipe_result_download, - ) - - -def _supports_delete_file(provider: Provider) -> bool: - method = getattr(provider.__class__, "delete_file", None) - base_method = getattr(Provider, "delete_file", None) - return callable(method) and method is not base_method - - -def _supports_capability(provider: Provider, capability: str) -> bool: - capability_key = str(capability or "").strip().lower() - if capability_key == "search": - return _supports_search(provider) - if capability_key in {"upload", "file", "file-provider"}: - return _supports_upload(provider) - if capability_key in {"download", "download-file", "download_file"}: - return _supports_download(provider) - if capability_key in {"pipe-download", "pipe_result_download", "pipe-result-download"}: - return _supports_pipe_result_download(provider) - if capability_key in {"delete-file", "delete_file", "delete"}: - return _supports_delete_file(provider) - if capability_key in {"pipe-item-context", "pipe-context"}: - return _class_supports_method( - provider.__class__, - "resolve_pipe_item_context", - Provider.resolve_pipe_item_context, - ) - if capability_key in {"playlist-store", "playback-store"}: - return _class_supports_method( - provider.__class__, - "infer_playlist_store", - Provider.infer_playlist_store, - ) - return False - - -def _info_supports_capability(info: PluginInfo, capability: str) -> bool: - capability_key = str(capability or "").strip().lower() - if capability_key == "search": - return bool(info.supports_search) - if capability_key in {"upload", "file", "file-provider"}: - return bool(info.supports_upload) - if capability_key in {"download", "download-file", "download_file"}: - return bool(info.supports_download) - if capability_key in {"pipe-download", "pipe_result_download", "pipe-result-download"}: - return _class_supports_method( - info.plugin_class, - "resolve_pipe_result_download", - Provider.resolve_pipe_result_download, - ) - if capability_key in {"delete-file", "delete_file", "delete"}: - method = getattr(info.plugin_class, "delete_file", None) - base_method = getattr(Provider, "delete_file", None) - return callable(method) and method is not base_method - if capability_key in {"pipe-item-context", "pipe-context"}: - return _class_supports_method( - info.plugin_class, - "resolve_pipe_item_context", - Provider.resolve_pipe_item_context, - ) - if capability_key in {"playlist-store", "playback-store"}: - return _class_supports_method( - info.plugin_class, - "infer_playlist_store", - Provider.infer_playlist_store, - ) - return False - - def get_plugin(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]: info = REGISTRY.get(name) if info is None: @@ -760,77 +666,60 @@ def list_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]: return availability -def get_plugin_with_capability( +def get_plugin_for_cmdlet( name: str, - capability: str, + cmdlet_name: str, config: Optional[Dict[str, Any]] = None, ) -> Optional[Provider]: - plugin = get_plugin(name, config) - if plugin is None: + info = REGISTRY.get(name) + if info is None: + debug(f"[plugin] Unknown plugin: {name}") return None - if not _supports_capability(plugin, capability): - debug(f"[plugin] Plugin '{name}' does not support capability '{capability}'") + + cmd = str(cmdlet_name or "").strip().lower() + if not cmd or cmd not in info.supported_cmdlets: + debug(f"[plugin] Plugin '{name}' does not declare cmdlet '{cmdlet_name}'") return None - return plugin + + return get_plugin(name, config) -def list_plugins_with_capability( - capability: str, +def list_plugins_for_cmdlet( + cmdlet_name: str, config: Optional[Dict[str, Any]] = None, ) -> Dict[str, bool]: availability: Dict[str, bool] = {} - for info in REGISTRY.iter_plugins(): + for info in REGISTRY.get_plugins_for_cmdlet(cmdlet_name): try: plugin = info.plugin_class(config) - availability[info.canonical_name] = bool( - plugin.validate() and _supports_capability(plugin, capability) - ) + availability[info.canonical_name] = plugin.validate() except Exception: availability[info.canonical_name] = False return availability -def list_plugin_names_with_capability(capability: str) -> List[str]: - return sorted( - info.canonical_name - for info in REGISTRY.iter_plugins() - if _info_supports_capability(info, capability) +def _info_has_configured_plugin_entry( + info: PluginInfo, + cfg: Optional[Dict[str, Any]] = None, + plugin_section: Optional[Dict[str, Any]] = None, +) -> bool: + config_dict = cfg or {} + section: Dict[str, Any] = ( + plugin_section if isinstance(plugin_section, dict) + else (config_dict.get("plugin") or {}) # type: ignore[assignment] ) + if info.is_multi_instance: + try: + plugin_obj = info.plugin_class(config_dict) + instances = plugin_obj.configured_instances() + # Treat explicit multi-instance names as configured, but also allow + # a default/single config block for multi-instance plugins. + return bool(instances or plugin_obj.plugin_config_root()) + except Exception: + return False -def list_configured_plugin_names_with_capability( - capability: str, - config: Optional[Dict[str, Any]] = None, -) -> List[str]: - """Return plugin names that support `capability` AND have configuration present. - - For MULTI_INSTANCE plugins (e.g. hydrusnetwork, ftp) the plugin must have at - least one configured instance. For single-instance plugins the plugin's section - must exist under config["plugin"]. - """ - cfg = config or {} - plugin_section: Dict[str, Any] = cfg.get("plugin") or {} # type: ignore[assignment] - - result: List[str] = [] - for info in REGISTRY.iter_plugins(): - if not _info_supports_capability(info, capability): - continue - name = info.canonical_name - if info.is_multi_instance: - try: - plugin_obj = info.plugin_class(cfg) - instances = plugin_obj.configured_instances() - # Treat explicit multi-instance names as configured, but also allow - # a default/single config block for multi-instance plugins. - if instances or bool(plugin_obj.plugin_config_root()): - result.append(name) - except Exception: - pass - else: - pname = name.lower() - if isinstance(plugin_section.get(pname), dict): - result.append(name) - return sorted(result) + return isinstance(section.get(info.canonical_name.lower()), dict) def list_plugin_names_for_cmdlet( @@ -839,46 +728,24 @@ def list_plugin_names_for_cmdlet( *, configured_only: bool = False, ) -> List[str]: - """Return plugin names suitable for a cmdlet. - - Priority: - 1) Plugins that explicitly declare the cmdlet in SUPPORTED_CMDLETS. - 2) Capability fallback for legacy plugins that do not yet declare cmdlets. - """ + """Return plugin names that explicitly declare support for a cmdlet.""" cmd = str(cmdlet_name or "").strip().lower() if not cmd: return [] - supported = { - info.canonical_name for info in REGISTRY.get_plugins_for_cmdlet(cmd) - } - - fallback_capability = { - "search-file": "search", - "add-file": "upload", - "download-file": "download", - "delete-file": "delete-file", - }.get(cmd) - - if fallback_capability: - supported.update(list_plugin_names_with_capability(fallback_capability)) + supported_infos = list(REGISTRY.get_plugins_for_cmdlet(cmd)) + supported = {info.canonical_name for info in supported_infos} if not configured_only: return sorted(supported) cfg = config or {} - configured: set[str] = set() - if fallback_capability: - configured.update(list_configured_plugin_names_with_capability(fallback_capability, cfg)) - - # Keep cmdlet-declared plugins if they appear configured in the plugin section. plugin_section: Dict[str, Any] = cfg.get("plugin") or {} # type: ignore[assignment] - for name in supported: - key = str(name or "").strip().lower() - if isinstance(plugin_section.get(key), dict): - configured.add(name) - - return sorted(configured) + return sorted( + info.canonical_name + for info in supported_infos + if _info_has_configured_plugin_entry(info, cfg, plugin_section) + ) def match_plugin_name_for_url(url: str) -> Optional[str]: @@ -1051,11 +918,9 @@ __all__ = [ "register_plugin", "get_plugin", "list_plugins", - "get_plugin_with_capability", - "list_plugins_with_capability", - "list_plugin_names_with_capability", + "get_plugin_for_cmdlet", + "list_plugins_for_cmdlet", "list_plugin_names_for_cmdlet", - "list_configured_plugin_names_with_capability", "match_plugin_name_for_url", "get_plugin_for_url", "list_selection_url_prefixes", diff --git a/cmdlet/file/add.py b/cmdlet/file/add.py index 24b167e..38aea01 100644 --- a/cmdlet/file/add.py +++ b/cmdlet/file/add.py @@ -75,19 +75,19 @@ class _CommandDependencies: self._plugins[norm_name] = plugin return plugin - def get_plugin_with_capability(self, name: str, capability: str) -> Optional[Any]: - """Cached plugin lookup with capability check.""" - from PluginCore.registry import get_plugin_with_capability + def get_plugin_for_cmdlet(self, name: str, cmdlet_name: str) -> Optional[Any]: + """Cached plugin lookup with explicit cmdlet support check.""" + from PluginCore.registry import get_plugin_for_cmdlet norm_name = str(name or "").strip().lower() if not norm_name: return None - cache_key = f"{norm_name}#{capability}" + cache_key = f"{norm_name}#{cmdlet_name}" if cache_key in self._plugins: return self._plugins[cache_key] - plugin = get_plugin_with_capability(norm_name, capability, self.config) + plugin = get_plugin_for_cmdlet(norm_name, cmdlet_name, self.config) self._plugins[cache_key] = plugin return plugin @@ -1512,6 +1512,8 @@ class Add_File(Cmdlet): args: Sequence[str], config: Optional[Dict[str, Any]] = None, ) -> Optional[str]: + from PluginCore.registry import PLUGIN_REGISTRY + cfg = config if isinstance(config, dict) else {} if Add_File._uses_legacy_path_flag(args): @@ -1556,17 +1558,20 @@ class Add_File(Cmdlet): normalized_plugin_name = Add_File._normalize_provider_key(plugin_name) if normalized_plugin_name: - upload_plugin = deps.get_plugin_with_capability(normalized_plugin_name, "upload") + upload_plugin = deps.get_plugin_for_cmdlet(normalized_plugin_name, "add-file") if upload_plugin is None: - plugin_exists = deps.get_plugin(normalized_plugin_name) is not None - if plugin_exists: - if normalized_plugin_name == "loc": + plugin_info = PLUGIN_REGISTRY.get(normalized_plugin_name) + if plugin_info is not None: + canonical_plugin_name = str(plugin_info.canonical_name or normalized_plugin_name).strip().lower() + if canonical_plugin_name == "loc": return ( - "Pipeline error: plugin 'loc' does not support add-file/upload. " + "Pipeline error: plugin 'loc' does not support add-file. " "Use -plugin local -instance for local export." ) - return f"Pipeline error: plugin '{normalized_plugin_name}' does not support add-file/upload." - return f"Pipeline error: unknown upload plugin '{plugin_name}'." + if "add-file" not in plugin_info.supported_cmdlets: + return f"Pipeline error: plugin '{canonical_plugin_name}' does not support add-file." + return f"Pipeline error: plugin '{canonical_plugin_name}' is not configured or not available for add-file." + return f"Pipeline error: unknown add-file plugin '{plugin_name}'." if normalized_plugin_name == "local": requested_local = str(plugin_instance or location or "").strip() or "" @@ -1600,7 +1605,7 @@ class Add_File(Cmdlet): if deps is None: deps = _CommandDependencies(config) - file_provider = deps.get_plugin_with_capability(plugin_key, "upload") + file_provider = deps.get_plugin_for_cmdlet(plugin_key, "add-file") if file_provider is None: return None @@ -1656,7 +1661,7 @@ class Add_File(Cmdlet): if deps is None: deps = _CommandDependencies(config) - file_provider = deps.get_plugin_with_capability("local", "upload") + file_provider = deps.get_plugin_for_cmdlet("local", "add-file") if file_provider is None: return None, None @@ -2477,27 +2482,27 @@ class Add_File(Cmdlet): Any], delete_after: bool, ) -> int: - """Handle uploading via an upload plugin (e.g. 0x0).""" + """Handle uploading via an add-file plugin (e.g. 0x0).""" from PluginCore.registry import ( - get_plugin_with_capability, - list_plugin_names_with_capability, - list_plugins_with_capability, + PLUGIN_REGISTRY, + get_plugin_for_cmdlet, + list_plugins_for_cmdlet, ) try: - file_provider = get_plugin_with_capability(plugin_name, "upload", config) + file_provider = get_plugin_for_cmdlet(plugin_name, "add-file", config) if not file_provider: - available_map = list_plugins_with_capability("upload", config) - known_upload_plugins = set(list_plugin_names_with_capability("upload")) - available_uploads = [name for name, enabled in available_map.items() if enabled and name in known_upload_plugins] + available_map = list_plugins_for_cmdlet("add-file", config) + available_add_plugins = [name for name, enabled in available_map.items() if enabled] + requested_plugin_info = PLUGIN_REGISTRY.get(plugin_name) - if str(plugin_name or "").strip().lower() in known_upload_plugins: - show_plugin_config_panel([plugin_name]) + if requested_plugin_info is not None and "add-file" in requested_plugin_info.supported_cmdlets: + show_plugin_config_panel([requested_plugin_info.canonical_name]) else: - log(f"Upload plugin '{plugin_name}' is not available or does not support upload", file=sys.stderr) + log(f"Add-file plugin '{plugin_name}' is not available or does not support add-file", file=sys.stderr) - if available_uploads: - show_available_plugins_panel(sorted(available_uploads)) + if available_add_plugins: + show_available_plugins_panel(sorted(available_add_plugins)) return 1 upload_kwargs: Dict[str, Any] = { diff --git a/cmdlet/file/search.py b/cmdlet/file/search.py index 3e01983..416bd15 100644 --- a/cmdlet/file/search.py +++ b/cmdlet/file/search.py @@ -15,7 +15,7 @@ from urllib.parse import urlparse, parse_qs, unquote, urljoin from SYS.logger import log, debug, debug_panel from SYS.payload_builders import build_file_result_payload, normalize_file_extension -from PluginCore.registry import get_plugin_with_capability, list_plugins_with_capability +from PluginCore.registry import get_plugin_for_cmdlet, list_plugins_for_cmdlet from SYS.rich_display import ( show_plugin_config_panel, show_store_config_panel, @@ -1478,7 +1478,7 @@ class search_file(Cmdlet): log("Error: search-file -plugin requires both plugin and query", file=sys.stderr) log(f"Usage: {self.usage}", file=sys.stderr) - providers_map = list_plugins_with_capability("search", config) + providers_map = list_plugins_for_cmdlet("search-file", config) available = [n for n, a in providers_map.items() if a] unconfigured = [n for n, a in providers_map.items() if not a] @@ -1499,8 +1499,10 @@ class search_file(Cmdlet): if hasattr(ctx_mod, "get_pipeline_state"): progress = ctx_mod.get_pipeline_state().live_progress - provider = get_plugin_with_capability(plugin_name, "search", config) - if not provider: + providers_map = list_plugins_for_cmdlet("search-file", config) + provider = get_plugin_for_cmdlet(plugin_name, "search-file", config) + resolved_plugin_name = str(getattr(provider, "name", "") or plugin_name).strip().lower() + if not provider or not providers_map.get(resolved_plugin_name, False): if progress: try: progress.stop() @@ -1509,7 +1511,6 @@ class search_file(Cmdlet): show_plugin_config_panel([plugin_name]) - providers_map = list_plugins_with_capability("search", config) available = [n for n, a in providers_map.items() if a] if available: show_available_plugins_panel(available) diff --git a/plugins/alldebrid/__init__.py b/plugins/alldebrid/__init__.py index cc6edbe..81e9932 100644 --- a/plugins/alldebrid/__init__.py +++ b/plugins/alldebrid/__init__.py @@ -658,6 +658,7 @@ class AllDebrid(TablePluginMixin, Provider): AUTO_STAGE_USE_SELECTION_ARGS = True URL = ("magnet:", "alldebrid:magnet:", "alldebrid:", "alldebrid🧲", "alldebrid.com") URL_DOMAINS = () + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]: normalized = str(query or "").strip() diff --git a/plugins/bandcamp/__init__.py b/plugins/bandcamp/__init__.py index ed2402f..d8515a7 100644 --- a/plugins/bandcamp/__init__.py +++ b/plugins/bandcamp/__init__.py @@ -13,6 +13,7 @@ from plugins.playwright import PlaywrightTool class Bandcamp(Provider): """Search provider for Bandcamp.""" + SUPPORTED_CMDLETS = frozenset({"search-file"}) TABLE_AUTO_STAGES = { "bandcamp": ["download-file"], } diff --git a/plugins/fileio/__init__.py b/plugins/fileio/__init__.py index ef4ac2e..f08a2b1 100644 --- a/plugins/fileio/__init__.py +++ b/plugins/fileio/__init__.py @@ -51,6 +51,7 @@ def _extract_key(payload: Any) -> Optional[str]: class FileIO(Provider): """File provider for file.io.""" PLUGIN_NAME = "file.io" + SUPPORTED_CMDLETS = frozenset({"add-file"}) @classmethod def config_schema(cls) -> List[Dict[str, Any]]: diff --git a/plugins/hello/__init__.py b/plugins/hello/__init__.py index 6492f76..bbf539f 100644 --- a/plugins/hello/__init__.py +++ b/plugins/hello/__init__.py @@ -25,6 +25,7 @@ class HelloProvider(Provider): """ PLUGIN_NAME = "hello" + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) URL = ("hello:",) URL_DOMAINS = () diff --git a/plugins/hifi/__init__.py b/plugins/hifi/__init__.py index 2540c83..de15d0b 100644 --- a/plugins/hifi/__init__.py +++ b/plugins/hifi/__init__.py @@ -65,6 +65,7 @@ def _format_total_seconds(seconds: Any) -> str: class HIFI(Provider): PLUGIN_NAME = "hifi" + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) TABLE_AUTO_STAGES = { "hifi.track": ["download-file"], diff --git a/plugins/internetarchive/__init__.py b/plugins/internetarchive/__init__.py index c87a148..fd4c1f7 100644 --- a/plugins/internetarchive/__init__.py +++ b/plugins/internetarchive/__init__.py @@ -593,6 +593,7 @@ class InternetArchive(Provider): - add-file -plugin internetarchive (uploads) """ URL = ("archive.org",) + SUPPORTED_CMDLETS = frozenset({"add-file", "download-file", "search-file"}) def get_table_type(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str: return "internetarchive.folder" diff --git a/plugins/libgen/__init__.py b/plugins/libgen/__init__.py index cd633cf..3ca32df 100644 --- a/plugins/libgen/__init__.py +++ b/plugins/libgen/__init__.py @@ -656,6 +656,7 @@ def _libgen_metadata_to_tags(meta: Dict[str, Any]) -> List[str]: class Libgen(Provider): + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) TABLE_AUTO_STAGES = { "libgen": ["download-file"], } diff --git a/plugins/loc/__init__.py b/plugins/loc/__init__.py index 6ec44e7..d6b2896 100644 --- a/plugins/loc/__init__.py +++ b/plugins/loc/__init__.py @@ -14,6 +14,8 @@ class LOC(Provider): Currently implements Chronicling America collection search via the LoC JSON API. """ + SUPPORTED_CMDLETS = frozenset({"search-file"}) + @property def preserve_order(self) -> bool: return True diff --git a/plugins/matrix/__init__.py b/plugins/matrix/__init__.py index 47060ac..38cc0d7 100644 --- a/plugins/matrix/__init__.py +++ b/plugins/matrix/__init__.py @@ -302,7 +302,7 @@ class Matrix(TablePluginMixin, Provider): EXPOSE_AS_FILE_PROVIDER = False MULTI_INSTANCE = True - SUPPORTED_CMDLETS = frozenset({"add-file"}) + SUPPORTED_CMDLETS = frozenset({"add-file", "search-file"}) @classmethod def config_schema(cls) -> List[Dict[str, Any]]: diff --git a/plugins/mpv/commands.py b/plugins/mpv/commands.py index 30c12ad..c676f8d 100644 --- a/plugins/mpv/commands.py +++ b/plugins/mpv/commands.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from urllib.parse import urlparse, parse_qs from pathlib import Path from SYS.cmdlet_spec import Cmdlet, CmdletArg, parse_cmdlet_args -from PluginCore.registry import get_plugin, get_plugin_for_url, list_plugin_names_with_capability +from PluginCore.registry import get_plugin, get_plugin_for_url from SYS.logger import debug, get_thread_stream, is_debug_enabled, set_debug, set_thread_stream from SYS.result_table import Table from plugins.mpv.mpv_ipc import MPV @@ -566,34 +566,32 @@ def _iter_provider_hook_candidates( providers: List[Any] = [] seen: set[str] = set() - for target in targets or (): - try: - provider = get_plugin_for_url(str(target or ""), config or {}) - except Exception: - provider = None + def _append_provider(provider: Any) -> None: if provider is None: - continue + return name = str(getattr(provider, "name", "") or "").strip().lower() if name and name not in seen: seen.add(name) providers.append(provider) - try: - provider_names = list_plugin_names_with_capability(capability) - except Exception: - provider_names = [] + for target in targets or (): + try: + provider = get_plugin_for_url(str(target or ""), config or {}) + except Exception: + provider = None + _append_provider(provider) - for provider_name in provider_names: + fallback_provider_names = { + "pipe-item-context": ("hydrusnetwork",), + "playlist-store": ("hydrusnetwork",), + }.get(str(capability or "").strip().lower(), ()) + + for provider_name in fallback_provider_names: try: provider = get_plugin(provider_name, config or {}) except Exception: provider = None - if provider is None: - continue - name = str(getattr(provider, "name", provider_name) or provider_name).strip().lower() - if name and name not in seen: - seen.add(name) - providers.append(provider) + _append_provider(provider) return providers diff --git a/plugins/openlibrary/__init__.py b/plugins/openlibrary/__init__.py index 9f7dedb..9d354eb 100644 --- a/plugins/openlibrary/__init__.py +++ b/plugins/openlibrary/__init__.py @@ -608,6 +608,7 @@ def title_hint_from_url_slug(u: str) -> str: class OpenLibrary(Provider): + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) TABLE_AUTO_STAGES = { "openlibrary.edition": ["download-file"], } diff --git a/plugins/podcastindex/__init__.py b/plugins/podcastindex/__init__.py index d4210c0..8455e38 100644 --- a/plugins/podcastindex/__init__.py +++ b/plugins/podcastindex/__init__.py @@ -30,6 +30,7 @@ def _get_podcastindex_credentials(config: Dict[str, Any]) -> Tuple[str, str]: class PodcastIndex(Provider): """Search provider for PodcastIndex.org.""" + SUPPORTED_CMDLETS = frozenset({"search-file"}) TABLE_AUTO_STAGES = { "podcastindex": ["download-file"], "podcastindex.episodes": ["download-file"], diff --git a/plugins/scp/__init__.py b/plugins/scp/__init__.py index 66824e9..1992fd8 100644 --- a/plugins/scp/__init__.py +++ b/plugins/scp/__init__.py @@ -74,6 +74,7 @@ def _unique_path(path: Path) -> Path: class SCP(Provider): PLUGIN_NAME = "scp" URL = ("scp://", "sftp://") + SUPPORTED_CMDLETS = frozenset({"add-file", "download-file", "search-file"}) @property def label(self) -> str: diff --git a/plugins/soulseek/__init__.py b/plugins/soulseek/__init__.py index 8e899e4..83ad440 100644 --- a/plugins/soulseek/__init__.py +++ b/plugins/soulseek/__init__.py @@ -266,6 +266,7 @@ def _suppress_aioslsk_noise() -> Any: class Soulseek(Provider): + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) TABLE_AUTO_STAGES = { "soulseek": ["download-file", "-plugin", "soulseek"], } diff --git a/plugins/telegram/__init__.py b/plugins/telegram/__init__.py index 237856a..856b01e 100644 --- a/plugins/telegram/__init__.py +++ b/plugins/telegram/__init__.py @@ -151,6 +151,7 @@ class Telegram(Provider): bot_token= """ URL = ("t.me", "telegram.me") + SUPPORTED_CMDLETS = frozenset({"download-file"}) @classmethod def config_schema(cls) -> List[Dict[str, Any]]: diff --git a/plugins/tidal/__init__.py b/plugins/tidal/__init__.py index fc2e47b..34cc17e 100644 --- a/plugins/tidal/__init__.py +++ b/plugins/tidal/__init__.py @@ -65,6 +65,7 @@ def _format_total_seconds(seconds: Any) -> str: class Tidal(Provider): PLUGIN_NAME = "tidal" + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) TABLE_AUTO_STAGES = { "tidal.track": ["download-file"], diff --git a/plugins/torrent/__init__.py b/plugins/torrent/__init__.py index 4e405a5..1d014e3 100644 --- a/plugins/torrent/__init__.py +++ b/plugins/torrent/__init__.py @@ -361,6 +361,7 @@ class ApiBayScraper(Scraper): class Torrent(Provider): TABLE_AUTO_STAGES = {"torrent": ["download-file"]} + SUPPORTED_CMDLETS = frozenset({"search-file"}) @property def preserve_order(self) -> bool: diff --git a/plugins/vimm/__init__.py b/plugins/vimm/__init__.py index 00df0b5..0a956b4 100644 --- a/plugins/vimm/__init__.py +++ b/plugins/vimm/__init__.py @@ -52,6 +52,7 @@ class Vimm(TablePluginMixin, Provider): The code below implements these choices (and contains inline comments explaining specific decisions).""" + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) URL = ("https://vimm.net/vault/",) URL_DOMAINS = ("vimm.net",) diff --git a/plugins/ytdlp/__init__.py b/plugins/ytdlp/__init__.py index ef48b63..4dc777d 100644 --- a/plugins/ytdlp/__init__.py +++ b/plugins/ytdlp/__init__.py @@ -508,6 +508,7 @@ class ytdlp(TablePluginMixin, Provider): PLUGIN_NAME = "ytdlp" PLUGIN_ALIASES = ("youtube",) SEARCH_QUERY_KEYS = ("search", "q") + SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"}) @staticmethod def config_schema() -> List[Dict[str, Any]]: diff --git a/plugins/zeroxzero/__init__.py b/plugins/zeroxzero/__init__.py index 390f813..4d59175 100644 --- a/plugins/zeroxzero/__init__.py +++ b/plugins/zeroxzero/__init__.py @@ -13,6 +13,7 @@ class ZeroXZero(Provider): PLUGIN_NAME = "0x0" PLUGIN_ALIASES = ("zeroxzero",) + SUPPORTED_CMDLETS = frozenset({"add-file"}) def upload(self, file_path: str, **kwargs: Any) -> str: from API.HTTP import HTTPClient