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:
@@ -1,5 +1,6 @@
|
||||
"""Provider core modules.
|
||||
"""Plugin core modules.
|
||||
|
||||
This package contains the provider framework (base types, registry, and shared helpers).
|
||||
Concrete provider implementations live in the `Provider/` package.
|
||||
This package contains the plugin framework (base types, registry, and shared
|
||||
helpers). Built-in plugins continue to live in the `Provider/` package for
|
||||
backward compatibility.
|
||||
"""
|
||||
|
||||
+139
-16
@@ -10,9 +10,9 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Callable
|
||||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
"""Unified search result format across all search providers."""
|
||||
"""Unified search result format across all search plugins."""
|
||||
|
||||
table: str # Provider name: "libgen", "soulseek", "bandcamp", "youtube", etc.
|
||||
table: str # Plugin name: "libgen", "soulseek", "bandcamp", "youtube", etc.
|
||||
title: str # Display title/filename
|
||||
path: str # Download target (URL, path, magnet, identifier)
|
||||
|
||||
@@ -84,7 +84,7 @@ class SearchResult:
|
||||
|
||||
|
||||
def parse_inline_query_arguments(raw_query: str) -> Tuple[str, Dict[str, str]]:
|
||||
"""Extract inline key:value arguments from a provider search query."""
|
||||
"""Extract inline key:value arguments from a plugin search query."""
|
||||
|
||||
query_text = str(raw_query or "").strip()
|
||||
if not query_text:
|
||||
@@ -112,10 +112,10 @@ def parse_inline_query_arguments(raw_query: str) -> Tuple[str, Dict[str, str]]:
|
||||
|
||||
|
||||
class Provider(ABC):
|
||||
"""Unified provider base class.
|
||||
"""Unified plugin base class.
|
||||
|
||||
This replaces the older split between "search providers" and "file providers".
|
||||
Concrete providers may implement any subset of:
|
||||
This replaces the older split between search and upload providers.
|
||||
Concrete plugins may implement any subset of:
|
||||
- search(query, ...)
|
||||
- download(result, output_dir)
|
||||
- upload(file_path, ...)
|
||||
@@ -124,7 +124,8 @@ class Provider(ABC):
|
||||
"""
|
||||
|
||||
URL: Sequence[str] = ()
|
||||
NAME: str = ""
|
||||
PLUGIN_NAME: str = ""
|
||||
PLUGIN_ALIASES: Sequence[str] = ()
|
||||
|
||||
# Optional provider-driven defaults for what to do when a user selects @N from a
|
||||
# provider table. The CLI uses this to auto-insert stages (e.g. download-file)
|
||||
@@ -141,24 +142,23 @@ class Provider(ABC):
|
||||
# Used for dynamically generating config panels (e.g., missing credentials).
|
||||
REQUIRED_CONFIG_KEYS: Sequence[str] = ()
|
||||
|
||||
# Some providers implement `upload()` but are not intended to be used as
|
||||
# generic "file host" providers via `add-file -provider ...`.
|
||||
# Some plugins implement `upload()` but are not intended to be used as
|
||||
# generic "file host" plugins via `add-file -plugin ...`.
|
||||
EXPOSE_AS_FILE_PROVIDER: bool = True
|
||||
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||
self.config = config or {}
|
||||
# Prioritize explicit NAME property for the instance name
|
||||
self.name = str(
|
||||
getattr(self, "NAME", None)
|
||||
or getattr(self, "PROVIDER_NAME", None)
|
||||
getattr(self, "PLUGIN_NAME", None)
|
||||
or self.__class__.__name__
|
||||
).lower()
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""Friendly display name for the provider."""
|
||||
if hasattr(self, "NAME") and self.NAME:
|
||||
name = str(self.NAME)
|
||||
"""Friendly display name for the plugin."""
|
||||
name = str(getattr(self, "PLUGIN_NAME", None) or self.__class__.__name__)
|
||||
|
||||
if name:
|
||||
if name.lower() == "loc":
|
||||
return "LoC"
|
||||
if name.lower() == "openlibrary":
|
||||
@@ -186,7 +186,7 @@ class Provider(ABC):
|
||||
|
||||
def get_table_metadata(self, query: str, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Return metadata for the results table."""
|
||||
return {"provider": self.name}
|
||||
return {"plugin": self.name}
|
||||
|
||||
def get_source_command(self, args_list: List[str]) -> Tuple[str, List[str]]:
|
||||
"""Return the command and arguments that produced this search result.
|
||||
@@ -308,6 +308,49 @@ class Provider(ABC):
|
||||
_ = config
|
||||
return 0
|
||||
|
||||
def resolve_pipe_result_download(
|
||||
self,
|
||||
result: Any,
|
||||
pipe_obj: Any,
|
||||
) -> Tuple[Optional[Path], Optional[str], Optional[Path]]:
|
||||
"""Materialize a piped plugin result into a local file for add-file."""
|
||||
|
||||
_ = result
|
||||
_ = pipe_obj
|
||||
return None, None, None
|
||||
|
||||
def expand_selection(
|
||||
self,
|
||||
selected_items: List[Any],
|
||||
*,
|
||||
ctx: Any,
|
||||
stage_is_last: bool = True,
|
||||
table_type: str = "",
|
||||
**_kwargs: Any,
|
||||
) -> Optional[List[Any]]:
|
||||
"""Optionally expand a selection into downstream items for non-terminal pipelines."""
|
||||
|
||||
_ = selected_items
|
||||
_ = ctx
|
||||
_ = stage_is_last
|
||||
_ = table_type
|
||||
return None
|
||||
|
||||
def status_summary(self) -> Dict[str, Any]:
|
||||
"""Return plugin-owned status details for startup/status views."""
|
||||
|
||||
enabled = False
|
||||
try:
|
||||
enabled = bool(self.validate())
|
||||
except Exception:
|
||||
enabled = False
|
||||
return {
|
||||
"status": "ENABLED" if enabled else "DISABLED",
|
||||
"name": self.label,
|
||||
"plugin": self.name,
|
||||
"detail": "Configured" if enabled else "Not configured",
|
||||
}
|
||||
|
||||
def handle_url(self, url: str, *, output_dir: Optional[Path] = None) -> Tuple[bool, Optional[Path | Dict[str, Any]]]:
|
||||
"""Optional provider override to parse and act on URLs."""
|
||||
|
||||
@@ -315,6 +358,67 @@ class Provider(ABC):
|
||||
_ = output_dir
|
||||
return False, None
|
||||
|
||||
def download_url(self, url: str, output_dir: Path, **_kwargs: Any) -> Optional[Any]:
|
||||
"""Optional direct-URL download hook used by generic cmdlets."""
|
||||
|
||||
_ = url
|
||||
_ = output_dir
|
||||
return None
|
||||
|
||||
def resolve_url(self, url: str, **_kwargs: Any) -> str:
|
||||
"""Optionally normalize or exchange a URL before downstream use."""
|
||||
|
||||
return str(url or "")
|
||||
|
||||
def resolve_playback_path(self, item: Any, **_kwargs: Any) -> Optional[str]:
|
||||
"""Optionally turn a plugin-owned item into a playable local path or URL."""
|
||||
|
||||
_ = item
|
||||
return None
|
||||
|
||||
def list_url_formats(self, url: str, **_kwargs: Any) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Optionally return picker-friendly format metadata for a URL."""
|
||||
|
||||
_ = url
|
||||
return None
|
||||
|
||||
def filter_picker_formats(
|
||||
self,
|
||||
formats: List[Dict[str, Any]],
|
||||
**_kwargs: Any,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Optionally filter or reorder raw format rows before UI display."""
|
||||
|
||||
return list(formats or [])
|
||||
|
||||
def enrich_playlist_entries(
|
||||
self,
|
||||
entries: List[Dict[str, Any]],
|
||||
**_kwargs: Any,
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Optionally expand lightweight playlist entries with richer metadata."""
|
||||
|
||||
_ = entries
|
||||
return None
|
||||
|
||||
def maybe_show_picker(
|
||||
self,
|
||||
*,
|
||||
url: str,
|
||||
item: Optional[Any] = None,
|
||||
parsed: Optional[Dict[str, Any]] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
quiet_mode: bool = False,
|
||||
) -> Optional[int]:
|
||||
"""Optional hook for plugins that want to render an interactive picker/table."""
|
||||
|
||||
_ = url
|
||||
_ = item
|
||||
_ = parsed
|
||||
_ = config
|
||||
_ = quiet_mode
|
||||
return None
|
||||
|
||||
def upload(self, file_path: str, **kwargs: Any) -> str:
|
||||
"""Upload a file and return a URL or identifier."""
|
||||
raise NotImplementedError(f"Provider '{self.name}' does not support upload")
|
||||
@@ -419,6 +523,25 @@ class Provider(ABC):
|
||||
patterns.append(candidate)
|
||||
return tuple(patterns)
|
||||
|
||||
@classmethod
|
||||
def selection_url_prefixes(cls) -> Tuple[str, ...]:
|
||||
"""Return URL-like prefixes that selection parsing should treat as URLs."""
|
||||
|
||||
prefixes: List[str] = []
|
||||
seen: set[str] = set()
|
||||
for pattern in cls.url_patterns():
|
||||
try:
|
||||
candidate = str(pattern or "").strip().lower()
|
||||
except Exception:
|
||||
continue
|
||||
if not candidate:
|
||||
continue
|
||||
if "://" in candidate or candidate.endswith(":") or "🧲" in candidate:
|
||||
if candidate not in seen:
|
||||
seen.add(candidate)
|
||||
prefixes.append(candidate)
|
||||
return tuple(prefixes)
|
||||
|
||||
|
||||
class SearchProvider(Provider):
|
||||
"""Compatibility alias for older code.
|
||||
|
||||
+225
-92
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user