cleanup and rename provider to plugin
This commit is contained in:
@@ -98,7 +98,7 @@ from SYS.result_table import Table
|
|||||||
|
|
||||||
from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession
|
from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession
|
||||||
from SYS.pipeline import PipelineExecutor
|
from SYS.pipeline import PipelineExecutor
|
||||||
from ProviderCore.registry import plugin_inline_query_choices
|
from PluginCore.registry import plugin_inline_query_choices
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -505,7 +505,7 @@ class CmdletIntrospection:
|
|||||||
if normalized_arg == "plugin":
|
if normalized_arg == "plugin":
|
||||||
canonical_cmd = (cmd_name or "").replace("_", "-").lower()
|
canonical_cmd = (cmd_name or "").replace("_", "-").lower()
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import (
|
from PluginCore.registry import (
|
||||||
list_configured_plugin_names_with_capability,
|
list_configured_plugin_names_with_capability,
|
||||||
list_plugin_names_for_cmdlet,
|
list_plugin_names_for_cmdlet,
|
||||||
)
|
)
|
||||||
@@ -546,7 +546,7 @@ class CmdletIntrospection:
|
|||||||
|
|
||||||
if normalized_arg == "scrape":
|
if normalized_arg == "scrape":
|
||||||
try:
|
try:
|
||||||
from plugins.metadata_provider import list_metadata_plugins
|
from plugins.metadata_plugin import list_metadata_plugins
|
||||||
|
|
||||||
metadata_plugins = list_metadata_plugins(config) or {}
|
metadata_plugins = list_metadata_plugins(config) or {}
|
||||||
if metadata_plugins:
|
if metadata_plugins:
|
||||||
@@ -765,7 +765,7 @@ class CmdletCompleter(Completer):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin_class
|
from PluginCore.registry import get_plugin_class
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from SYS.logger import log, debug
|
from SYS.logger import log, debug
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
|
|
||||||
|
|
||||||
_EXTERNAL_PLUGIN_ENV_VARS: tuple[str, ...] = ("MM_PLUGIN_PATH", "MEDEIA_PLUGIN_PATH")
|
_EXTERNAL_PLUGIN_ENV_VARS: tuple[str, ...] = ("MM_PLUGIN_PATH", "MEDEIA_PLUGIN_PATH")
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Plugin core modules.
|
|
||||||
|
|
||||||
This package contains the plugin framework (base types, registry, and shared
|
|
||||||
helpers). Bundled plugins live in the `plugins/` package.
|
|
||||||
"""
|
|
||||||
@@ -6,8 +6,8 @@ from pathlib import Path
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import logging
|
import logging
|
||||||
from ProviderCore.commands import get_primary_command_object
|
from PluginCore.commands import get_primary_command_object
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
+1
-1
@@ -279,7 +279,7 @@ def extract_records(doc_or_html: Any, base_url: Optional[str] = None, xpaths: Op
|
|||||||
|
|
||||||
# Small convenience: convert records to SearchResult. Providers can call this or
|
# Small convenience: convert records to SearchResult. Providers can call this or
|
||||||
# use their own mapping when they need full SearchResult objects.
|
# use their own mapping when they need full SearchResult objects.
|
||||||
from ProviderCore.base import SearchResult # local import to avoid circular issues
|
from PluginCore.base import SearchResult # local import to avoid circular issues
|
||||||
|
|
||||||
|
|
||||||
def records_to_search_results(records: List[Dict[str, str]], table: str = "provider") -> List[SearchResult]:
|
def records_to_search_results(records: List[Dict[str, str]], table: str = "provider") -> List[SearchResult]:
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
||||||
|
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
from SYS.yt_metadata import extract_ytdlp_tags
|
from SYS.yt_metadata import extract_ytdlp_tags
|
||||||
|
|
||||||
try: # Optional; used when available for richer metadata fetches
|
try: # Optional; used when available for richer metadata fetches
|
||||||
|
|||||||
+4
-4
@@ -1562,7 +1562,7 @@ class PipelineExecutor:
|
|||||||
_add(getattr(item, "table", None))
|
_add(getattr(item, "table", None))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin, is_known_plugin_name
|
from PluginCore.registry import get_plugin, is_known_plugin_name
|
||||||
except Exception:
|
except Exception:
|
||||||
get_plugin = None # type: ignore
|
get_plugin = None # type: ignore
|
||||||
is_known_plugin_name = None # type: ignore
|
is_known_plugin_name = None # type: ignore
|
||||||
@@ -1679,7 +1679,7 @@ class PipelineExecutor:
|
|||||||
_add(getattr(item, "source", None))
|
_add(getattr(item, "source", None))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin, is_known_plugin_name
|
from PluginCore.registry import get_plugin, is_known_plugin_name
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -2313,7 +2313,7 @@ class PipelineExecutor:
|
|||||||
auto_stage = None
|
auto_stage = None
|
||||||
if isinstance(table_type, str) and table_type:
|
if isinstance(table_type, str) and table_type:
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import selection_auto_stage_for_table
|
from PluginCore.registry import selection_auto_stage_for_table
|
||||||
|
|
||||||
auto_stage = selection_auto_stage_for_table(table_type)
|
auto_stage = selection_auto_stage_for_table(table_type)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -3098,7 +3098,7 @@ class PipelineExecutor:
|
|||||||
auto_stage = None
|
auto_stage = None
|
||||||
if isinstance(table_type, str) and table_type:
|
if isinstance(table_type, str) and table_type:
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import selection_auto_stage_for_table
|
from PluginCore.registry import selection_auto_stage_for_table
|
||||||
|
|
||||||
# Preserve historical behavior: only forward selection-stage args
|
# Preserve historical behavior: only forward selection-stage args
|
||||||
# to the auto stage when we are appending a new last stage.
|
# to the auto stage when we are appending a new last stage.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pkgutil
|
|||||||
from typing import Any, Dict, Iterable, List, Optional
|
from typing import Any, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
from SYS.config import global_config
|
from SYS.config import global_config
|
||||||
from ProviderCore.registry import get_plugin_class, list_plugins
|
from PluginCore.registry import get_plugin_class, list_plugins
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ def get_required_config_keys(item_type: str, item_name: str) -> List[str]:
|
|||||||
|
|
||||||
def get_configurable_store_types() -> List[str]:
|
def get_configurable_store_types() -> List[str]:
|
||||||
"""Return configurable multi-instance plugin types (formerly 'store types')."""
|
"""Return configurable multi-instance plugin types (formerly 'store types')."""
|
||||||
from ProviderCore.registry import REGISTRY
|
from PluginCore.registry import REGISTRY
|
||||||
options: List[str] = []
|
options: List[str] = []
|
||||||
for info in REGISTRY.iter_plugins():
|
for info in REGISTRY.iter_plugins():
|
||||||
plugin_cls = info.plugin_class
|
plugin_cls = info.plugin_class
|
||||||
@@ -205,7 +205,7 @@ def get_configurable_store_types() -> List[str]:
|
|||||||
|
|
||||||
def get_configurable_plugin_types() -> List[str]:
|
def get_configurable_plugin_types() -> List[str]:
|
||||||
"""Return all plugin types that can be configured: those with a schema or MULTI_INSTANCE flag."""
|
"""Return all plugin types that can be configured: those with a schema or MULTI_INSTANCE flag."""
|
||||||
from ProviderCore.registry import REGISTRY
|
from PluginCore.registry import REGISTRY
|
||||||
options: List[str] = []
|
options: List[str] = []
|
||||||
for info in REGISTRY.iter_plugins():
|
for info in REGISTRY.iter_plugins():
|
||||||
plugin_cls = info.plugin_class
|
plugin_cls = info.plugin_class
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""Convenience mixins and helpers for table-based providers.
|
"""Convenience mixins and helpers for table-based plugins.
|
||||||
|
|
||||||
Provides a small `TableProviderMixin` that handles HTTP fetch + table extraction
|
Provides a small `TablePluginMixin` that handles HTTP fetch + table extraction
|
||||||
(using `SYS.html_table.extract_records`) and converts records into
|
(using `SYS.html_table.extract_records`) and converts records into
|
||||||
`ProviderCore.base.SearchResult` rows with sane default column ordering.
|
`PluginCore.base.SearchResult` rows with sane default column ordering.
|
||||||
|
|
||||||
Providers can subclass this mixin to implement search quickly:
|
Plugins can subclass this mixin to implement search quickly:
|
||||||
|
|
||||||
class MyProvider(TableProviderMixin, Provider):
|
class MyPlugin(TablePluginMixin, Provider):
|
||||||
URL = ("https://example.org/search",)
|
URL = ("https://example.org/search",)
|
||||||
|
|
||||||
def search(self, query, limit=50, **kwargs):
|
def search(self, query, limit=50, **kwargs):
|
||||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from API.HTTP import HTTPClient
|
from API.HTTP import HTTPClient
|
||||||
from ProviderCore.base import SearchResult
|
from PluginCore.base import SearchResult
|
||||||
from SYS.html_table import extract_records
|
from SYS.html_table import extract_records
|
||||||
import lxml.html as lxml_html
|
import lxml.html as lxml_html
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TableProviderMixin:
|
class TablePluginMixin:
|
||||||
"""Mixin to simplify providers that scrape table/list results from HTML.
|
"""Mixin to simplify plugins that scrape table/list results from HTML.
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
- search_table_from_url(url, limit, xpaths): fetches HTML, extracts records, returns SearchResults
|
- search_table_from_url(url, limit, xpaths): fetches HTML, extracts records, returns SearchResults
|
||||||
@@ -59,7 +59,7 @@ class TableProviderMixin:
|
|||||||
resp = client.get(url)
|
resp = client.get(url)
|
||||||
content = resp.content
|
content = resp.content
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to fetch URL %s for provider %s", url, getattr(self, 'name', '<provider>'))
|
logger.exception("Failed to fetch URL %s for plugin %s", url, getattr(self, 'name', '<plugin>'))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Ensure we pass an lxml document or string (httpx returns bytes)
|
# Ensure we pass an lxml document or string (httpx returns bytes)
|
||||||
@@ -99,14 +99,14 @@ class TableProviderMixin:
|
|||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
SearchResult(
|
SearchResult(
|
||||||
table=(getattr(self, "name", "provider") or "provider"),
|
table=(getattr(self, "name", "plugin") or "plugin"),
|
||||||
title=title,
|
title=title,
|
||||||
path=path,
|
path=path,
|
||||||
detail="",
|
detail="",
|
||||||
annotations=[],
|
annotations=[],
|
||||||
media_kind="file",
|
media_kind="file",
|
||||||
size_bytes=None,
|
size_bytes=None,
|
||||||
tag={getattr(self, "name", "provider")},
|
tag={getattr(self, "name", "plugin") or "plugin"},
|
||||||
columns=cols,
|
columns=cols,
|
||||||
full_metadata={"raw_record": rec},
|
full_metadata={"raw_record": rec},
|
||||||
)
|
)
|
||||||
+2
-2
@@ -88,7 +88,7 @@ def _load_root_modules() -> None:
|
|||||||
|
|
||||||
def _load_helper_modules() -> None:
|
def _load_helper_modules() -> None:
|
||||||
# Provider-specific module pre-loading removed; providers are loaded lazily
|
# Provider-specific module pre-loading removed; providers are loaded lazily
|
||||||
# through ProviderCore.registry when first referenced.
|
# through PluginCore.registry when first referenced.
|
||||||
#
|
#
|
||||||
# Keep explicit imports for cmdlets moved into subpackages so they remain
|
# Keep explicit imports for cmdlets moved into subpackages so they remain
|
||||||
# registered under their legacy command names.
|
# registered under their legacy command names.
|
||||||
@@ -118,7 +118,7 @@ def _register_native_commands() -> None:
|
|||||||
|
|
||||||
def _register_plugin_commands() -> None:
|
def _register_plugin_commands() -> None:
|
||||||
try:
|
try:
|
||||||
from ProviderCore.commands import register_plugin_commands
|
from PluginCore.commands import register_plugin_commands
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|||||||
+3
-3
@@ -276,7 +276,7 @@ class SharedArgs:
|
|||||||
|
|
||||||
# Plugin-based multi-instance backends (config["plugin"] / config["provider"] sections)
|
# Plugin-based multi-instance backends (config["plugin"] / config["provider"] sections)
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import REGISTRY
|
from PluginCore.registry import REGISTRY
|
||||||
plugin_instances = REGISTRY.list_storage_plugin_instances(config)
|
plugin_instances = REGISTRY.list_storage_plugin_instances(config)
|
||||||
for _plugin_name, instance_names in plugin_instances.items():
|
for _plugin_name, instance_names in plugin_instances.items():
|
||||||
names.update(instance_names)
|
names.update(instance_names)
|
||||||
@@ -1448,7 +1448,7 @@ def fetch_hydrus_metadata(
|
|||||||
client = hydrus_client
|
client = hydrus_client
|
||||||
hydrus_provider = None
|
hydrus_provider = None
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
|
|
||||||
hydrus_provider = get_plugin("hydrusnetwork", config)
|
hydrus_provider = get_plugin("hydrusnetwork", config)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -4177,7 +4177,7 @@ def check_url_exists_in_storage(
|
|||||||
|
|
||||||
hydrus_provider = None
|
hydrus_provider = None
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
|
|
||||||
hydrus_provider = get_plugin("hydrusnetwork", config)
|
hydrus_provider = get_plugin("hydrusnetwork", config)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+3
-3
@@ -63,7 +63,7 @@ class _CommandDependencies:
|
|||||||
|
|
||||||
def get_plugin(self, name: str) -> Optional[Any]:
|
def get_plugin(self, name: str) -> Optional[Any]:
|
||||||
"""Cached plugin lookup by name."""
|
"""Cached plugin lookup by name."""
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
|
|
||||||
norm_name = str(name or "").strip().lower()
|
norm_name = str(name or "").strip().lower()
|
||||||
if not norm_name:
|
if not norm_name:
|
||||||
@@ -77,7 +77,7 @@ class _CommandDependencies:
|
|||||||
|
|
||||||
def get_plugin_with_capability(self, name: str, capability: str) -> Optional[Any]:
|
def get_plugin_with_capability(self, name: str, capability: str) -> Optional[Any]:
|
||||||
"""Cached plugin lookup with capability check."""
|
"""Cached plugin lookup with capability check."""
|
||||||
from ProviderCore.registry import get_plugin_with_capability
|
from PluginCore.registry import get_plugin_with_capability
|
||||||
|
|
||||||
norm_name = str(name or "").strip().lower()
|
norm_name = str(name or "").strip().lower()
|
||||||
if not norm_name:
|
if not norm_name:
|
||||||
@@ -2336,7 +2336,7 @@ class Add_File(Cmdlet):
|
|||||||
delete_after: bool,
|
delete_after: bool,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Handle uploading via an upload plugin (e.g. 0x0)."""
|
"""Handle uploading via an upload plugin (e.g. 0x0)."""
|
||||||
from ProviderCore.registry import (
|
from PluginCore.registry import (
|
||||||
get_plugin_with_capability,
|
get_plugin_with_capability,
|
||||||
list_plugin_names_with_capability,
|
list_plugin_names_with_capability,
|
||||||
list_plugins_with_capability,
|
list_plugins_with_capability,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Sequence, Set
|
|||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
from SYS.item_accessors import get_http_url, get_sha256_hex, get_store_name
|
from SYS.item_accessors import get_http_url, get_sha256_hex, get_store_name
|
||||||
from SYS.utils import extract_hydrus_hash_from_url
|
from SYS.utils import extract_hydrus_hash_from_url
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from SYS.logger import debug, log
|
from SYS.logger import debug, log
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
from Store import Store
|
from Store import Store
|
||||||
from .. import _shared as sh
|
from .. import _shared as sh
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
|
|||||||
@@ -992,8 +992,8 @@ class Download_File(Cmdlet):
|
|||||||
def _load_provider_registry() -> Dict[str, Any]:
|
def _load_provider_registry() -> Dict[str, Any]:
|
||||||
"""Lightweight accessor for plugin helpers without hard dependencies."""
|
"""Lightweight accessor for plugin helpers without hard dependencies."""
|
||||||
try:
|
try:
|
||||||
from ProviderCore import registry as provider_registry # type: ignore
|
from PluginCore import registry as provider_registry # type: ignore
|
||||||
from ProviderCore.base import SearchResult # type: ignore
|
from PluginCore.base import SearchResult # type: ignore
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"get_plugin": getattr(provider_registry, "get_plugin", None),
|
"get_plugin": getattr(provider_registry, "get_plugin", None),
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
|
|
||||||
if urls_to_download and len(urls_to_download) >= 2:
|
if urls_to_download and len(urls_to_download) >= 2:
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin_for_url
|
from PluginCore.registry import get_plugin_for_url
|
||||||
|
|
||||||
expanded: List[Dict[str, Any]] = []
|
expanded: List[Dict[str, Any]] = []
|
||||||
downloaded_any = False
|
downloaded_any = False
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from urllib.parse import urlparse, parse_qs, unquote, urljoin
|
|||||||
|
|
||||||
from SYS.logger import log, debug, debug_panel
|
from SYS.logger import log, debug, debug_panel
|
||||||
from SYS.payload_builders import build_file_result_payload, normalize_file_extension
|
from SYS.payload_builders import build_file_result_payload, normalize_file_extension
|
||||||
from ProviderCore.registry import get_plugin_with_capability, list_plugins_with_capability
|
from PluginCore.registry import get_plugin_with_capability, list_plugins_with_capability
|
||||||
from SYS.rich_display import (
|
from SYS.rich_display import (
|
||||||
show_plugin_config_panel,
|
show_plugin_config_panel,
|
||||||
show_store_config_panel,
|
show_store_config_panel,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import sys
|
|||||||
|
|
||||||
from SYS.detail_view_helpers import create_detail_view, prepare_detail_metadata
|
from SYS.detail_view_helpers import create_detail_view, prepare_detail_metadata
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
from SYS.result_table_helpers import add_row_columns
|
from SYS.result_table_helpers import add_row_columns
|
||||||
from SYS.selection_builder import build_hash_store_selection
|
from SYS.selection_builder import build_hash_store_selection
|
||||||
from SYS.result_publication import publish_result_table
|
from SYS.result_publication import publish_result_table
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import sys
|
|||||||
|
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from SYS.item_accessors import get_sha256_hex, get_store_name
|
from SYS.item_accessors import get_sha256_hex, get_store_name
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
|
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from .. import _shared as sh
|
from .. import _shared as sh
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ import sys
|
|||||||
|
|
||||||
from SYS.logger import log, debug
|
from SYS.logger import log, debug
|
||||||
|
|
||||||
# plugins.metadata_provider is deferred: it transitively loads yt_dlp, Cryptodome,
|
# plugins.metadata_plugin is deferred: it transitively loads yt_dlp, Cryptodome,
|
||||||
# imdbinfo, musicbrainzngs and ~1400 modules (~1.5s). Import lazily on first use.
|
# imdbinfo, musicbrainzngs and ~1400 modules (~1.5s). Import lazily on first use.
|
||||||
_METADATA_PROVIDER_MOD: Optional[Any] = None
|
_METADATA_PLUGIN_MOD: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
def _mp() -> Any:
|
def _mp() -> Any:
|
||||||
"""Return the (lazily imported) plugins.metadata_provider module."""
|
"""Return the (lazily imported) plugins.metadata_plugin module."""
|
||||||
global _METADATA_PROVIDER_MOD
|
global _METADATA_PLUGIN_MOD
|
||||||
if _METADATA_PROVIDER_MOD is None:
|
if _METADATA_PLUGIN_MOD is None:
|
||||||
import plugins.metadata_provider as _m
|
import plugins.metadata_plugin as _m
|
||||||
_METADATA_PROVIDER_MOD = _m
|
_METADATA_PLUGIN_MOD = _m
|
||||||
return _METADATA_PROVIDER_MOD
|
return _METADATA_PLUGIN_MOD
|
||||||
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
select_raw = parsed.get("select")
|
select_raw = parsed.get("select")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider = get_plugin(plugin_name)
|
plugin = get_plugin(plugin_name)
|
||||||
except Exception:
|
except Exception:
|
||||||
log(f"Unknown plugin: {plugin_name}", file=sys.stderr)
|
log(f"Unknown plugin: {plugin_name}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -52,7 +52,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
if use_sample:
|
if use_sample:
|
||||||
# Try to locate SAMPLE_ITEMS in the adapter's module (convention only)
|
# Try to locate SAMPLE_ITEMS in the adapter's module (convention only)
|
||||||
try:
|
try:
|
||||||
mod = __import__(provider.adapter.__module__, fromlist=["*"])
|
mod = __import__(plugin.adapter.__module__, fromlist=["*"])
|
||||||
items = getattr(mod, "SAMPLE_ITEMS", None)
|
items = getattr(mod, "SAMPLE_ITEMS", None)
|
||||||
if items is None:
|
if items is None:
|
||||||
log("Plugin does not expose SAMPLE_ITEMS; no sample available", file=sys.stderr)
|
log("Plugin does not expose SAMPLE_ITEMS; no sample available", file=sys.stderr)
|
||||||
@@ -69,14 +69,14 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
items = inputs
|
items = inputs
|
||||||
|
|
||||||
try:
|
try:
|
||||||
table = provider.build_table(items)
|
table = plugin.build_table(items)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Plugin '{provider.name}' failed: {exc}", file=sys.stderr)
|
log(f"Plugin '{plugin.name}' failed: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Emit rows for downstream pipeline consumption (pipable behavior).
|
# Emit rows for downstream pipeline consumption (pipable behavior).
|
||||||
try:
|
try:
|
||||||
for item in provider.serialize_rows(table.rows):
|
for item in plugin.serialize_rows(table.rows):
|
||||||
try:
|
try:
|
||||||
ctx.emit(item)
|
ctx.emit(item)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -115,7 +115,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
selected = table.rows[select_idx]
|
selected = table.rows[select_idx]
|
||||||
sel_args = provider.selection_args(selected)
|
sel_args = plugin.selection_args(selected)
|
||||||
|
|
||||||
if not run_cmd:
|
if not run_cmd:
|
||||||
# Print selection args for caller
|
# Print selection args for caller
|
||||||
@@ -41,7 +41,7 @@ def _provider_config_map(config: dict) -> dict[str, Any]:
|
|||||||
|
|
||||||
def _iter_registered_plugin_infos() -> tuple[Any, ...]:
|
def _iter_registered_plugin_infos() -> tuple[Any, ...]:
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import REGISTRY
|
from PluginCore.registry import REGISTRY
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
sorted(
|
sorted(
|
||||||
|
|||||||
+412
-16
@@ -12,6 +12,13 @@ from SYS.config import (
|
|||||||
)
|
)
|
||||||
from SYS.database import LOG_DB_PATH, db
|
from SYS.database import LOG_DB_PATH, db
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
|
from SYS.plugin_config import (
|
||||||
|
build_default_plugin_config,
|
||||||
|
build_default_tool_config,
|
||||||
|
get_configurable_plugin_types,
|
||||||
|
get_configurable_store_types,
|
||||||
|
get_configurable_tool_types,
|
||||||
|
)
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from SYS.result_table import Table
|
from SYS.result_table import Table
|
||||||
from cmdnat._parsing import (
|
from cmdnat._parsing import (
|
||||||
@@ -26,6 +33,7 @@ from cmdnat._parsing import (
|
|||||||
_PREFERENCES_BROWSE_PATH = "__preferences__"
|
_PREFERENCES_BROWSE_PATH = "__preferences__"
|
||||||
_PLUGINS_BROWSE_PATH = "__plugins__"
|
_PLUGINS_BROWSE_PATH = "__plugins__"
|
||||||
_PLUGIN_CATEGORY_KEYS = ("plugin", "provider", "tool")
|
_PLUGIN_CATEGORY_KEYS = ("plugin", "provider", "tool")
|
||||||
|
_CREATE_INSTANCE_FLAG = "-create-instance"
|
||||||
_KNOWN_SECTION_LABELS = {
|
_KNOWN_SECTION_LABELS = {
|
||||||
"plugin": "Plugins",
|
"plugin": "Plugins",
|
||||||
"provider": "Plugins",
|
"provider": "Plugins",
|
||||||
@@ -52,6 +60,18 @@ _SENSITIVE_CONFIG_KEYS = {
|
|||||||
"secret",
|
"secret",
|
||||||
"token",
|
"token",
|
||||||
}
|
}
|
||||||
|
_CONFIG_ITEM_FIELDS = (
|
||||||
|
"kind",
|
||||||
|
"key",
|
||||||
|
"title",
|
||||||
|
"browse_path",
|
||||||
|
"name",
|
||||||
|
"value",
|
||||||
|
"value_display",
|
||||||
|
"type",
|
||||||
|
"display_path",
|
||||||
|
"instance_target",
|
||||||
|
)
|
||||||
|
|
||||||
CMDLET = Cmdlet(
|
CMDLET = Cmdlet(
|
||||||
name=".config",
|
name=".config",
|
||||||
@@ -271,6 +291,168 @@ def _format_config_entry_count(value: Any) -> str:
|
|||||||
return f"{count} entries"
|
return f"{count} entries"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_configurable_plugin_names() -> List[str]:
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
str(name).strip().lower()
|
||||||
|
for name in (get_configurable_plugin_types() or [])
|
||||||
|
if str(name).strip()
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_configurable_tool_names() -> List[str]:
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
str(name).strip().lower()
|
||||||
|
for name in (get_configurable_tool_types() or [])
|
||||||
|
if str(name).strip()
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_multi_instance_plugin_names() -> set[str]:
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
str(name).strip().lower()
|
||||||
|
for name in (get_configurable_store_types() or [])
|
||||||
|
if str(name).strip()
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def _split_config_path(value: Optional[str]) -> List[str]:
|
||||||
|
return [part for part in str(value or "").split(".") if part]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_multi_instance_plugin_name(name: str) -> bool:
|
||||||
|
return str(name or "").strip().lower() in _get_multi_instance_plugin_names()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_multi_instance_plugin_root_path(browse_path: Optional[str]) -> bool:
|
||||||
|
parts = _split_config_path(browse_path)
|
||||||
|
return (
|
||||||
|
len(parts) == 2
|
||||||
|
and parts[0] in {"plugin", "provider"}
|
||||||
|
and _is_multi_instance_plugin_name(parts[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin_schema_field_keys(plugin_name: str) -> set[str]:
|
||||||
|
defaults = build_default_plugin_config(plugin_name)
|
||||||
|
if not isinstance(defaults, dict):
|
||||||
|
return set()
|
||||||
|
return {
|
||||||
|
str(key or "").strip().lower()
|
||||||
|
for key in defaults.keys()
|
||||||
|
if str(key or "").strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_single_instance_branch(plugin_name: str, branch: Any) -> bool:
|
||||||
|
if not isinstance(branch, dict) or not branch:
|
||||||
|
return False
|
||||||
|
|
||||||
|
schema_keys = _plugin_schema_field_keys(plugin_name)
|
||||||
|
entry_keys = {str(key or "").strip().lower() for key in branch.keys()}
|
||||||
|
looks_like_single = bool(schema_keys and entry_keys.intersection(schema_keys))
|
||||||
|
if not looks_like_single:
|
||||||
|
looks_like_single = not all(isinstance(value, dict) for value in branch.values())
|
||||||
|
return looks_like_single
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_multi_instance_branch(plugin_name: str, branch: Any) -> Dict[str, Any]:
|
||||||
|
if not isinstance(branch, dict):
|
||||||
|
return {}
|
||||||
|
if _looks_like_single_instance_branch(plugin_name, branch):
|
||||||
|
return {"default": dict(branch)}
|
||||||
|
return {
|
||||||
|
str(key): value
|
||||||
|
for key, value in _visible_config_entries(branch)
|
||||||
|
if isinstance(value, dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_create_instance_item(category: str, plugin_name: str) -> Dict[str, Any]:
|
||||||
|
target = f"{category}.{plugin_name}"
|
||||||
|
return {
|
||||||
|
"kind": "create_instance",
|
||||||
|
"key": f"{target}.__new_instance__",
|
||||||
|
"title": "Add Instance",
|
||||||
|
"name": "add_instance",
|
||||||
|
"value": None,
|
||||||
|
"value_display": "Create with @N | .config <name>",
|
||||||
|
"display_path": f"{_format_config_path_label(target)} / Add Instance",
|
||||||
|
"type": "action",
|
||||||
|
"instance_target": target,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_synthetic_plugin_branch(category: str, name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
normalized_category = str(category or "").strip().lower()
|
||||||
|
normalized_name = str(name or "").strip().lower()
|
||||||
|
if not normalized_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if normalized_category == "tool":
|
||||||
|
branch = build_default_tool_config(normalized_name)
|
||||||
|
return dict(branch) if isinstance(branch, dict) else None
|
||||||
|
|
||||||
|
branch = build_default_plugin_config(normalized_name)
|
||||||
|
if not isinstance(branch, dict):
|
||||||
|
return None
|
||||||
|
if normalized_name in _get_multi_instance_plugin_names():
|
||||||
|
return {"default": dict(branch)}
|
||||||
|
return dict(branch)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_configured_plugin_branch(
|
||||||
|
config_data: Dict[str, Any],
|
||||||
|
category: str,
|
||||||
|
name: str,
|
||||||
|
) -> Optional[tuple[str, Dict[str, Any]]]:
|
||||||
|
category_block = config_data.get(category)
|
||||||
|
if not isinstance(category_block, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
target = str(name or "").strip().lower()
|
||||||
|
for raw_name, raw_value in _visible_config_entries(category_block):
|
||||||
|
if str(raw_name or "").strip().lower() != target or not isinstance(raw_value, dict):
|
||||||
|
continue
|
||||||
|
return raw_name, raw_value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_plugin_branch(
|
||||||
|
config_data: Dict[str, Any],
|
||||||
|
category: str,
|
||||||
|
name: str,
|
||||||
|
) -> Optional[tuple[str, Dict[str, Any], bool]]:
|
||||||
|
found = _find_configured_plugin_branch(config_data, category, name)
|
||||||
|
if found is not None:
|
||||||
|
resolved_name, resolved_value = found
|
||||||
|
return resolved_name, resolved_value, True
|
||||||
|
|
||||||
|
normalized_category = str(category or "").strip().lower()
|
||||||
|
normalized_name = str(name or "").strip().lower()
|
||||||
|
if not normalized_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if normalized_category == "tool":
|
||||||
|
if normalized_name not in _get_configurable_tool_names():
|
||||||
|
return None
|
||||||
|
elif normalized_name not in _get_configurable_plugin_names():
|
||||||
|
return None
|
||||||
|
|
||||||
|
synthetic = _build_synthetic_plugin_branch(normalized_category, normalized_name)
|
||||||
|
if synthetic is None:
|
||||||
|
return None
|
||||||
|
return normalized_name, synthetic, False
|
||||||
|
|
||||||
|
|
||||||
def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any]]:
|
def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any]]:
|
||||||
branches: List[tuple[str, str, Any]] = []
|
branches: List[tuple[str, str, Any]] = []
|
||||||
if not isinstance(config_data, dict):
|
if not isinstance(config_data, dict):
|
||||||
@@ -285,9 +467,41 @@ def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, A
|
|||||||
return branches
|
return branches
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_available_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any, bool]]:
|
||||||
|
branches: List[tuple[str, str, Any, bool]] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
|
||||||
|
for category, name, value in _iter_plugin_branches(config_data):
|
||||||
|
normalized_name = str(name or "").strip().lower()
|
||||||
|
if not normalized_name:
|
||||||
|
continue
|
||||||
|
branches.append((category, name, value, True))
|
||||||
|
seen.add(normalized_name)
|
||||||
|
|
||||||
|
for name in _get_configurable_plugin_names():
|
||||||
|
if name in seen:
|
||||||
|
continue
|
||||||
|
synthetic = _build_synthetic_plugin_branch("plugin", name)
|
||||||
|
if synthetic is None:
|
||||||
|
continue
|
||||||
|
branches.append(("plugin", name, synthetic, False))
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
for name in _get_configurable_tool_names():
|
||||||
|
if name in seen:
|
||||||
|
continue
|
||||||
|
synthetic = _build_synthetic_plugin_branch("tool", name)
|
||||||
|
if synthetic is None:
|
||||||
|
continue
|
||||||
|
branches.append(("tool", name, synthetic, False))
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
return branches
|
||||||
|
|
||||||
|
|
||||||
def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
plugin_items: Dict[str, Dict[str, Any]] = {}
|
plugin_items: Dict[str, Dict[str, Any]] = {}
|
||||||
for category, name, value in _iter_plugin_branches(config_data):
|
for category, name, value, is_configured in _iter_available_plugin_branches(config_data):
|
||||||
key = str(name or "").strip().lower()
|
key = str(name or "").strip().lower()
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
@@ -299,7 +513,7 @@ def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, An
|
|||||||
"browse_path": f"{category}.{name}",
|
"browse_path": f"{category}.{name}",
|
||||||
"summary": _format_config_entry_count(value),
|
"summary": _format_config_entry_count(value),
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"description": "Plugin configuration",
|
"description": "Plugin configuration" if is_configured else "Plugin configuration (available to configure)",
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -337,8 +551,22 @@ def _resolve_config_branch(
|
|||||||
for item in _collect_plugin_root_items(config_data)
|
for item in _collect_plugin_root_items(config_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parts = [part for part in text.split(".") if part]
|
||||||
|
if len(parts) >= 2 and parts[0] in _PLUGIN_CATEGORY_KEYS:
|
||||||
|
resolved = _resolve_plugin_branch(config_data, parts[0], parts[1])
|
||||||
|
if resolved is None:
|
||||||
|
return None
|
||||||
|
_, current, _ = resolved
|
||||||
|
if parts[0] in {"plugin", "provider"} and _is_multi_instance_plugin_name(parts[1]):
|
||||||
|
current = _normalize_multi_instance_branch(parts[1], current)
|
||||||
|
for part in parts[2:]:
|
||||||
|
if not isinstance(current, dict):
|
||||||
|
return None
|
||||||
|
current = current.get(part)
|
||||||
|
return current if isinstance(current, dict) else None
|
||||||
|
|
||||||
current: Any = config_data
|
current: Any = config_data
|
||||||
for part in text.split("."):
|
for part in parts:
|
||||||
if not isinstance(current, dict):
|
if not isinstance(current, dict):
|
||||||
return None
|
return None
|
||||||
current = current.get(part)
|
current = current.get(part)
|
||||||
@@ -386,6 +614,66 @@ def _build_value_item(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_or_get_plugin_instance(
|
||||||
|
config_data: Dict[str, Any],
|
||||||
|
instance_target: str,
|
||||||
|
instance_name: str,
|
||||||
|
) -> tuple[str, bool]:
|
||||||
|
parts = _split_config_path(instance_target)
|
||||||
|
if len(parts) != 2 or parts[0] not in {"plugin", "provider"}:
|
||||||
|
raise ValueError(f"Unsupported instance target '{instance_target}'")
|
||||||
|
|
||||||
|
category, plugin_name = parts
|
||||||
|
raw_instance_name = str(instance_name or "").strip()
|
||||||
|
if not raw_instance_name:
|
||||||
|
raise ValueError("Instance name is required")
|
||||||
|
if raw_instance_name.startswith("_"):
|
||||||
|
raise ValueError("Instance names cannot start with '_' characters")
|
||||||
|
|
||||||
|
category_block = config_data.get(category)
|
||||||
|
if not isinstance(category_block, dict):
|
||||||
|
category_block = {}
|
||||||
|
config_data[category] = category_block
|
||||||
|
|
||||||
|
plugin_block = category_block.get(plugin_name)
|
||||||
|
if not isinstance(plugin_block, dict):
|
||||||
|
plugin_block = {}
|
||||||
|
category_block[plugin_name] = plugin_block
|
||||||
|
|
||||||
|
if _looks_like_single_instance_branch(plugin_name, plugin_block):
|
||||||
|
existing_default = dict(plugin_block)
|
||||||
|
plugin_block.clear()
|
||||||
|
plugin_block["default"] = existing_default
|
||||||
|
|
||||||
|
target_key = None
|
||||||
|
lowered_target = raw_instance_name.lower()
|
||||||
|
for existing_key in plugin_block.keys():
|
||||||
|
if str(existing_key or "").strip().lower() == lowered_target:
|
||||||
|
target_key = str(existing_key)
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_key is not None and isinstance(plugin_block.get(target_key), dict):
|
||||||
|
return f"{category}.{plugin_name}.{target_key}", False
|
||||||
|
|
||||||
|
plugin_block[raw_instance_name] = dict(build_default_plugin_config(plugin_name))
|
||||||
|
return f"{category}.{plugin_name}.{raw_instance_name}", True
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_update_key(config_data: Dict[str, Any], selection_key: str) -> str:
|
||||||
|
parts = _split_config_path(selection_key)
|
||||||
|
if (
|
||||||
|
len(parts) >= 4
|
||||||
|
and parts[0] in {"plugin", "provider"}
|
||||||
|
and parts[2].lower() == "default"
|
||||||
|
and _is_multi_instance_plugin_name(parts[1])
|
||||||
|
):
|
||||||
|
category_block = config_data.get(parts[0])
|
||||||
|
plugin_block = category_block.get(parts[1]) if isinstance(category_block, dict) else None
|
||||||
|
if _looks_like_single_instance_branch(parts[1], plugin_block):
|
||||||
|
return ".".join([parts[0], parts[1], *parts[3:]])
|
||||||
|
return selection_key
|
||||||
|
|
||||||
|
|
||||||
def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
items: List[Dict[str, Any]] = []
|
items: List[Dict[str, Any]] = []
|
||||||
visible_entries = _visible_config_entries(config_data)
|
visible_entries = _visible_config_entries(config_data)
|
||||||
@@ -444,7 +732,13 @@ def _build_nested_config_items(
|
|||||||
|
|
||||||
section_items: List[Dict[str, Any]] = []
|
section_items: List[Dict[str, Any]] = []
|
||||||
value_items: List[Dict[str, Any]] = []
|
value_items: List[Dict[str, Any]] = []
|
||||||
|
action_items: List[Dict[str, Any]] = []
|
||||||
is_preferences_view = browse_path == _PREFERENCES_BROWSE_PATH
|
is_preferences_view = browse_path == _PREFERENCES_BROWSE_PATH
|
||||||
|
parts = _split_config_path(browse_path)
|
||||||
|
is_multi_instance_root = _is_multi_instance_plugin_root_path(browse_path)
|
||||||
|
|
||||||
|
if is_multi_instance_root:
|
||||||
|
branch = _normalize_multi_instance_branch(parts[1], branch)
|
||||||
|
|
||||||
for key, value in _visible_config_entries(branch):
|
for key, value in _visible_config_entries(branch):
|
||||||
full_key = key if is_preferences_view else f"{browse_path}.{key}"
|
full_key = key if is_preferences_view else f"{browse_path}.{key}"
|
||||||
@@ -467,7 +761,9 @@ def _build_nested_config_items(
|
|||||||
|
|
||||||
section_items.sort(key=lambda item: str(item.get("title") or "").lower())
|
section_items.sort(key=lambda item: str(item.get("title") or "").lower())
|
||||||
value_items.sort(key=lambda item: str(item.get("name") or "").lower())
|
value_items.sort(key=lambda item: str(item.get("name") or "").lower())
|
||||||
return section_items + value_items
|
if is_multi_instance_root:
|
||||||
|
action_items.append(_build_create_instance_item(parts[0], parts[1]))
|
||||||
|
return section_items + value_items + action_items
|
||||||
|
|
||||||
|
|
||||||
def _build_config_items(
|
def _build_config_items(
|
||||||
@@ -493,12 +789,31 @@ def _build_config_header_lines(browse_path: Optional[str]) -> List[str]:
|
|||||||
return [
|
return [
|
||||||
"Use @N on a section to drill in. Use @.. to go back.",
|
"Use @N on a section to drill in. Use @.. to go back.",
|
||||||
]
|
]
|
||||||
|
if _is_multi_instance_plugin_root_path(text):
|
||||||
|
return [
|
||||||
|
f"Path: {_format_config_path_label(text)}",
|
||||||
|
"Use @N on an instance to drill in. Use @N | .config <name> on Add Instance to create a new instance, then update its fields in the table that opens. Use @.. to go back.",
|
||||||
|
]
|
||||||
|
parts = _split_config_path(text)
|
||||||
|
if (
|
||||||
|
len(parts) == 3
|
||||||
|
and parts[0] in {"plugin", "provider"}
|
||||||
|
and _is_multi_instance_plugin_name(parts[1])
|
||||||
|
):
|
||||||
|
return [
|
||||||
|
f"Path: {_format_config_path_label(text)}",
|
||||||
|
"Use @N | .config <value> to update a setting. After creating an instance, set its path, credentials, or other fields here. Use @.. to go back.",
|
||||||
|
]
|
||||||
return [
|
return [
|
||||||
f"Path: {_format_config_path_label(text)}",
|
f"Path: {_format_config_path_label(text)}",
|
||||||
"Use @N on a section to drill in. Use @N | .config <value> to update a setting. Use @.. to go back.",
|
"Use @N on a section to drill in. Use @N | .config <value> to update a setting. Use @.. to go back.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_create_instance_target(args: Sequence[str]) -> Optional[str]:
|
||||||
|
return _extract_arg_value(args, flags={_CREATE_INSTANCE_FLAG, "--create-instance"}, allow_positional=False)
|
||||||
|
|
||||||
|
|
||||||
def _extract_browse_arg(args: Sequence[str]) -> Optional[str]:
|
def _extract_browse_arg(args: Sequence[str]) -> Optional[str]:
|
||||||
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
|
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
|
||||||
|
|
||||||
@@ -529,18 +844,49 @@ def _get_selected_config_item() -> Optional[Dict[str, Any]]:
|
|||||||
idx = indices[0]
|
idx = indices[0]
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
return None
|
return None
|
||||||
item = items[idx]
|
return _normalize_config_item(items[idx])
|
||||||
if isinstance(item, dict):
|
|
||||||
return item
|
|
||||||
|
def _normalize_config_item(candidate: Any) -> Optional[Dict[str, Any]]:
|
||||||
|
if candidate is None:
|
||||||
|
return None
|
||||||
|
|
||||||
normalized: Dict[str, Any] = {}
|
normalized: Dict[str, Any] = {}
|
||||||
for key in ("kind", "key", "title", "browse_path", "name", "value", "value_display", "type"):
|
sources: List[Any] = [candidate]
|
||||||
|
|
||||||
|
if isinstance(candidate, dict):
|
||||||
|
extra = candidate.get("extra")
|
||||||
|
if isinstance(extra, dict):
|
||||||
|
sources.append(extra)
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
value = getattr(item, key, None)
|
extra = getattr(candidate, "extra", None)
|
||||||
except Exception:
|
except Exception:
|
||||||
value = None
|
extra = None
|
||||||
if value is not None:
|
if isinstance(extra, dict):
|
||||||
normalized[key] = value
|
sources.append(extra)
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
if isinstance(source, dict):
|
||||||
|
getter = source.get
|
||||||
|
for key in _CONFIG_ITEM_FIELDS:
|
||||||
|
if key in normalized:
|
||||||
|
continue
|
||||||
|
value = getter(key)
|
||||||
|
if value is not None:
|
||||||
|
normalized[key] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
for key in _CONFIG_ITEM_FIELDS:
|
||||||
|
if key in normalized:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
value = getattr(source, key, None)
|
||||||
|
except Exception:
|
||||||
|
value = None
|
||||||
|
if value is not None:
|
||||||
|
normalized[key] = value
|
||||||
|
|
||||||
return normalized or None
|
return normalized or None
|
||||||
|
|
||||||
|
|
||||||
@@ -573,6 +919,11 @@ def _show_config_table(
|
|||||||
idx,
|
idx,
|
||||||
[".config", "-browse", str(item.get("browse_path"))],
|
[".config", "-browse", str(item.get("browse_path"))],
|
||||||
)
|
)
|
||||||
|
elif item.get("kind") == "create_instance" and item.get("instance_target"):
|
||||||
|
table.set_row_selection_action(
|
||||||
|
idx,
|
||||||
|
[".config", _CREATE_INSTANCE_FLAG, str(item.get("instance_target"))],
|
||||||
|
)
|
||||||
|
|
||||||
ctx.set_last_result_table(table, items)
|
ctx.set_last_result_table(table, items)
|
||||||
ctx.set_current_stage_table(table)
|
ctx.set_current_stage_table(table)
|
||||||
@@ -602,6 +953,15 @@ def _resolve_direct_browse_path(
|
|||||||
return _PREFERENCES_BROWSE_PATH
|
return _PREFERENCES_BROWSE_PATH
|
||||||
if lowered in {"plugins", "plugin", "providers", "provider", "tools", "tool"}:
|
if lowered in {"plugins", "plugin", "providers", "provider", "tools", "tool"}:
|
||||||
return _PLUGINS_BROWSE_PATH
|
return _PLUGINS_BROWSE_PATH
|
||||||
|
|
||||||
|
plugin_branch = _resolve_plugin_branch(config_data, "plugin", lowered)
|
||||||
|
if plugin_branch is not None:
|
||||||
|
return f"plugin.{plugin_branch[0]}"
|
||||||
|
|
||||||
|
tool_branch = _resolve_plugin_branch(config_data, "tool", lowered)
|
||||||
|
if tool_branch is not None:
|
||||||
|
return f"tool.{tool_branch[0]}"
|
||||||
|
|
||||||
branch = _resolve_config_branch(config_data, text)
|
branch = _resolve_config_branch(config_data, text)
|
||||||
if isinstance(branch, dict):
|
if isinstance(branch, dict):
|
||||||
return text
|
return text
|
||||||
@@ -629,27 +989,63 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
|||||||
if browse_path:
|
if browse_path:
|
||||||
return _show_config_table(current_config, browse_path=browse_path)
|
return _show_config_table(current_config, browse_path=browse_path)
|
||||||
|
|
||||||
selection_item = _get_selected_config_item()
|
selection_item = _get_selected_config_item() or _normalize_config_item(piped_result)
|
||||||
|
|
||||||
|
create_instance_target = _extract_create_instance_target(args)
|
||||||
|
if create_instance_target:
|
||||||
|
print(
|
||||||
|
f"Use @N | .config <instance_name> to create a new instance under '{_format_config_path_label(create_instance_target)}', then set its fields in the table that opens."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
value_from_pipe = _extract_piped_value(piped_result)
|
value_from_pipe = _extract_piped_value(piped_result)
|
||||||
selection_kind = str((selection_item or {}).get("kind") or "").strip().lower()
|
selection_kind = str((selection_item or {}).get("kind") or "").strip().lower()
|
||||||
selection_key = str((selection_item or {}).get("key") or "").strip() or None
|
selection_key = str((selection_item or {}).get("key") or "").strip() or None
|
||||||
selection_browse_path = str((selection_item or {}).get("browse_path") or "").strip() or None
|
selection_browse_path = str((selection_item or {}).get("browse_path") or "").strip() or None
|
||||||
selection_display_path = str((selection_item or {}).get("display_path") or selection_key or "").strip() or selection_key
|
selection_display_path = str((selection_item or {}).get("display_path") or selection_key or "").strip() or selection_key
|
||||||
|
selection_instance_target = str((selection_item or {}).get("instance_target") or "").strip() or None
|
||||||
|
|
||||||
if selection_kind == "section" and selection_browse_path and not args and value_from_pipe is None:
|
if selection_kind == "section" and selection_browse_path and not args and value_from_pipe is None:
|
||||||
return _show_config_table(current_config, browse_path=selection_browse_path)
|
return _show_config_table(current_config, browse_path=selection_browse_path)
|
||||||
|
|
||||||
|
if selection_kind == "create_instance" and selection_instance_target:
|
||||||
|
new_instance_name = value_from_pipe or _extract_selected_update_value(args)
|
||||||
|
if new_instance_name is None:
|
||||||
|
print(
|
||||||
|
f"Use @N | .config <instance_name> to create a new instance under '{_format_config_path_label(selection_instance_target)}', then set its fields in the table that opens."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
new_instance_name = _strip_value_quotes(new_instance_name)
|
||||||
|
try:
|
||||||
|
new_browse_path, created = _create_or_get_plugin_instance(
|
||||||
|
current_config,
|
||||||
|
selection_instance_target,
|
||||||
|
new_instance_name,
|
||||||
|
)
|
||||||
|
_save_updated_config(current_config, new_browse_path)
|
||||||
|
status_text = "Created" if created else "Using existing"
|
||||||
|
print(
|
||||||
|
f"{status_text} instance '{new_instance_name}' at '{_format_config_path_label(new_browse_path)}'. "
|
||||||
|
"Configure its fields in the table below."
|
||||||
|
)
|
||||||
|
return _show_config_table(current_config, browse_path=new_browse_path)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Error creating config instance '{selection_instance_target}': {exc}")
|
||||||
|
print(f"Error creating config instance: {exc}")
|
||||||
|
return 1
|
||||||
|
|
||||||
if selection_kind == "value" and selection_key:
|
if selection_kind == "value" and selection_key:
|
||||||
new_value = value_from_pipe or _extract_selected_update_value(args)
|
new_value = value_from_pipe or _extract_selected_update_value(args)
|
||||||
if new_value is not None:
|
if new_value is not None:
|
||||||
new_value = _strip_value_quotes(new_value)
|
new_value = _strip_value_quotes(new_value)
|
||||||
|
target_key = _resolve_update_key(current_config, selection_key)
|
||||||
try:
|
try:
|
||||||
set_nested_config(current_config, selection_key, new_value)
|
set_nested_config(current_config, target_key, new_value)
|
||||||
_save_updated_config(current_config, selection_key)
|
_save_updated_config(current_config, target_key)
|
||||||
print(f"Updated '{selection_display_path}' to '{new_value}'")
|
print(f"Updated '{selection_display_path}' to '{new_value}'")
|
||||||
return 0
|
return 0
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Error updating config '{selection_key}': {exc}")
|
log(f"Error updating config '{target_key}': {exc}")
|
||||||
print(f"Error updating config: {exc}")
|
print(f"Error updating config: {exc}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased (2026-01-05)
|
## Unreleased (2026-01-05)
|
||||||
|
|
||||||
- **docs:** Add `docs/provider_authoring.md` as a plugin authoring Quick Start for the strict `ResultTable` API (ResultModel/ColumnSpec/selection_fn). The file keeps its legacy name for compatibility.
|
- **docs:** Add `docs/plugin_authoring.md` as a plugin authoring Quick Start for the strict `ResultTable` API (ResultModel/ColumnSpec/selection_fn).
|
||||||
- **docs:** Add a link from `docs/result_table.md` to the plugin authoring guide.
|
- **docs:** Add a link from `docs/result_table.md` to the plugin authoring guide.
|
||||||
- **tests:** Add `tests/test_provider_author_examples.py` validating example plugin registration and adapter behavior. The test file keeps its legacy name for compatibility.
|
- **tests:** Add plugin-author example coverage validating example plugin registration and adapter behavior.
|
||||||
- **notes:** Existing example plugins (`plugins/example_provider.py`, `plugins/vimm/__init__.py`) are referenced as canonical patterns.
|
- **notes:** Existing example plugins (`plugins/example_plugin.py`, `plugins/vimm/__init__.py`) are referenced as canonical patterns.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
PR Title: docs: Add plugin authoring doc, examples, and tests
|
PR Title: docs: Add plugin authoring doc, examples, and tests
|
||||||
|
|
||||||
Summary:
|
Summary:
|
||||||
- Add `docs/provider_authoring.md` describing the strict `ResultModel`-based plugin adapter pattern, `ColumnSpec` usage, `selection_fn`, and `TableProviderMixin` for HTML table scraping.
|
- Add `docs/plugin_authoring.md` describing the strict `ResultModel`-based plugin adapter pattern, `ColumnSpec` usage, `selection_fn`, and `TablePluginMixin` for HTML table scraping.
|
||||||
- Link the new doc from `docs/result_table.md`.
|
- Link the new doc from `docs/result_table.md`.
|
||||||
- Add `tests/test_provider_author_examples.py` to validate the example plugin integration with the registry.
|
- Add plugin-author example coverage to validate the example plugin integration with the registry.
|
||||||
|
|
||||||
Why:
|
Why:
|
||||||
- Provide a short, focused Quick Start to help contributors author plugins that integrate with the strict ResultTable API.
|
- Provide a short, focused Quick Start to help contributors author plugins that integrate with the strict ResultTable API.
|
||||||
@@ -5,16 +5,15 @@ ResultTable API: adapters yield `ResultModel` instances, and plugins register
|
|||||||
via `SYS.result_table_adapters.register_plugin` with columns and a
|
via `SYS.result_table_adapters.register_plugin` with columns and a
|
||||||
`selection_fn`.
|
`selection_fn`.
|
||||||
|
|
||||||
Note: this file keeps its historical `provider_authoring` name, but the public
|
The public terminology is plugin-first, even though some internal classes and
|
||||||
terminology is plugin-first. Some internal classes and metadata fields still use
|
metadata fields still use `Provider` naming.
|
||||||
`Provider` naming.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick summary
|
## Quick summary
|
||||||
- Plugins register a plugin adapter, a `columns` definition, and a `selection_fn`.
|
- Plugins register a plugin adapter, a `columns` definition, and a `selection_fn`.
|
||||||
- `selection_fn` returns CLI args for a selected row.
|
- `selection_fn` returns CLI args for a selected row.
|
||||||
- For HTML table or list scraping, prefer `TableProviderMixin` from `SYS.provider_helpers`.
|
- For HTML table or list scraping, prefer `TablePluginMixin` from `SYS.plugin_helpers`.
|
||||||
|
|
||||||
## Runtime dependency policy
|
## Runtime dependency policy
|
||||||
- Treat required runtime dependencies such as Playwright as mandatory: import them unconditionally and let missing dependencies fail fast.
|
- Treat required runtime dependencies such as Playwright as mandatory: import them unconditionally and let missing dependencies fail fast.
|
||||||
@@ -74,16 +73,16 @@ register_plugin("myplugin", adapter, columns=columns_factory, selection_fn=selec
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table scraping with `TableProviderMixin`
|
## Table scraping with `TablePluginMixin`
|
||||||
|
|
||||||
If your plugin scrapes HTML tables or list-like results, use `TableProviderMixin`:
|
If your plugin scrapes HTML tables or list-like results, use `TablePluginMixin`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from ProviderCore.base import Provider
|
from PluginCore.base import Provider
|
||||||
from SYS.provider_helpers import TableProviderMixin
|
from SYS.plugin_helpers import TablePluginMixin
|
||||||
|
|
||||||
|
|
||||||
class MyTablePlugin(TableProviderMixin, Provider):
|
class MyTablePlugin(TablePluginMixin, Provider):
|
||||||
URL = ("https://example.org/search",)
|
URL = ("https://example.org/search",)
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
@@ -94,8 +93,8 @@ class MyTablePlugin(TableProviderMixin, Provider):
|
|||||||
return self.search_table_from_url(url, limit=limit)
|
return self.search_table_from_url(url, limit=limit)
|
||||||
```
|
```
|
||||||
|
|
||||||
`TableProviderMixin.search_table_from_url` returns
|
`TablePluginMixin.search_table_from_url` returns
|
||||||
`ProviderCore.base.SearchResult` entries. If you want to integrate the plugin
|
`PluginCore.base.SearchResult` entries. If you want to integrate the plugin
|
||||||
with the strict `ResultTable` registry, add a small adapter that converts
|
with the strict `ResultTable` registry, add a small adapter that converts
|
||||||
`SearchResult` to `ResultModel` and register it using `register_plugin`.
|
`SearchResult` to `ResultModel` and register it using `register_plugin`.
|
||||||
|
|
||||||
@@ -128,22 +127,22 @@ Example test skeleton:
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
from SYS.result_table_adapters import get_plugin
|
from SYS.result_table_adapters import get_plugin
|
||||||
from plugins import example_provider
|
from plugins import example_plugin
|
||||||
|
|
||||||
|
|
||||||
def test_example_plugin_registration():
|
def test_example_plugin_registration():
|
||||||
plugin = get_plugin("example")
|
plugin = get_plugin("example")
|
||||||
rows = list(plugin.adapter(example_provider.SAMPLE_ITEMS))
|
rows = list(plugin.adapter(example_plugin.SAMPLE_ITEMS))
|
||||||
assert rows and rows[0].title
|
assert rows and rows[0].title
|
||||||
cols = plugin.get_columns(rows)
|
cols = plugin.get_columns(rows)
|
||||||
assert any(col.name == "title" for col in cols)
|
assert any(col.name == "title" for col in cols)
|
||||||
table = plugin.build_table(example_provider.SAMPLE_ITEMS)
|
table = plugin.build_table(example_plugin.SAMPLE_ITEMS)
|
||||||
assert table.provider == "example" and table.rows
|
assert table.provider == "example" and table.rows
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## References and examples
|
## References and examples
|
||||||
- Read `plugins/example_provider.py` for a compact example of a strict adapter and dynamic columns.
|
- Read `plugins/example_plugin.py` for a compact example of a strict adapter and dynamic columns.
|
||||||
- Read `plugins/vimm/__init__.py` for a table-oriented plugin that uses `TableProviderMixin` and converts `SearchResult` to `ResultModel` for registration.
|
- Read `plugins/vimm/__init__.py` for a table-oriented plugin that uses `TablePluginMixin` and converts `SearchResult` to `ResultModel` for registration.
|
||||||
- See `docs/provider_guide.md` for a broader plugin development checklist.
|
- See `docs/plugin_guide.md` for a broader plugin development checklist.
|
||||||
@@ -4,9 +4,8 @@
|
|||||||
This guide describes how to write, test, and register a plugin so the
|
This guide describes how to write, test, and register a plugin so the
|
||||||
application can discover and use it as a pluggable component.
|
application can discover and use it as a pluggable component.
|
||||||
|
|
||||||
Note: this file keeps its historical `provider_guide` name, but the public
|
The public model is plugin-first, even though the internal base class still
|
||||||
model is plugin-first. Some runtime classes still use `Provider` naming
|
uses `Provider` naming.
|
||||||
internally.
|
|
||||||
|
|
||||||
Keep plugin code small, focused, and well-tested. Bundled plugins and drop-in
|
Keep plugin code small, focused, and well-tested. Bundled plugins and drop-in
|
||||||
plugins share the same `plugins/` layout.
|
plugins share the same `plugins/` layout.
|
||||||
@@ -15,7 +14,7 @@ plugins share the same `plugins/` layout.
|
|||||||
|
|
||||||
## Anatomy of a plugin
|
## Anatomy of a plugin
|
||||||
A plugin is a Python class that currently extends the internal base class
|
A plugin is a Python class that currently extends the internal base class
|
||||||
`ProviderCore.base.Provider` and implements a few key methods and attributes.
|
`PluginCore.base.Provider` and implements a few key methods and attributes.
|
||||||
|
|
||||||
Minimum expectations:
|
Minimum expectations:
|
||||||
- `class MyPlugin(Provider):` subclasses the current internal base plugin class.
|
- `class MyPlugin(Provider):` subclasses the current internal base plugin class.
|
||||||
@@ -31,7 +30,7 @@ Optional but common:
|
|||||||
---
|
---
|
||||||
|
|
||||||
## SearchResult
|
## SearchResult
|
||||||
Use `ProviderCore.base.SearchResult` to describe results returned by `search()`.
|
Use `PluginCore.base.SearchResult` to describe results returned by `search()`.
|
||||||
|
|
||||||
Important fields:
|
Important fields:
|
||||||
- `table` (str): plugin table name
|
- `table` (str): plugin table name
|
||||||
@@ -55,7 +54,7 @@ Return a list of `SearchResult(...)` objects or simple dicts convertible with `.
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
|
|
||||||
|
|
||||||
class HelloPlugin(Provider):
|
class HelloPlugin(Provider):
|
||||||
@@ -132,7 +131,7 @@ pytest -q
|
|||||||
- Bundled plugins live under `plugins/` and are auto-discovered from that package.
|
- Bundled plugins live under `plugins/` and are auto-discovered from that package.
|
||||||
- External plugins can be dropped into `plugins/` or any directory listed in `MM_PLUGIN_PATH` or `MEDEIA_PLUGIN_PATH`.
|
- External plugins can be dropped into `plugins/` or any directory listed in `MM_PLUGIN_PATH` or `MEDEIA_PLUGIN_PATH`.
|
||||||
- Package directories are preferred so plugin-specific files travel with the plugin.
|
- Package directories are preferred so plugin-specific files travel with the plugin.
|
||||||
- Plugin authors should import from `ProviderCore.*`.
|
- Plugin authors should import from `PluginCore.*`.
|
||||||
|
|
||||||
If a plugin supports multiple configured endpoints or accounts, the user-facing
|
If a plugin supports multiple configured endpoints or accounts, the user-facing
|
||||||
concept is a plugin instance. Some stored config still lives under legacy key
|
concept is a plugin instance. Some stored config still lives under legacy key
|
||||||
@@ -181,4 +181,4 @@ another, such as artist to discography drill-in.
|
|||||||
- ResultTable implementation: `SYS/result_table.py`
|
- ResultTable implementation: `SYS/result_table.py`
|
||||||
- Pipeline helpers: `SYS/pipeline.py`
|
- Pipeline helpers: `SYS/pipeline.py`
|
||||||
- Row replay and selection flow: `SYS/pipeline.py`
|
- Row replay and selection flow: `SYS/pipeline.py`
|
||||||
- Plugin authoring guide: `docs/provider_authoring.md`
|
- Plugin authoring guide: `docs/plugin_authoring.md`
|
||||||
|
|||||||
+2
-2
@@ -29,13 +29,13 @@ drop-in plugin search paths are:
|
|||||||
|
|
||||||
Plugin rules:
|
Plugin rules:
|
||||||
- A plugin can be a single `.py` file or a package directory with `__init__.py`.
|
- A plugin can be a single `.py` file or a package directory with `__init__.py`.
|
||||||
- Current plugin classes inherit from `ProviderCore.base.Provider`.
|
- Current plugin classes inherit from `PluginCore.base.Provider`.
|
||||||
- Give the plugin a stable name using `PLUGIN_NAME` or the class name.
|
- Give the plugin a stable name using `PLUGIN_NAME` or the class name.
|
||||||
|
|
||||||
Example skeleton:
|
Example skeleton:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
|
|
||||||
|
|
||||||
class MyPlugin(Provider):
|
class MyPlugin(Provider):
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from API.HTTP import HTTPClient, _download_direct_file
|
from API.HTTP import HTTPClient, _download_direct_file
|
||||||
from plugins.alldebrid.api import AllDebridClient, parse_magnet_or_hash, is_torrent_file
|
from plugins.alldebrid.api import AllDebridClient, parse_magnet_or_hash, is_torrent_file
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.provider_helpers import TableProviderMixin
|
from SYS.plugin_helpers import TablePluginMixin
|
||||||
from SYS.item_accessors import get_field as _extract_value
|
from SYS.item_accessors import get_field as _extract_value
|
||||||
from SYS.utils import sanitize_filename
|
from SYS.utils import sanitize_filename
|
||||||
from SYS.logger import log, debug, debug_panel
|
from SYS.logger import log, debug, debug_panel
|
||||||
@@ -637,7 +637,7 @@ def adjust_output_dir_for_alldebrid(
|
|||||||
return output_dir
|
return output_dir
|
||||||
|
|
||||||
|
|
||||||
class AllDebrid(TableProviderMixin, Provider):
|
class AllDebrid(TablePluginMixin, Provider):
|
||||||
"""AllDebrid account provider with magnet folder/file browsing and downloads.
|
"""AllDebrid account provider with magnet folder/file browsing and downloads.
|
||||||
|
|
||||||
This provider uses the new table system (strict ResultTable adapter pattern) for
|
This provider uses the new table system (strict ResultTable adapter pattern) for
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import sys
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.logger import log, debug, debug_panel
|
from SYS.logger import log, debug, debug_panel
|
||||||
|
|
||||||
from tool.playwright import PlaywrightTool
|
from tool.playwright import PlaywrightTool
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""Example plugin that uses the new `ResultTable` API.
|
"""Example plugin that uses the new `ResultTable` API.
|
||||||
|
|
||||||
This module demonstrates a minimal provider adapter that yields `ResultModel`
|
This module demonstrates a minimal plugin adapter that yields `ResultModel`
|
||||||
instances, a set of `ColumnSpec` definitions, and a tiny CLI-friendly renderer
|
instances, a set of `ColumnSpec` definitions, and a tiny CLI-friendly renderer
|
||||||
(`render_table`) for demonstration.
|
(`render_table`) for demonstration.
|
||||||
|
|
||||||
Run this to see sample output:
|
Run this to see sample output:
|
||||||
python -m Provider.example_provider
|
python -m plugins.example_plugin
|
||||||
|
|
||||||
Example usage (piped selector):
|
Example usage (piped selector):
|
||||||
plugin-table -plugin example -sample | select -select 1 | add-file -instance default
|
plugin-table -plugin example -sample | select -select 1 | add-file -instance default
|
||||||
@@ -44,7 +44,7 @@ SAMPLE_ITEMS = [
|
|||||||
|
|
||||||
|
|
||||||
def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
|
def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
|
||||||
"""Convert provider-specific items into `ResultModel` instances.
|
"""Convert plugin-specific items into `ResultModel` instances.
|
||||||
|
|
||||||
This adapter enforces the strict API requirement: it yields only
|
This adapter enforces the strict API requirement: it yields only
|
||||||
`ResultModel` instances (no legacy dict objects).
|
`ResultModel` instances (no legacy dict objects).
|
||||||
@@ -62,7 +62,7 @@ def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
|
|||||||
|
|
||||||
|
|
||||||
# Columns are intentionally *not* mandated. Create a factory that inspects
|
# Columns are intentionally *not* mandated. Create a factory that inspects
|
||||||
# sample rows and builds only columns that make sense for the provider data.
|
# sample rows and builds only columns that make sense for the plugin data.
|
||||||
from SYS.result_table_api import metadata_column
|
from SYS.result_table_api import metadata_column
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ register_plugin(
|
|||||||
adapter,
|
adapter,
|
||||||
columns=columns_factory,
|
columns=columns_factory,
|
||||||
selection_fn=selection_fn,
|
selection_fn=selection_fn,
|
||||||
metadata={"description": "Example provider demonstrating dynamic columns and selectors"},
|
metadata={"description": "Example plugin demonstrating dynamic columns and selectors"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from ProviderCore.base import Provider
|
from PluginCore.base import Provider
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
from urllib.parse import quote, unquote, urlparse
|
from urllib.parse import quote, unquote, urlparse
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||||
|
|
||||||
|
|
||||||
def _coerce_bool(value: Any, default: bool = False) -> bool:
|
def _coerce_bool(value: Any, default: bool = False) -> bool:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ This minimal plugin demonstrates the typical hooks a plugin may implement:
|
|||||||
- `search()` to return `SearchResult` items
|
- `search()` to return `SearchResult` items
|
||||||
- `download()` to persist a sample file (useful for local tests)
|
- `download()` to persist a sample file (useful for local tests)
|
||||||
|
|
||||||
See `docs/provider_guide.md` for authoring guidance.
|
See `docs/plugin_guide.md` for authoring guidance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
|
|
||||||
|
|
||||||
class HelloProvider(Provider):
|
class HelloProvider(Provider):
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from plugins.tidal.api import (
|
|||||||
extract_artists,
|
extract_artists,
|
||||||
stringify,
|
stringify,
|
||||||
)
|
)
|
||||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||||
from SYS.field_access import get_field
|
from SYS.field_access import get_field
|
||||||
from plugins.tidal_manifest import resolve_tidal_manifest_path
|
from plugins.tidal_manifest import resolve_tidal_manifest_path
|
||||||
from SYS import pipeline as pipeline_context
|
from SYS import pipeline as pipeline_context
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
from urllib.parse import quote, unquote, urlparse
|
from urllib.parse import quote, unquote, urlparse
|
||||||
|
|
||||||
from API.HTTP import _download_direct_file
|
from API.HTTP import _download_direct_file
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.utils import sanitize_filename, unique_path
|
from SYS.utils import sanitize_filename, unique_path
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from SYS.config import get_provider_block
|
from SYS.config import get_provider_block
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
from urllib.parse import urljoin, urlparse, unquote
|
from urllib.parse import urljoin, urlparse, unquote
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.utils import sanitize_filename
|
from SYS.utils import sanitize_filename
|
||||||
from SYS.logger import log, debug
|
from SYS.logger import log, debug
|
||||||
from SYS.models import ProgressBar
|
from SYS.models import ProgressBar
|
||||||
@@ -383,7 +383,7 @@ def _enrich_book_tags_from_isbn(isbn: str,
|
|||||||
|
|
||||||
# 2) isbnsearch metadata plugin fallback.
|
# 2) isbnsearch metadata plugin fallback.
|
||||||
try:
|
try:
|
||||||
from plugins.metadata_provider import get_metadata_plugin
|
from plugins.metadata_plugin import get_metadata_plugin
|
||||||
|
|
||||||
provider = get_metadata_plugin("isbnsearch",
|
provider = get_metadata_plugin("isbnsearch",
|
||||||
config or {})
|
config or {})
|
||||||
@@ -660,7 +660,7 @@ class Libgen(Provider):
|
|||||||
"libgen": ["download-file"],
|
"libgen": ["download-file"],
|
||||||
}
|
}
|
||||||
# Domains that should be routed to this provider when the user supplies a URL.
|
# Domains that should be routed to this provider when the user supplies a URL.
|
||||||
# (Used by ProviderCore.registry.match_provider_name_for_url)
|
# (Used by PluginCore.registry.match_provider_name_for_url)
|
||||||
URL_DOMAINS = (
|
URL_DOMAINS = (
|
||||||
"libgen.gl",
|
"libgen.gl",
|
||||||
"libgen.li",
|
"libgen.li",
|
||||||
@@ -1056,7 +1056,7 @@ class Libgen(Provider):
|
|||||||
def download_url(self, url: str, output_dir: Path) -> Optional[Path]:
|
def download_url(self, url: str, output_dir: Path) -> Optional[Path]:
|
||||||
"""Download a direct LibGen URL using the regular mirror logic."""
|
"""Download a direct LibGen URL using the regular mirror logic."""
|
||||||
try:
|
try:
|
||||||
from ProviderCore.base import SearchResult
|
from PluginCore.base import SearchResult
|
||||||
sr = SearchResult(
|
sr = SearchResult(
|
||||||
table="libgen",
|
table="libgen",
|
||||||
title="libgen",
|
title="libgen",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from plugins.loc.api import LOCClient
|
from plugins.loc.api import LOCClient
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.cli_syntax import get_free_text, parse_query
|
from SYS.cli_syntax import get_free_text, parse_query
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import shutil
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from ProviderCore.base import Provider
|
from PluginCore.base import Provider
|
||||||
from SYS.metadata import write_metadata, write_tags
|
from SYS.metadata import write_metadata, write_tags
|
||||||
from SYS.utils import sanitize_filename, sha256_file, unique_path
|
from SYS.utils import sanitize_filename, sha256_file, unique_path
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from urllib.parse import quote
|
|||||||
from API.requests_client import get_requests_session
|
from API.requests_client import get_requests_session
|
||||||
from SYS.utils import ffprobe as probe_media_metadata
|
from SYS.utils import ffprobe as probe_media_metadata
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.provider_helpers import TableProviderMixin
|
from SYS.plugin_helpers import TablePluginMixin
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
|
|
||||||
_MATRIX_INIT_CHECK_CACHE: Dict[str,
|
_MATRIX_INIT_CHECK_CACHE: Dict[str,
|
||||||
@@ -276,7 +276,7 @@ def _matrix_health_check(*,
|
|||||||
return False, str(exc)
|
return False, str(exc)
|
||||||
|
|
||||||
|
|
||||||
class Matrix(TableProviderMixin, Provider):
|
class Matrix(TablePluginMixin, Provider):
|
||||||
"""Matrix (Element) room provider with file uploads and selection.
|
"""Matrix (Element) room provider with file uploads and selection.
|
||||||
|
|
||||||
This provider uses the new table system (strict ResultTable adapter pattern) for
|
This provider uses the new table system (strict ResultTable adapter pattern) for
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from SYS.command_parsing import (
|
|||||||
normalize_to_list as _normalize_to_list,
|
normalize_to_list as _normalize_to_list,
|
||||||
)
|
)
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from ProviderCore.registry import get_plugin, get_plugin_for_url
|
from PluginCore.registry import get_plugin, get_plugin_for_url
|
||||||
|
|
||||||
_MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items"
|
_MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items"
|
||||||
_MATRIX_PENDING_TEXT_KEY = "matrix_pending_text"
|
_MATRIX_PENDING_TEXT_KEY = "matrix_pending_text"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import subprocess
|
|||||||
|
|
||||||
from API.HTTP import HTTPClient
|
from API.HTTP import HTTPClient
|
||||||
from API.requests_client import get_requests_session
|
from API.requests_client import get_requests_session
|
||||||
from ProviderCore.base import SearchResult
|
from PluginCore.base import SearchResult
|
||||||
try:
|
try:
|
||||||
from plugins.tidal import Tidal
|
from plugins.tidal import Tidal
|
||||||
except ImportError: # pragma: no cover - optional
|
except ImportError: # pragma: no cover - optional
|
||||||
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
|
|||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from SYS.cmdlet_spec import Cmdlet, CmdletArg, parse_cmdlet_args
|
from SYS.cmdlet_spec import Cmdlet, CmdletArg, parse_cmdlet_args
|
||||||
from ProviderCore.registry import get_plugin, get_plugin_for_url, list_plugin_names_with_capability
|
from PluginCore.registry import get_plugin, get_plugin_for_url, list_plugin_names_with_capability
|
||||||
from SYS.logger import debug, get_thread_stream, is_debug_enabled, set_debug, set_thread_stream
|
from SYS.logger import debug, get_thread_stream, is_debug_enabled, set_debug, set_thread_stream
|
||||||
from SYS.result_table import Table
|
from SYS.result_table import Table
|
||||||
from plugins.mpv.mpv_ipc import MPV
|
from plugins.mpv.mpv_ipc import MPV
|
||||||
|
|||||||
@@ -1170,7 +1170,7 @@ def _infer_hydrus_store_from_url_target(*, target: str, config: dict) -> Optiona
|
|||||||
is_hydrus_backend = backend_type == "hydrusnetwork" or backend_class == "hydrusnetwork"
|
is_hydrus_backend = backend_type == "hydrusnetwork" or backend_class == "hydrusnetwork"
|
||||||
if not is_hydrus_backend:
|
if not is_hydrus_backend:
|
||||||
try:
|
try:
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
|
|
||||||
hydrus_provider = get_plugin("hydrusnetwork", config)
|
hydrus_provider = get_plugin("hydrusnetwork", config)
|
||||||
checker = getattr(hydrus_provider, "is_backend", None) if hydrus_provider is not None else None
|
checker = getattr(hydrus_provider, "is_backend", None) if hydrus_provider is not None else None
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ from SYS.config import load_config, reload_config # noqa: E402
|
|||||||
from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402
|
from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402
|
||||||
from SYS.repl_queue import enqueue_repl_command # noqa: E402
|
from SYS.repl_queue import enqueue_repl_command # noqa: E402
|
||||||
from SYS.utils import format_bytes # noqa: E402
|
from SYS.utils import format_bytes # noqa: E402
|
||||||
from ProviderCore.registry import get_plugin, get_plugin_class # noqa: E402
|
from PluginCore.registry import get_plugin, get_plugin_class # noqa: E402
|
||||||
from tool.ytdlp import get_display_format_id, get_selection_format_id # noqa: E402
|
from tool.ytdlp import get_display_format_id, get_selection_format_id # noqa: E402
|
||||||
|
|
||||||
REQUEST_PROP = "user-data/medeia-pipeline-request"
|
REQUEST_PROP = "user-data/medeia-pipeline-request"
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import requests
|
|||||||
|
|
||||||
from API.HTTP import HTTPClient
|
from API.HTTP import HTTPClient
|
||||||
from API.requests_client import get_requests_session
|
from API.requests_client import get_requests_session
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.utils import sanitize_filename
|
from SYS.utils import sanitize_filename
|
||||||
from SYS.cli_syntax import get_field, get_free_text, parse_query
|
from SYS.cli_syntax import get_field, get_free_text, parse_query
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from plugins.metadata_provider import (
|
from plugins.metadata_plugin import (
|
||||||
archive_item_metadata_to_tags,
|
archive_item_metadata_to_tags,
|
||||||
fetch_archive_item_metadata,
|
fetch_archive_item_metadata,
|
||||||
)
|
)
|
||||||
@@ -654,7 +654,7 @@ class OpenLibrary(Provider):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Domains that should be routed to this provider when the user supplies a URL.
|
# Domains that should be routed to this provider when the user supplies a URL.
|
||||||
# (Used by ProviderCore.registry.match_provider_name_for_url)
|
# (Used by PluginCore.registry.match_provider_name_for_url)
|
||||||
URL_DOMAINS = (
|
URL_DOMAINS = (
|
||||||
"openlibrary.org",
|
"openlibrary.org",
|
||||||
"archive.org",
|
"archive.org",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import hashlib
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from SYS.utils import format_bytes
|
from SYS.utils import format_bytes
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from urllib.parse import quote, unquote, urlparse
|
|||||||
import paramiko
|
import paramiko
|
||||||
from scp import SCPClient
|
from scp import SCPClient
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||||
|
|
||||||
|
|
||||||
def _coerce_bool(value: Any, default: bool = False) -> bool:
|
def _coerce_bool(value: Any, default: bool = False) -> bool:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.logger import log, debug, debug_panel
|
from SYS.logger import log, debug, debug_panel
|
||||||
from SYS.models import ProgressBar
|
from SYS.models import ProgressBar
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.logger import debug
|
from SYS.logger import debug
|
||||||
|
|
||||||
_TELEGRAM_DEFAULT_TIMESTAMP_STEM_RE = re.compile(
|
_TELEGRAM_DEFAULT_TIMESTAMP_STEM_RE = re.compile(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from SYS.command_parsing import has_flag as _has_flag, normalize_to_list as _nor
|
|||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
from SYS.result_table import Table
|
from SYS.result_table import Table
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from ProviderCore.registry import get_plugin
|
from PluginCore.registry import get_plugin
|
||||||
|
|
||||||
_TELEGRAM_PENDING_ITEMS_KEY = "telegram_pending_items"
|
_TELEGRAM_PENDING_ITEMS_KEY = "telegram_pending_items"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from plugins.tidal.api import (
|
|||||||
extract_artists,
|
extract_artists,
|
||||||
stringify,
|
stringify,
|
||||||
)
|
)
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.field_access import get_field
|
from SYS.field_access import get_field
|
||||||
from plugins.tidal_manifest import resolve_tidal_manifest_path
|
from plugins.tidal_manifest import resolve_tidal_manifest_path
|
||||||
from SYS import pipeline as pipeline_context
|
from SYS import pipeline as pipeline_context
|
||||||
@@ -216,7 +216,7 @@ class Tidal(Provider):
|
|||||||
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
|
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
|
||||||
"""Parse inline `key:value` query arguments.
|
"""Parse inline `key:value` query arguments.
|
||||||
|
|
||||||
Unlike the generic parser in ProviderCore, this supports multi-word
|
Unlike the generic parser in PluginCore, this supports multi-word
|
||||||
values (e.g. `artist:elliott smith`).
|
values (e.g. `artist:elliott smith`).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from API.requests_client import get_requests_session
|
from API.requests_client import get_requests_session
|
||||||
from ProviderCore.base import Provider, SearchResult
|
from PluginCore.base import Provider, SearchResult
|
||||||
from SYS.logger import debug, log
|
from SYS.logger import debug, log
|
||||||
try: # Preferred HTML parser
|
try: # Preferred HTML parser
|
||||||
from lxml import html as lxml_html
|
from lxml import html as lxml_html
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from API.HTTP import HTTPClient
|
from API.HTTP import HTTPClient
|
||||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||||
from ProviderCore.inline_utils import resolve_filter
|
from PluginCore.inline_utils import resolve_filter
|
||||||
from SYS.logger import debug, debug_panel
|
from SYS.logger import debug, debug_panel
|
||||||
from SYS.provider_helpers import TableProviderMixin
|
from SYS.plugin_helpers import TablePluginMixin
|
||||||
from tool.playwright import PlaywrightTool
|
from tool.playwright import PlaywrightTool
|
||||||
|
|
||||||
|
|
||||||
class Vimm(TableProviderMixin, Provider):
|
class Vimm(TablePluginMixin, Provider):
|
||||||
"""Minimal provider for vimm.net vault listings using TableProvider mixin.
|
"""Minimal provider for vimm.net vault listings using TableProvider mixin.
|
||||||
|
|
||||||
NOTES / HOW-TO (selection & auto-download):
|
NOTES / HOW-TO (selection & auto-download):
|
||||||
@@ -142,7 +142,7 @@ class Vimm(TableProviderMixin, Provider):
|
|||||||
],
|
],
|
||||||
"region": REGION_CHOICES,
|
"region": REGION_CHOICES,
|
||||||
}
|
}
|
||||||
# ProviderCore still looks for INLINE_QUERY_FIELD_CHOICES, so expose this
|
# PluginCore still looks for INLINE_QUERY_FIELD_CHOICES, so expose this
|
||||||
# mapping once and keep QUERY_ARG_CHOICES as the readable name we prefer.
|
# mapping once and keep QUERY_ARG_CHOICES as the readable name we prefer.
|
||||||
INLINE_QUERY_FIELD_CHOICES = QUERY_ARG_CHOICES
|
INLINE_QUERY_FIELD_CHOICES = QUERY_ARG_CHOICES
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||||
from SYS.provider_helpers import TableProviderMixin
|
from SYS.plugin_helpers import TablePluginMixin
|
||||||
from SYS.logger import debug, log
|
from SYS.logger import debug, log
|
||||||
from SYS.models import DownloadError, DownloadMediaResult, DownloadOptions
|
from SYS.models import DownloadError, DownloadMediaResult, DownloadOptions
|
||||||
from SYS.payload_builders import build_file_result_payload, build_table_result_payload
|
from SYS.payload_builders import build_file_result_payload, build_table_result_payload
|
||||||
@@ -501,7 +501,7 @@ def _build_pipe_objects(
|
|||||||
return pipe_objects
|
return pipe_objects
|
||||||
|
|
||||||
|
|
||||||
class ytdlp(TableProviderMixin, Provider):
|
class ytdlp(TablePluginMixin, Provider):
|
||||||
"""yt-dlp-backed search and direct download plugin."""
|
"""yt-dlp-backed search and direct download plugin."""
|
||||||
|
|
||||||
PLUGIN_NAME = "ytdlp"
|
PLUGIN_NAME = "ytdlp"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ProviderCore.base import Provider
|
from PluginCore.base import Provider
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Plugins are the main integration surface for the app.
|
|||||||
- Plugins can expose named instances, so one plugin can target multiple endpoints, accounts, or servers via `-instance <name>`.
|
- Plugins can expose named instances, so one plugin can target multiple endpoints, accounts, or servers via `-instance <name>`.
|
||||||
- Bundled and external plugins use the same `plugins/<name>/` layout.
|
- Bundled and external plugins use the same `plugins/<name>/` layout.
|
||||||
- External plugin search paths include the repo `plugins/` folder, the current working directory `plugins/` folder, `MM_PLUGIN_PATH`, and `MEDEIA_PLUGIN_PATH`.
|
- External plugin search paths include the repo `plugins/` folder, the current working directory `plugins/` folder, `MM_PLUGIN_PATH`, and `MEDEIA_PLUGIN_PATH`.
|
||||||
- Plugin authoring still uses the current Python base class name `ProviderCore.base.Provider`. That is an implementation detail rather than the preferred user-facing term.
|
- Plugin authoring still uses the current Python base class name `PluginCore.base.Provider`. That is an implementation detail rather than the preferred user-facing term.
|
||||||
|
|
||||||
See [plugins/README.md](plugins/README.md) for plugin packaging and discovery details.
|
See [plugins/README.md](plugins/README.md) for plugin packaging and discovery details.
|
||||||
|
|
||||||
@@ -140,8 +140,8 @@ The exact meaning of `@1` depends on the current table and plugin. For example,
|
|||||||
|
|
||||||
- [docs/tag_template_syntax.md](docs/tag_template_syntax.md)
|
- [docs/tag_template_syntax.md](docs/tag_template_syntax.md)
|
||||||
- [plugins/README.md](plugins/README.md)
|
- [plugins/README.md](plugins/README.md)
|
||||||
- [docs/provider_guide.md](docs/provider_guide.md)
|
- [docs/plugin_guide.md](docs/plugin_guide.md)
|
||||||
- [docs/provider_authoring.md](docs/provider_authoring.md)
|
- [docs/plugin_authoring.md](docs/plugin_authoring.md)
|
||||||
- [docs/ftp_plugin_tutorial.md](docs/ftp_plugin_tutorial.md)
|
- [docs/ftp_plugin_tutorial.md](docs/ftp_plugin_tutorial.md)
|
||||||
- [docs/scp_plugin_tutorial.md](docs/scp_plugin_tutorial.md)
|
- [docs/scp_plugin_tutorial.md](docs/scp_plugin_tutorial.md)
|
||||||
- [docs/BOOTSTRAP_TROUBLESHOOTING.md](docs/BOOTSTRAP_TROUBLESHOOTING.md)
|
- [docs/BOOTSTRAP_TROUBLESHOOTING.md](docs/BOOTSTRAP_TROUBLESHOOTING.md)
|
||||||
|
|||||||
@@ -121,8 +121,7 @@ packages = [
|
|||||||
"cmdnat",
|
"cmdnat",
|
||||||
"API",
|
"API",
|
||||||
"Store",
|
"Store",
|
||||||
"ProviderCore",
|
"PluginCore",
|
||||||
"Provider",
|
|
||||||
"SYS",
|
"SYS",
|
||||||
"tool",
|
"tool",
|
||||||
"TUI",
|
"TUI",
|
||||||
|
|||||||
Reference in New Issue
Block a user