This commit is contained in:
2026-05-26 19:00:04 -07:00
parent 0db899d0c3
commit cdae571385
24 changed files with 119 additions and 241 deletions
+44 -179
View File
@@ -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",