huge refactor of plugin system
This commit is contained in:
+46
-7
@@ -30,6 +30,7 @@ class SearchResult:
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for pipeline processing."""
|
||||
full_metadata = self.full_metadata if isinstance(self.full_metadata, dict) else {}
|
||||
out = {
|
||||
"table": self.table,
|
||||
"title": self.title,
|
||||
@@ -40,15 +41,29 @@ class SearchResult:
|
||||
"size_bytes": self.size_bytes,
|
||||
"tag": list(self.tag),
|
||||
"columns": list(self.columns),
|
||||
"full_metadata": self.full_metadata,
|
||||
"full_metadata": full_metadata,
|
||||
}
|
||||
|
||||
try:
|
||||
url_value = getattr(self, "url", None)
|
||||
if url_value is not None:
|
||||
out["url"] = url_value
|
||||
except Exception:
|
||||
pass
|
||||
for key in (
|
||||
"url",
|
||||
"hash",
|
||||
"hash_hex",
|
||||
"store",
|
||||
"name",
|
||||
"mime",
|
||||
"file_id",
|
||||
"ext",
|
||||
"size",
|
||||
):
|
||||
value = None
|
||||
try:
|
||||
value = getattr(self, key, None)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is None and key in full_metadata:
|
||||
value = full_metadata.get(key)
|
||||
if value is not None:
|
||||
out[key] = value
|
||||
|
||||
try:
|
||||
selection_args = getattr(self, "selection_args", None)
|
||||
@@ -195,6 +210,30 @@ class Provider(ABC):
|
||||
"""
|
||||
return "search-file", list(args_list)
|
||||
|
||||
def resolve_pipe_item_context(
|
||||
self,
|
||||
item: Any,
|
||||
*,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
store: Optional[str] = None,
|
||||
file_hash: Optional[str] = None,
|
||||
targets: Optional[Sequence[str]] = None,
|
||||
) -> Optional[Tuple[Optional[str], Optional[str]]]:
|
||||
"""Optionally normalize store/hash context for pipe playback helpers."""
|
||||
_ = item, metadata, store, file_hash, targets
|
||||
return None
|
||||
|
||||
def infer_playlist_store(
|
||||
self,
|
||||
item: Any,
|
||||
*,
|
||||
target: str,
|
||||
file_storage: Any = None,
|
||||
) -> Optional[str]:
|
||||
"""Optionally infer a friendly store label for an MPV playlist entry."""
|
||||
_ = item, target, file_storage
|
||||
return None
|
||||
|
||||
@property
|
||||
def prefers_transfer_progress(self) -> bool:
|
||||
"""True if this plugin prefers explicit transfer progress tracking (begin/finish) during download."""
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Iterable, Sequence
|
||||
|
||||
|
||||
CmdletFn = Callable[[Any, Sequence[str], Dict[str, Any]], int]
|
||||
|
||||
|
||||
def iter_command_objects(module: Any) -> list[Any]:
|
||||
objects: list[Any] = []
|
||||
|
||||
many = getattr(module, "COMMANDS", None)
|
||||
if isinstance(many, (list, tuple)):
|
||||
for item in many:
|
||||
if item is not None:
|
||||
objects.append(item)
|
||||
|
||||
single = getattr(module, "COMMAND", None)
|
||||
if single is not None:
|
||||
objects.append(single)
|
||||
|
||||
legacy = getattr(module, "CMDLET", None)
|
||||
if legacy is not None:
|
||||
objects.append(legacy)
|
||||
|
||||
deduped: list[Any] = []
|
||||
seen: set[int] = set()
|
||||
for item in objects:
|
||||
marker = id(item)
|
||||
if marker in seen:
|
||||
continue
|
||||
seen.add(marker)
|
||||
deduped.append(item)
|
||||
return deduped
|
||||
|
||||
|
||||
def get_primary_command_object(module: Any) -> Any:
|
||||
commands = iter_command_objects(module)
|
||||
return commands[0] if commands else None
|
||||
|
||||
|
||||
def _register_command_object(cmdlet_obj: Any, registry: Dict[str, CmdletFn]) -> None:
|
||||
run_fn = getattr(cmdlet_obj, "exec", None) if hasattr(cmdlet_obj, "exec") else None
|
||||
if not callable(run_fn):
|
||||
return
|
||||
|
||||
name = getattr(cmdlet_obj, "name", None)
|
||||
if name:
|
||||
registry[str(name).replace("_", "-").lower()] = run_fn
|
||||
|
||||
aliases: list[str] = []
|
||||
if hasattr(cmdlet_obj, "alias") and getattr(cmdlet_obj, "alias"):
|
||||
aliases.extend(getattr(cmdlet_obj, "alias") or [])
|
||||
if hasattr(cmdlet_obj, "aliases") and getattr(cmdlet_obj, "aliases"):
|
||||
aliases.extend(getattr(cmdlet_obj, "aliases") or [])
|
||||
|
||||
for alias in aliases:
|
||||
text = str(alias or "").strip()
|
||||
if text:
|
||||
registry[text.replace("_", "-").lower()] = run_fn
|
||||
|
||||
|
||||
def iter_plugin_command_module_names() -> list[str]:
|
||||
try:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
plugins_dir = repo_root / "plugins"
|
||||
if not plugins_dir.is_dir():
|
||||
return []
|
||||
|
||||
module_names: list[str] = []
|
||||
for entry in sorted(plugins_dir.iterdir(), key=lambda path: path.name.lower()):
|
||||
if not entry.is_dir() or entry.name.startswith("."):
|
||||
continue
|
||||
if not (entry / "__init__.py").is_file():
|
||||
continue
|
||||
if (entry / "commands.py").is_file() or (entry / "commands" / "__init__.py").is_file():
|
||||
module_names.append(f"plugins.{entry.name}.commands")
|
||||
return module_names
|
||||
|
||||
|
||||
def register_plugin_commands(registry: Dict[str, CmdletFn]) -> None:
|
||||
for module_name in iter_plugin_command_module_names():
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
for cmdlet_obj in iter_command_objects(module):
|
||||
_register_command_object(cmdlet_obj, registry)
|
||||
except Exception as exc:
|
||||
import sys
|
||||
|
||||
print(
|
||||
f"Error importing plugin command '{module_name}': {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
@@ -505,6 +505,18 @@ 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 {"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
|
||||
|
||||
|
||||
@@ -514,6 +526,18 @@ 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 {"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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user