update and cleanup repo

This commit is contained in:
2026-05-26 15:32:01 -07:00
parent 5041d9fbb9
commit 0db899d0c3
72 changed files with 788 additions and 1884 deletions
+33 -111
View File
@@ -23,6 +23,7 @@ from urllib.parse import urlparse
from SYS.logger import log, debug
from PluginCore.base import Provider, SearchResult
from PluginCore.inline_utils import collect_choice, resolve_filter
_EXTERNAL_PLUGIN_ENV_VARS: tuple[str, ...] = ("MM_PLUGIN_PATH", "MEDEIA_PLUGIN_PATH")
@@ -150,6 +151,14 @@ class PluginInfo:
exposed = True
return exposed and _class_supports_method(self.plugin_class, "upload", Provider.upload)
@property
def supports_download(self) -> bool:
return (
_class_supports_method(self.plugin_class, "handle_url", Provider.handle_url)
or _class_supports_method(self.plugin_class, "download_url", Provider.download_url)
or _class_supports_method(self.plugin_class, "download", Provider.download)
)
@property
def is_multi_instance(self) -> bool:
"""True if the plugin declares MULTI_INSTANCE = True."""
@@ -542,6 +551,7 @@ def get_plugin_capabilities(
"supported_cmdlets": [],
"supports_search": False,
"supports_upload": False,
"supports_download": False,
"supports_pipe_download": False,
"supports_delete_file": False,
"supports_url_association": False,
@@ -582,6 +592,7 @@ def get_plugin_capabilities(
"supported_cmdlets": supported_cmdlets,
"supports_search": bool(info.supports_search),
"supports_upload": bool(info.supports_upload),
"supports_download": bool(info.supports_download),
"supports_pipe_download": bool(supports_pipe_download),
"supports_delete_file": bool(supports_delete_file),
"supports_url_association": bool(supports_url_association),
@@ -627,6 +638,14 @@ def _supports_upload(provider: Provider) -> bool:
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__,
@@ -647,6 +666,8 @@ def _supports_capability(provider: Provider, capability: str) -> bool:
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"}:
@@ -672,6 +693,8 @@ def _info_supports_capability(info: PluginInfo, capability: str) -> bool:
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,
@@ -697,62 +720,6 @@ def _info_supports_capability(info: PluginInfo, capability: str) -> bool:
return False
def _normalize_choice_entry(entry: Any) -> Optional[Dict[str, Any]]:
if entry is None:
return None
if isinstance(entry, dict):
value = entry.get("value")
text = entry.get("text") or entry.get("label") or value
aliases = entry.get("alias") or entry.get("aliases") or []
value_str = str(value) if value is not None else (str(text) if text is not None else None)
text_str = str(text) if text is not None else value_str
if not value_str or not text_str:
return None
alias_list = [str(a) for a in aliases if a is not None]
return {"value": value_str, "text": text_str, "aliases": alias_list}
return {"value": str(entry), "text": str(entry), "aliases": []}
def _collect_inline_choice_mapping(provider: Provider) -> Dict[str, List[Dict[str, Any]]]:
mapping: Dict[str, List[Dict[str, Any]]] = {}
base = getattr(provider, "QUERY_ARG_CHOICES", None)
if not isinstance(base, dict):
base = getattr(provider, "INLINE_QUERY_FIELD_CHOICES", None)
def _merge_from(obj: Any) -> None:
if not isinstance(obj, dict):
return
for key, value in obj.items():
normalized: List[Dict[str, Any]] = []
seq = value
try:
if callable(seq):
seq = seq()
except Exception:
seq = value
if isinstance(seq, dict):
seq = seq.get("choices") or seq.get("values") or seq
if isinstance(seq, (list, tuple, set)):
for entry in seq:
n = _normalize_choice_entry(entry)
if n:
normalized.append(n)
if normalized:
mapping[str(key).strip().lower()] = normalized
_merge_from(base)
try:
fn = getattr(provider, "inline_query_field_choices", None)
if callable(fn):
_merge_from(fn())
except Exception:
pass
return mapping
def get_plugin(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
info = REGISTRY.get(name)
if info is None:
@@ -838,12 +805,11 @@ def list_configured_plugin_names_with_capability(
"""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"] or config["provider"].
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]
provider_section: Dict[str, Any] = cfg.get("provider") or {} # type: ignore[assignment]
result: List[str] = []
for info in REGISTRY.iter_plugins():
@@ -862,7 +828,7 @@ def list_configured_plugin_names_with_capability(
pass
else:
pname = name.lower()
if isinstance(plugin_section.get(pname), dict) or isinstance(provider_section.get(pname), dict):
if isinstance(plugin_section.get(pname), dict):
result.append(name)
return sorted(result)
@@ -890,7 +856,7 @@ def list_plugin_names_for_cmdlet(
fallback_capability = {
"search-file": "search",
"add-file": "upload",
"download-file": "search",
"download-file": "download",
"delete-file": "delete-file",
}.get(cmd)
@@ -905,12 +871,11 @@ def list_plugin_names_for_cmdlet(
if fallback_capability:
configured.update(list_configured_plugin_names_with_capability(fallback_capability, cfg))
# Keep cmdlet-declared plugins if they appear configured in plugin/provider sections.
# Keep cmdlet-declared plugins if they appear configured in the plugin section.
plugin_section: Dict[str, Any] = cfg.get("plugin") or {} # type: ignore[assignment]
provider_section: Dict[str, Any] = cfg.get("provider") or {} # type: ignore[assignment]
for name in supported:
key = str(name or "").strip().lower()
if isinstance(plugin_section.get(key), dict) or isinstance(provider_section.get(key), dict):
if isinstance(plugin_section.get(key), dict):
configured.add(name)
return sorted(configured)
@@ -995,13 +960,13 @@ def plugin_inline_query_choices(
mapping: Dict[str, List[Dict[str, Any]]] = {}
info = REGISTRY.get(pname)
if info is not None:
mapping = _collect_inline_choice_mapping(info.plugin_class)
mapping = collect_choice(info.plugin_class)
if not mapping:
plugin = get_plugin(pname, config)
if plugin is None:
return []
mapping = _collect_inline_choice_mapping(plugin)
mapping = collect_choice(plugin)
if not mapping:
return []
@@ -1065,52 +1030,9 @@ def resolve_inline_filters(
*,
field_transforms: Optional[Dict[str, Any]] = None,
) -> Dict[str, str]:
"""Map inline query args to provider filter values using declared choices.
"""Map inline query args to plugin filter values using the canonical helper."""
- Uses provider's inline choice mapping (value/text/aliases) to resolve user text.
- Applies optional per-field transforms (e.g., str.upper).
- Returns normalized filters suitable for provider.search.
"""
filters: Dict[str, str] = {}
if not inline_args:
return filters
mapping = _collect_inline_choice_mapping(provider)
transforms = field_transforms or {}
for raw_key, raw_val in inline_args.items():
if raw_val is None:
continue
key = str(raw_key or "").strip().lower()
val_str = str(raw_val).strip()
if not key or not val_str:
continue
entries = mapping.get(key, [])
resolved: Optional[str] = None
val_lower = val_str.lower()
for entry in entries:
text = str(entry.get("text") or "").strip()
value = str(entry.get("value") or "").strip()
aliases = [str(a).strip() for a in entry.get("aliases", []) if a is not None]
if val_lower in {text.lower(), value.lower()} or val_lower in {a.lower() for a in aliases}:
resolved = value or text or val_str
break
if resolved is None:
resolved = val_str
transform = transforms.get(key)
if callable(transform):
try:
resolved = transform(resolved)
except Exception:
pass
if resolved:
filters[key] = str(resolved)
return filters
return resolve_filter(provider, inline_args, field_transforms=field_transforms)
def clear_plugin_cache() -> None: