This commit is contained in:
nose
2025-12-19 02:29:42 -08:00
parent d637532237
commit 52cf3f5c9f
24 changed files with 1284 additions and 176 deletions

View File

@@ -39,14 +39,26 @@ class SearchResult:
}
class SearchProvider(ABC):
"""Base class for search providers."""
class Provider(ABC):
"""Unified provider base class.
This replaces the older split between "search providers" and "file providers".
Concrete providers may implement any subset of:
- search(query, ...)
- download(result, output_dir)
- upload(file_path, ...)
- login(...)
- validate()
"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
self.name = self.__class__.__name__.lower()
@abstractmethod
# Standard lifecycle/auth hook.
def login(self, **_kwargs: Any) -> bool:
return True
def search(
self,
query: str,
@@ -55,30 +67,46 @@ class SearchProvider(ABC):
**kwargs: Any,
) -> List[SearchResult]:
"""Search for items matching the query."""
raise NotImplementedError(f"Provider '{self.name}' does not support search")
def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
"""Download an item from a search result."""
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")
def validate(self) -> bool:
"""Check if provider is available and properly configured."""
return True
def selector(self, selected_items: List[Any], *, ctx: Any, stage_is_last: bool = True, **_kwargs: Any) -> bool:
"""Optional hook for handling `@N` selection semantics.
class FileProvider(ABC):
"""Base class for file upload providers."""
The CLI can delegate selection behavior to a provider/store instead of
applying the default selection filtering.
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
self.name = self.__class__.__name__.lower()
Return True if the selection was handled and default behavior should be skipped.
"""
@abstractmethod
def upload(self, file_path: str, **kwargs: Any) -> str:
"""Upload a file and return the URL."""
_ = selected_items
_ = ctx
_ = stage_is_last
return False
def validate(self) -> bool:
"""Check if provider is available/configured."""
return True
class SearchProvider(Provider):
"""Compatibility alias for older code.
Prefer inheriting from Provider directly.
"""
class FileProvider(Provider):
"""Compatibility alias for older code.
Prefer inheriting from Provider directly.
"""

View File

@@ -11,33 +11,47 @@ import sys
from SYS.logger import log
from ProviderCore.base import FileProvider, SearchProvider, SearchResult
from ProviderCore.base import Provider, SearchProvider, FileProvider, SearchResult
from Provider.alldebrid import AllDebrid
from Provider.bandcamp import Bandcamp
from Provider.libgen import Libgen
from Provider.matrix import Matrix
from Provider.openlibrary import OpenLibrary
from Provider.soulseek import Soulseek, download_soulseek_file
from Provider.telegram import Telegram
from Provider.youtube import YouTube
from Provider.zeroxzero import ZeroXZero
_SEARCH_PROVIDERS: Dict[str, Type[SearchProvider]] = {
_PROVIDERS: Dict[str, Type[Provider]] = {
# Search-capable providers
"alldebrid": AllDebrid,
"libgen": Libgen,
"openlibrary": OpenLibrary,
"soulseek": Soulseek,
"bandcamp": Bandcamp,
"youtube": YouTube,
"telegram": Telegram,
# Upload-capable providers
"0x0": ZeroXZero,
"matrix": Matrix,
}
def get_search_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[SearchProvider]:
"""Get a search provider by name."""
def _supports_search(provider: Provider) -> bool:
return provider.__class__.search is not Provider.search
provider_class = _SEARCH_PROVIDERS.get((name or "").lower())
def _supports_upload(provider: Provider) -> bool:
return provider.__class__.upload is not Provider.upload
def get_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
"""Get a provider by name (unified registry)."""
provider_class = _PROVIDERS.get((name or "").lower())
if provider_class is None:
log(f"[provider] Unknown search provider: {name}", file=sys.stderr)
log(f"[provider] Unknown provider: {name}", file=sys.stderr)
return None
try:
@@ -51,11 +65,11 @@ def get_search_provider(name: str, config: Optional[Dict[str, Any]] = None) -> O
return None
def list_search_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
"""List all search providers and their availability."""
def list_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
"""List all providers and their availability."""
availability: Dict[str, bool] = {}
for name, provider_class in _SEARCH_PROVIDERS.items():
for name, provider_class in _PROVIDERS.items():
try:
provider = provider_class(config)
availability[name] = provider.validate()
@@ -64,39 +78,51 @@ def list_search_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str,
return availability
_FILE_PROVIDERS: Dict[str, Type[FileProvider]] = {
"0x0": ZeroXZero,
"matrix": Matrix,
}
def get_search_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[SearchProvider]:
"""Get a search-capable provider by name (compat API)."""
provider = get_provider(name, config)
if provider is None:
return None
if not _supports_search(provider):
log(f"[provider] Provider '{name}' does not support search", file=sys.stderr)
return None
return provider # type: ignore[return-value]
def list_search_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
"""List all search providers and their availability."""
availability: Dict[str, bool] = {}
for name, provider_class in _PROVIDERS.items():
try:
provider = provider_class(config)
availability[name] = bool(provider.validate() and _supports_search(provider))
except Exception:
availability[name] = False
return availability
def get_file_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[FileProvider]:
"""Get a file provider by name."""
"""Get an upload-capable provider by name (compat API)."""
provider_class = _FILE_PROVIDERS.get((name or "").lower())
if provider_class is None:
log(f"[provider] Unknown file provider: {name}", file=sys.stderr)
provider = get_provider(name, config)
if provider is None:
return None
try:
provider = provider_class(config)
if not provider.validate():
log(f"[provider] File provider '{name}' is not available", file=sys.stderr)
return None
return provider
except Exception as exc:
log(f"[provider] Error initializing file provider '{name}': {exc}", file=sys.stderr)
if not _supports_upload(provider):
log(f"[provider] Provider '{name}' does not support upload", file=sys.stderr)
return None
return provider # type: ignore[return-value]
def list_file_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
"""List all file providers and their availability."""
availability: Dict[str, bool] = {}
for name, provider_class in _FILE_PROVIDERS.items():
for name, provider_class in _PROVIDERS.items():
try:
provider = provider_class(config)
availability[name] = provider.validate()
availability[name] = bool(provider.validate() and _supports_upload(provider))
except Exception:
availability[name] = False
return availability
@@ -104,8 +130,11 @@ def list_file_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bo
__all__ = [
"SearchResult",
"Provider",
"SearchProvider",
"FileProvider",
"get_provider",
"list_providers",
"get_search_provider",
"list_search_providers",
"get_file_provider",