huge refactor of the entire codebase, with the goal of improving maintainability, readability, and extensibility. This commit includes changes to almost every file in the project, including:

This commit is contained in:
2026-04-19 00:41:09 -07:00
parent d9e736172a
commit bafd37fdfb
50 changed files with 3258 additions and 4177 deletions
+225 -92
View File
@@ -1,17 +1,21 @@
"""Provider registry.
"""Plugin registry.
Concrete provider implementations live in the ``Provider`` package. This module
is the single source of truth for discovery, metadata, and lifecycle helpers
for those plugins.
Built-in plugin implementations live in the ``Provider`` package. External user
plugins can be dropped into a repo-local ``plugins/`` directory or discovered
via environment-configured plugin paths.
"""
from __future__ import annotations
from functools import lru_cache
import hashlib
import importlib
import importlib.util
import os
import pkgutil
import sys
from dataclasses import dataclass, field
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type
from urllib.parse import urlparse
@@ -21,21 +25,85 @@ from SYS.logger import log, debug
from ProviderCore.base import FileProvider, Provider, SearchProvider, SearchResult
def download_soulseek_file(*args: Any, **kwargs: Any) -> Any:
"""Lazy proxy for the soulseek downloader.
_EXTERNAL_PLUGIN_ENV_VARS: tuple[str, ...] = ("MM_PLUGIN_PATH", "MEDEIA_PLUGIN_PATH")
Importing the provider modules can be expensive; keeping this lazy avoids
paying that cost at registry import time.
"""
from Provider.soulseek import download_soulseek_file as _download
def _repo_root() -> Path:
try:
return Path(__file__).resolve().parents[1]
except Exception:
return Path.cwd()
return _download(*args, **kwargs)
def _iter_external_plugin_dirs() -> Tuple[Path, ...]:
seen: set[str] = set()
dirs: List[Path] = []
candidates: List[Path] = [_repo_root() / "plugins"]
try:
cwd_plugins = Path.cwd() / "plugins"
if cwd_plugins not in candidates:
candidates.append(cwd_plugins)
except Exception:
pass
for env_name in _EXTERNAL_PLUGIN_ENV_VARS:
raw_value = str(os.environ.get(env_name, "") or "").strip()
if not raw_value:
continue
for chunk in raw_value.split(os.pathsep):
text = str(chunk or "").strip().strip('"')
if not text:
continue
candidates.append(Path(text).expanduser())
for candidate in candidates:
try:
resolved = candidate.resolve()
except Exception:
resolved = candidate
key = str(resolved).lower()
if key in seen:
continue
seen.add(key)
try:
if resolved.exists() and resolved.is_dir():
dirs.append(resolved)
except Exception:
continue
return tuple(dirs)
def _iter_external_plugin_entries(plugin_dir: Path) -> Iterable[Tuple[str, Path, bool]]:
try:
children = sorted(plugin_dir.iterdir(), key=lambda entry: entry.name.lower())
except Exception:
return ()
out: List[Tuple[str, Path, bool]] = []
for child in children:
name = str(child.name or "").strip()
if not name or name.startswith("."):
continue
if child.is_file() and child.suffix.lower() == ".py" and child.stem != "__init__":
fingerprint = hashlib.sha1(str(child).encode("utf-8", errors="ignore")).hexdigest()[:10]
out.append((f"_medeia_plugin_{child.stem}_{fingerprint}", child, False))
continue
if child.is_dir():
init_py = child / "__init__.py"
if not init_py.exists() or not init_py.is_file():
continue
fingerprint = hashlib.sha1(str(child).encode("utf-8", errors="ignore")).hexdigest()[:10]
out.append((f"_medeia_plugin_pkg_{child.name}_{fingerprint}", init_py, True))
return tuple(out)
@dataclass(frozen=True)
class ProviderInfo:
"""Metadata about a single provider entry."""
"""Metadata about a single plugin entry."""
canonical_name: str
provider_class: Type[Provider]
@@ -56,14 +124,16 @@ class ProviderInfo:
class ProviderRegistry:
"""Handles discovery, registration, and lookup of provider classes."""
"""Handles discovery, registration, and lookup of built-in and external plugins."""
def __init__(self, package_name: str) -> None:
self.package_name = (package_name or "").strip()
self._infos: Dict[str, ProviderInfo] = {}
self._lookup: Dict[str, ProviderInfo] = {}
self._modules: set[str] = set()
self._external_modules: set[str] = set()
self._discovered = False
self._external_dirs_scanned = False
def _normalize(self, value: Any) -> str:
return str(value or "").strip().lower()
@@ -85,12 +155,10 @@ class ProviderRegistry:
if override_name:
_add(override_name)
else:
# Use explicit NAME or PROVIDER_NAME if available, else class name
_add(getattr(provider_class, "NAME", None))
_add(getattr(provider_class, "PROVIDER_NAME", None))
_add(getattr(provider_class, "PLUGIN_NAME", None))
_add(getattr(provider_class, "__name__", None))
for alias in getattr(provider_class, "PROVIDER_ALIASES", ()) or ():
for alias in getattr(provider_class, "PLUGIN_ALIASES", ()) or ():
_add(alias)
return names
@@ -104,14 +172,14 @@ class ProviderRegistry:
module_name: Optional[str] = None,
replace: bool = False,
) -> ProviderInfo:
"""Register a provider class with canonical and alias names."""
"""Register a plugin class with canonical and alias names."""
candidates = self._candidate_names(provider_class, override_name)
if not candidates:
raise ValueError("provider name candidates are required")
raise ValueError("plugin name candidates are required")
canonical = self._normalize(candidates[0])
if not canonical:
raise ValueError("provider name must not be empty")
raise ValueError("plugin name must not be empty")
alias_names: List[str] = []
alias_seen: set[str] = set()
@@ -165,7 +233,44 @@ class ProviderRegistry:
try:
self.register(candidate, module_name=module_name)
except Exception as exc:
log(f"[provider] Failed to register {module_name}.{candidate.__name__}: {exc}", file=sys.stderr)
log(f"[plugin] Failed to register {module_name}.{candidate.__name__}: {exc}", file=sys.stderr)
def _discover_external_plugins(self) -> None:
if self._external_dirs_scanned:
return
self._external_dirs_scanned = True
for plugin_dir in _iter_external_plugin_dirs():
try:
plugin_dir_str = str(plugin_dir)
if plugin_dir_str and plugin_dir_str not in sys.path:
sys.path.insert(0, plugin_dir_str)
except Exception:
pass
for module_name, module_path, is_package in _iter_external_plugin_entries(plugin_dir):
if module_name in self._external_modules:
continue
try:
if is_package:
spec = importlib.util.spec_from_file_location(
module_name,
str(module_path),
submodule_search_locations=[str(module_path.parent)],
)
else:
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
if spec is None or spec.loader is None:
raise ImportError("missing module spec loader")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
self._external_modules.add(module_name)
self._register_module(module)
except Exception as exc:
log(f"[plugin] Failed to load external plugin {module_path}: {exc}", file=sys.stderr)
def discover(self) -> None:
"""Import and register providers from the package."""
@@ -177,12 +282,13 @@ class ProviderRegistry:
try:
package = importlib.import_module(self.package_name)
except Exception as exc:
log(f"[provider] Failed to import package {self.package_name}: {exc}", file=sys.stderr)
log(f"[plugin] Failed to import package {self.package_name}: {exc}", file=sys.stderr)
return
self._register_module(package)
package_path = getattr(package, "__path__", None)
if not package_path:
self._discover_external_plugins()
return
for finder, module_name, _ in pkgutil.iter_modules(package_path):
@@ -194,18 +300,19 @@ class ProviderRegistry:
try:
module = importlib.import_module(module_path)
except Exception as exc:
log(f"[provider] Failed to load {module_path}: {exc}", file=sys.stderr)
log(f"[plugin] Failed to load {module_path}: {exc}", file=sys.stderr)
continue
self._register_module(module)
# Pick up any Provider subclasses loaded via other mechanisms.
self._sync_subclasses()
self._discover_external_plugins()
def _try_import_for_name(self, normalized_name: str) -> None:
"""Best-effort import for a single provider module.
"""Best-effort import for a single plugin module.
This avoids importing every provider module when the caller only needs
one provider (common for CLI usage).
one plugin (common for CLI usage).
"""
name = str(normalized_name or "").strip().lower()
if not name or not self.package_name:
@@ -249,6 +356,7 @@ class ProviderRegistry:
# module that matches the requested name.
if not self._discovered:
self._try_import_for_name(normalized)
self._discover_external_plugins()
info = self._lookup.get(normalized)
if info is not None:
return info
@@ -279,6 +387,9 @@ class ProviderRegistry:
_walk(Provider)
REGISTRY = ProviderRegistry("Provider")
PLUGIN_REGISTRY = REGISTRY
PluginInfo = ProviderInfo
PluginRegistry = ProviderRegistry
@lru_cache(maxsize=512)
@@ -289,18 +400,16 @@ def _provider_url_patterns(provider_class: Type[Provider]) -> Sequence[str]:
return []
def register_provider(
provider_class: Type[Provider],
def register_plugin(
plugin_class: Type[Provider],
*,
name: Optional[str] = None,
aliases: Optional[Sequence[str]] = None,
module_name: Optional[str] = None,
replace: bool = False,
) -> ProviderInfo:
"""Register a provider class from tests or third-party packages."""
return REGISTRY.register(
provider_class,
plugin_class,
override_name=name,
extra_aliases=aliases,
module_name=module_name,
@@ -308,7 +417,7 @@ def register_provider(
)
def get_provider_class(name: str) -> Optional[Type[Provider]]:
def get_plugin_class(name: str) -> Optional[Type[Provider]]:
info = REGISTRY.get(name)
if info is None:
return None
@@ -323,18 +432,18 @@ def selection_auto_stage_for_table(
if not t:
return None
provider_key = t.split(".", 1)[0] if "." in t else t
provider_class = get_provider_class(provider_key) or get_provider_class(t)
if provider_class is None:
plugin_key = t.split(".", 1)[0] if "." in t else t
plugin_class = get_plugin_class(plugin_key) or get_plugin_class(t)
if plugin_class is None:
return None
try:
return provider_class.selection_auto_stage(t, stage_args)
return plugin_class.selection_auto_stage(t, stage_args)
except Exception:
return None
def is_known_provider_name(name: str) -> bool:
def is_known_plugin_name(name: str) -> bool:
return REGISTRY.has_name(name)
@@ -406,83 +515,83 @@ def _collect_inline_choice_mapping(provider: Provider) -> Dict[str, List[Dict[st
return mapping
def get_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
def get_plugin(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
info = REGISTRY.get(name)
if info is None:
debug(f"[provider] Unknown provider: {name}")
debug(f"[plugin] Unknown plugin: {name}")
return None
try:
provider = info.provider_class(config)
if not provider.validate():
debug(f"[provider] Provider '{name}' is not available")
plugin = info.provider_class(config)
if not plugin.validate():
debug(f"[plugin] Plugin '{name}' is not available")
return None
return provider
return plugin
except Exception as exc:
debug(f"[provider] Error initializing '{name}': {exc}")
debug(f"[plugin] Error initializing '{name}': {exc}")
return None
def list_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
def list_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
availability: Dict[str, bool] = {}
for info in REGISTRY.iter_providers():
try:
provider = info.provider_class(config)
availability[info.canonical_name] = provider.validate()
plugin = info.provider_class(config)
availability[info.canonical_name] = plugin.validate()
except Exception:
availability[info.canonical_name] = False
return availability
def get_search_provider(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[SearchProvider]:
provider = get_provider(name, config)
if provider is None:
def get_search_plugin(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[SearchProvider]:
plugin = get_plugin(name, config)
if plugin is None:
return None
if not _supports_search(provider):
debug(f"[provider] Provider '{name}' does not support search")
if not _supports_search(plugin):
debug(f"[plugin] Plugin '{name}' does not support search")
return None
return provider # type: ignore[return-value]
return plugin # type: ignore[return-value]
def list_search_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
def list_search_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
availability: Dict[str, bool] = {}
for info in REGISTRY.iter_providers():
try:
provider = info.provider_class(config)
plugin = info.provider_class(config)
availability[info.canonical_name] = bool(
provider.validate() and info.supports_search
plugin.validate() and info.supports_search
)
except Exception:
availability[info.canonical_name] = False
return availability
def get_file_provider(name: str,
def get_upload_plugin(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[FileProvider]:
provider = get_provider(name, config)
if provider is None:
plugin = get_plugin(name, config)
if plugin is None:
return None
if not _supports_upload(provider):
debug(f"[provider] Provider '{name}' does not support upload")
if not _supports_upload(plugin):
debug(f"[plugin] Plugin '{name}' does not support upload")
return None
return provider # type: ignore[return-value]
return plugin # type: ignore[return-value]
def list_file_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
def list_upload_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
availability: Dict[str, bool] = {}
for info in REGISTRY.iter_providers():
try:
provider = info.provider_class(config)
plugin = info.provider_class(config)
availability[info.canonical_name] = bool(
provider.validate() and info.supports_upload
plugin.validate() and info.supports_upload
)
except Exception:
availability[info.canonical_name] = False
return availability
def match_provider_name_for_url(url: str) -> Optional[str]:
def match_plugin_name_for_url(url: str) -> Optional[str]:
raw_url = str(url or "").strip()
raw_url_lower = raw_url.lower()
try:
@@ -540,31 +649,31 @@ def match_provider_name_for_url(url: str) -> Optional[str]:
return None
def provider_inline_query_choices(
provider_name: str,
def plugin_inline_query_choices(
plugin_name: str,
field_name: str,
config: Optional[Dict[str, Any]] = None,
) -> List[str]:
"""Return provider-declared inline query choices for a field (e.g., system:GBA).
"""Return plugin-declared inline query choices for a field (e.g., system:GBA).
Providers can expose a mapping via ``QUERY_ARG_CHOICES`` (preferred) or
Plugins can expose a mapping via ``QUERY_ARG_CHOICES`` (preferred) or
``INLINE_QUERY_FIELD_CHOICES`` / ``inline_query_field_choices()``. The helper
keeps completion logic simple and reusable.
"""
pname = str(provider_name or "").strip().lower()
pname = str(plugin_name or "").strip().lower()
field = str(field_name or "").strip().lower()
if not pname or not field:
return []
provider = get_search_provider(pname, config)
if provider is None:
provider = get_provider(pname, config)
if provider is None:
plugin = get_search_plugin(pname, config)
if plugin is None:
plugin = get_plugin(pname, config)
if plugin is None:
return []
try:
mapping = _collect_inline_choice_mapping(provider)
mapping = _collect_inline_choice_mapping(plugin)
if not mapping:
return []
@@ -593,12 +702,32 @@ def provider_inline_query_choices(
return []
def get_provider_for_url(url: str,
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
name = match_provider_name_for_url(url)
def get_plugin_for_url(url: str,
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
name = match_plugin_name_for_url(url)
if not name:
return None
return get_provider(name, config)
return get_plugin(name, config)
def list_selection_url_prefixes() -> List[str]:
prefixes: List[str] = []
seen: set[str] = set()
for info in REGISTRY.iter_providers():
try:
values = info.provider_class.selection_url_prefixes()
except Exception:
values = ()
for value in values or ():
try:
normalized = str(value or "").strip().lower()
except Exception:
continue
if not normalized or normalized in seen:
continue
seen.add(normalized)
prefixes.append(normalized)
return prefixes
def resolve_inline_filters(
@@ -657,21 +786,25 @@ def resolve_inline_filters(
__all__ = [
"ProviderInfo",
"PluginInfo",
"Provider",
"SearchProvider",
"FileProvider",
"SearchResult",
"register_provider",
"get_provider",
"list_providers",
"get_search_provider",
"list_search_providers",
"get_file_provider",
"list_file_providers",
"match_provider_name_for_url",
"get_provider_for_url",
"get_provider_class",
"PluginRegistry",
"PLUGIN_REGISTRY",
"register_plugin",
"get_plugin",
"list_plugins",
"get_search_plugin",
"list_search_plugins",
"get_upload_plugin",
"list_upload_plugins",
"match_plugin_name_for_url",
"get_plugin_for_url",
"list_selection_url_prefixes",
"get_plugin_class",
"selection_auto_stage_for_table",
"download_soulseek_file",
"provider_inline_query_choices",
"plugin_inline_query_choices",
"is_known_plugin_name",
]