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:
+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.
|
||||
|
||||
Reference in New Issue
Block a user