update commit prev

This commit is contained in:
2026-04-26 16:49:23 -07:00
parent 39ee857559
commit bfd5c20dc3
25 changed files with 231 additions and 77 deletions
+6 -4
View File
@@ -527,7 +527,7 @@ class CmdletIntrospection:
if normalized_arg == "scrape":
try:
from Provider.metadata_provider import list_metadata_providers
from plugins.metadata_provider import list_metadata_providers
meta_providers = list_metadata_providers(config) or {}
if meta_providers:
@@ -2164,9 +2164,11 @@ Come to love it when others take what you share, as there is no greater joy
detail=detail
)
provider_cfg = config.get("provider"
) if isinstance(config,
dict) else None
provider_cfg = None
if isinstance(config, dict):
provider_cfg = config.get("plugin")
if not isinstance(provider_cfg, dict):
provider_cfg = config.get("provider")
if isinstance(provider_cfg, dict) and provider_cfg:
for check in _collect_plugin_startup_checks(config):
_add_startup_check(
+4 -9
View File
@@ -1,11 +1,6 @@
"""Built-in plugin modules.
"""Legacy compatibility package.
Concrete built-in plugins live in this package.
The public registry lives in ProviderCore.registry.
Bundled runtime plugins now live under ``plugins/``. This package remains only
for helper modules and backwards-compatible imports that have not been removed
yet.
"""
# Register providers with the strict ResultTable adapter system
try:
from . import alldebrid
except Exception:
pass
+1 -1
View File
@@ -12,7 +12,7 @@ from API.HTTP import HTTPClient
from API.requests_client import get_requests_session
from ProviderCore.base import SearchResult
try:
from Provider.Tidal import Tidal
from plugins.tidal import Tidal
except ImportError: # pragma: no cover - optional
Tidal = None
from API.Tidal import (
+1 -2
View File
@@ -1,6 +1,5 @@
"""Plugin core modules.
This package contains the plugin framework (base types, registry, and shared
helpers). Built-in plugins continue to live in the `Provider/` package for
backward compatibility.
helpers). Bundled plugins live in the `plugins/` package.
"""
+28 -10
View File
@@ -1,8 +1,8 @@
"""Plugin registry.
Built-in plugin implementations live in the ``Provider`` package. External user
plugins can be dropped into a repo-local ``plugins/`` directory or discovered
via environment-configured plugin paths.
Bundled plugins live in the ``plugins`` package. Additional drop-in plugins can
be discovered from external plugin directories configured via the environment or
current working directory.
"""
from __future__ import annotations
@@ -132,9 +132,23 @@ class ProviderRegistry:
self._lookup: Dict[str, ProviderInfo] = {}
self._modules: set[str] = set()
self._external_modules: set[str] = set()
self._builtin_package_dirs: Tuple[Path, ...] = ()
self._discovered = False
self._external_dirs_scanned = False
def _is_builtin_package_dir(self, candidate: Path) -> bool:
try:
resolved = candidate.resolve()
except Exception:
resolved = candidate
for package_dir in self._builtin_package_dirs:
try:
if resolved == package_dir:
return True
except Exception:
continue
return False
def _normalize(self, value: Any) -> str:
return str(value or "").strip().lower()
@@ -241,6 +255,8 @@ class ProviderRegistry:
self._external_dirs_scanned = True
for plugin_dir in _iter_external_plugin_dirs():
if self._is_builtin_package_dir(plugin_dir):
continue
try:
plugin_dir_str = str(plugin_dir)
if plugin_dir_str and plugin_dir_str not in sys.path:
@@ -287,6 +303,14 @@ class ProviderRegistry:
self._register_module(package)
package_path = getattr(package, "__path__", None)
if package_path:
builtin_dirs: List[Path] = []
for entry in package_path:
try:
builtin_dirs.append(Path(str(entry)).resolve())
except Exception:
builtin_dirs.append(Path(str(entry)))
self._builtin_package_dirs = tuple(builtin_dirs)
if not package_path:
self._discover_external_plugins()
return
@@ -294,8 +318,6 @@ class ProviderRegistry:
for finder, module_name, _ in pkgutil.iter_modules(package_path):
if module_name.startswith("_"):
continue
if module_name.strip().lower() == "hifi":
continue
module_path = f"{self.package_name}.{module_name}"
try:
module = importlib.import_module(module_path)
@@ -318,10 +340,6 @@ class ProviderRegistry:
if not name or not self.package_name:
return
# Keep behavior consistent with full discovery (which skips hifi).
if name == "hifi":
return
candidates: List[str] = [name]
if "-" in name:
candidates.append(name.replace("-", "_"))
@@ -386,7 +404,7 @@ class ProviderRegistry:
_walk(sub)
_walk(Provider)
REGISTRY = ProviderRegistry("Provider")
REGISTRY = ProviderRegistry("plugins")
PLUGIN_REGISTRY = REGISTRY
PluginInfo = ProviderInfo
PluginRegistry = ProviderRegistry
+82 -3
View File
@@ -93,7 +93,10 @@ def clear_config_cache() -> None:
def _log_config_load_summary(config: Dict[str, Any]) -> None:
try:
provs = list(config.get("provider", {}).keys()) if isinstance(config.get("provider"), dict) else []
plugin_block = config.get("plugin")
if not isinstance(plugin_block, dict):
plugin_block = config.get("provider")
provs = list(plugin_block.keys()) if isinstance(plugin_block, dict) else []
stores = list(config.get("store", {}).keys()) if isinstance(config.get("store"), dict) else []
mtime = None
try:
@@ -101,7 +104,7 @@ def _log_config_load_summary(config: Dict[str, Any]) -> None:
except Exception:
mtime = None
summary = (
f"Loaded config from {db.db_path.name}: providers={len(provs)} ({', '.join(provs[:10])}{'...' if len(provs)>10 else ''}), "
f"Loaded config from {db.db_path.name}: plugins={len(provs)} ({', '.join(provs[:10])}{'...' if len(provs)>10 else ''}), "
f"stores={len(stores)} ({', '.join(stores[:10])}{'...' if len(stores)>10 else ''}), mtime={mtime}"
)
log(summary)
@@ -325,6 +328,7 @@ def get_hydrus_url(config: Dict[str, Any], instance_name: str = "home") -> Optio
def get_provider_block(config: Dict[str, Any], name: str) -> Dict[str, Any]:
_normalize_plugin_config_aliases(config)
provider_cfg = config.get("provider")
if not isinstance(provider_cfg, dict):
return {}
@@ -518,7 +522,7 @@ def resolve_cookies_path(
except Exception as exc:
logger.debug("resolve_cookies_path: failed to read tool.ytdlp cookies: %s", exc, exc_info=True)
base_dir = script_dir or SCRIPT_DIR
base_dir = _resolve_app_root(script_dir)
for value in values:
if not value:
continue
@@ -528,6 +532,10 @@ def resolve_cookies_path(
if candidate.is_file():
return candidate
plugin_cookie = resolve_plugin_asset_path("ytdlp", "cookies.txt", script_dir=base_dir)
if plugin_cookie is not None:
return plugin_cookie
default_path = base_dir / "cookies.txt"
if default_path.is_file():
return default_path
@@ -547,6 +555,70 @@ def _normalize_provider_name(value: Any) -> Optional[str]:
candidate = str(value or "").strip().lower()
return candidate if candidate else None
def _resolve_app_root(script_dir: Optional[Path] = None) -> Path:
if script_dir is not None:
try:
candidate = expand_path(script_dir)
except Exception:
candidate = Path(script_dir)
return candidate if candidate.is_dir() else candidate.parent
return SCRIPT_DIR.parent
def resolve_plugin_asset_path(
plugin_name: str,
*relative_parts: str,
script_dir: Optional[Path] = None,
) -> Optional[Path]:
normalized = _normalize_provider_name(plugin_name)
if not normalized:
return None
plugin_dir = _resolve_app_root(script_dir) / "plugins" / normalized
if not plugin_dir.is_dir():
return None
for part in relative_parts:
text = str(part or "").strip().strip("/\\")
if not text:
continue
candidate = plugin_dir / text
if candidate.is_file():
return candidate
return None
def _normalize_plugin_config_aliases(config: Dict[str, Any]) -> None:
if not isinstance(config, dict):
return
plugin_block = config.get("plugin")
provider_block = config.get("provider")
normalized_provider: Dict[str, Any] = {}
if isinstance(provider_block, dict):
for key, value in provider_block.items():
normalized_key = _normalize_provider_name(key)
if normalized_key and normalized_key not in normalized_provider:
normalized_provider[normalized_key] = value
if isinstance(plugin_block, dict):
for key, value in plugin_block.items():
normalized_key = _normalize_provider_name(key)
if normalized_key and normalized_key not in normalized_provider:
normalized_provider[normalized_key] = value
if normalized_provider:
config["provider"] = normalized_provider
config["plugin"] = normalized_provider
else:
if isinstance(provider_block, dict):
config["plugin"] = provider_block
elif isinstance(plugin_block, dict):
config["provider"] = plugin_block
def _extract_api_key(value: Any) -> Optional[str]:
if isinstance(value, dict):
for key in ("api_key", "API_KEY", "apikey", "APIKEY"):
@@ -564,6 +636,8 @@ def _sync_alldebrid_api_key(config: Dict[str, Any]) -> None:
if not isinstance(config, dict):
return
_normalize_plugin_config_aliases(config)
providers = config.get("provider")
if not isinstance(providers, dict):
providers = {}
@@ -617,7 +691,10 @@ def _sync_alldebrid_api_key(config: Dict[str, Any]) -> None:
def _flatten_config_entries(config: Dict[str, Any]) -> Dict[Tuple[str, str, str, str], Any]:
entries: Dict[Tuple[str, str, str, str], Any] = {}
_normalize_plugin_config_aliases(config)
for key, value in config.items():
if key == 'plugin':
continue
if key in ('store', 'provider', 'tool') and isinstance(value, dict):
for subtype, instances in value.items():
if not isinstance(instances, dict):
@@ -655,6 +732,7 @@ def load_config(*, emit_summary: bool = True) -> Dict[str, Any]:
# Load strictly from database
db_config = get_config_all()
if db_config:
_normalize_plugin_config_aliases(db_config)
_sync_alldebrid_api_key(db_config)
_CONFIG_CACHE = db_config
_LAST_SAVED_CONFIG = deepcopy(db_config)
@@ -742,6 +820,7 @@ def _release_save_lock(lock_dir: Path) -> None:
def save_config(config: Dict[str, Any]) -> int:
global _CONFIG_CACHE, _LAST_SAVED_CONFIG
_normalize_plugin_config_aliases(config)
_sync_alldebrid_api_key(config)
# Acquire cross-process save lock to avoid concurrent saves from different
+27 -15
View File
@@ -61,11 +61,15 @@ def get_store_schema(store_type: str) -> List[ConfigField]:
return _call_schema(cls, f"store '{store_type}'")
def get_provider_schema(provider_name: str) -> List[ConfigField]:
plugin_class = get_plugin_class(str(provider_name or "").strip())
def get_plugin_schema(plugin_name: str) -> List[ConfigField]:
plugin_class = get_plugin_class(str(plugin_name or "").strip())
if plugin_class is None:
return []
return _call_schema(plugin_class, f"provider '{provider_name}'")
return _call_schema(plugin_class, f"plugin '{plugin_name}'")
def get_provider_schema(provider_name: str) -> List[ConfigField]:
return get_plugin_schema(provider_name)
def get_tool_schema(tool_name: str) -> List[ConfigField]:
@@ -85,8 +89,8 @@ def get_item_schema(item_type: str, item_name: str) -> List[ConfigField]:
normalized_name = str(item_name or "").strip()
if normalized_type.startswith("store-"):
return get_store_schema(normalized_type.replace("store-", "", 1))
if normalized_type == "provider":
return get_provider_schema(normalized_name)
if normalized_type in {"provider", "plugin"}:
return get_plugin_schema(normalized_name)
if normalized_type == "tool":
return get_tool_schema(normalized_name)
return []
@@ -126,25 +130,29 @@ def build_default_store_config(store_type: str, instance_name: str) -> Dict[str,
return config
def build_default_provider_config(provider_name: str) -> Dict[str, Any]:
def build_default_plugin_config(plugin_name: str) -> Dict[str, Any]:
config: Dict[str, Any] = {}
schema = get_provider_schema(provider_name)
schema = get_plugin_schema(plugin_name)
if schema:
for field in schema:
config[field["key"]] = field.get("default", "")
return config
plugin_class = get_plugin_class(str(provider_name or "").strip())
plugin_class = get_plugin_class(str(plugin_name or "").strip())
if plugin_class is None:
return config
try:
for required_key in plugin_class.required_config_keys():
config[str(required_key)] = ""
except Exception:
logger.exception("Failed to load legacy required config keys for provider '%s'", provider_name)
logger.exception("Failed to load legacy required config keys for plugin '%s'", plugin_name)
return config
def build_default_provider_config(provider_name: str) -> Dict[str, Any]:
return build_default_plugin_config(provider_name)
def build_default_tool_config(tool_name: str) -> Dict[str, Any]:
config: Dict[str, Any] = {}
for field in get_tool_schema(tool_name):
@@ -179,14 +187,14 @@ def get_required_config_keys(item_type: str, item_name: str) -> List[str]:
if cls is not None:
for required_key in _required_keys_for(cls):
_add_key(required_key)
elif normalized_type == "provider":
elif normalized_type in {"provider", "plugin"}:
plugin_class = get_plugin_class(normalized_name)
if plugin_class is not None:
try:
for required_key in plugin_class.required_config_keys():
_add_key(required_key)
except Exception:
logger.exception("Failed to load required config keys for provider '%s'", normalized_name)
logger.exception("Failed to load required config keys for plugin '%s'", normalized_name)
return required_keys
@@ -199,14 +207,18 @@ def get_configurable_store_types() -> List[str]:
return sorted(set(options))
def get_configurable_provider_types() -> List[str]:
def get_configurable_plugin_types() -> List[str]:
options: List[str] = []
for provider_name in list_plugins().keys():
if get_provider_schema(provider_name):
options.append(str(provider_name))
for plugin_name in list_plugins().keys():
if get_plugin_schema(plugin_name):
options.append(str(plugin_name))
return sorted(set(options))
def get_configurable_provider_types() -> List[str]:
return get_configurable_plugin_types()
def get_configurable_tool_types() -> List[str]:
options: List[str] = []
try:
+4 -1
View File
@@ -616,7 +616,10 @@ class PipelineHubApp(App):
# Provide a visible startup summary of configured plugins/stores for debugging
try:
cfg = load_config() or {}
provs = list(cfg.get("provider", {}).keys()) if isinstance(cfg.get("provider"), dict) else []
plugin_block = cfg.get("plugin")
if not isinstance(plugin_block, dict):
plugin_block = cfg.get("provider")
provs = list(plugin_block.keys()) if isinstance(plugin_block, dict) else []
stores = list(cfg.get("store", {}).keys()) if isinstance(cfg.get("store"), dict) else []
prov_display = ", ".join(provs[:10]) + ("..." if len(provs) > 10 else "")
store_display = ", ".join(stores[:10]) + ("..." if len(stores) > 10 else "")
+1 -1
View File
@@ -14,7 +14,7 @@ import sys
from SYS.logger import log, debug
from Provider.metadata_provider import (
from plugins.metadata_provider import (
get_default_subject_scrape_provider,
get_metadata_provider,
get_metadata_provider_for_url,
+10 -4
View File
@@ -25,7 +25,7 @@ def add_startup_check(
row = table.add_row()
row.add_column("STATUS", upper_text(status))
row.add_column("NAME", upper_text(name))
row.add_column("PROVIDER", upper_text(provider or ""))
row.add_column("PLUGIN", upper_text(provider or ""))
row.add_column("STORE", upper_text(store or ""))
row.add_column("FILES", "" if files is None else str(files))
row.add_column("DETAIL", upper_text(detail or ""))
@@ -42,7 +42,9 @@ def has_store_subtype(cfg: dict, subtype: str) -> bool:
def has_provider(cfg: dict, name: str) -> bool:
provider_cfg = cfg.get("provider")
provider_cfg = cfg.get("plugin")
if not isinstance(provider_cfg, dict):
provider_cfg = cfg.get("provider")
if not isinstance(provider_cfg, dict):
return False
block = provider_cfg.get(str(name).strip().lower())
@@ -74,7 +76,7 @@ def ping_url(url: str, timeout: float = 3.0) -> tuple[bool, str]:
def provider_display_name(key: str) -> str:
label = (key or "").strip()
return label[:1].upper() + label[1:] if label else "Provider"
return label[:1].upper() + label[1:] if label else "Plugin"
def ping_first(urls: list[str]) -> tuple[bool, str]:
@@ -88,7 +90,11 @@ def ping_first(urls: list[str]) -> tuple[bool, str]:
def collect_plugin_startup_checks(config: dict) -> list[dict[str, Any]]:
provider_cfg = config.get("provider") if isinstance(config, dict) else None
provider_cfg = None
if isinstance(config, dict):
provider_cfg = config.get("plugin")
if not isinstance(provider_cfg, dict):
provider_cfg = config.get("provider")
if not isinstance(provider_cfg, dict) or not provider_cfg:
return []
+1 -1
View File
@@ -16,7 +16,7 @@ from cmdnat._status_shared import (
CMDLET = Cmdlet(
name=".status",
summary="Check and display service/provider status",
summary="Check and display service/plugin status",
usage=".status",
arg=[],
)
+1 -1
View File
@@ -5,5 +5,5 @@
- **docs:** Add `docs/provider_authoring.md` with a Quick Start, examples, and testing guidance for providers that integrate with the strict `ResultTable` API (ResultModel/ColumnSpec/selection_fn).
- **docs:** Add link to `docs/result_table.md` pointing to the provider authoring guide.
- **tests:** Add `tests/test_provider_author_examples.py` validating example provider registration and adapter behavior.
- **notes:** Existing example providers (`Provider/example_provider.py`, `Provider/vimm.py`) are referenced as canonical patterns.
- **notes:** Existing example plugins (`plugins/example_provider.py`, `plugins/vimm/__init__.py`) are referenced as canonical patterns.
+5 -5
View File
@@ -84,7 +84,7 @@ class MyTableProvider(TableProviderMixin, Provider):
return self.search_table_from_url(url, limit=limit)
```
`TableProviderMixin.search_table_from_url` returns `ProviderCore.base.SearchResult` entries. If you want to integrate this plugin with the strict `ResultTable` registry, add a small adapter that converts `SearchResult` -> `ResultModel` and register it using `register_plugin` (see `Provider/vimm.py` for a real example).
`TableProviderMixin.search_table_from_url` returns `ProviderCore.base.SearchResult` entries. If you want to integrate this plugin with the strict `ResultTable` registry, add a small adapter that converts `SearchResult` -> `ResultModel` and register it using `register_plugin` (see `plugins/vimm/__init__.py` for a real example).
---
@@ -115,7 +115,7 @@ class MyTableProvider(TableProviderMixin, Provider):
```py
from SYS.result_table_adapters import get_provider
from Provider import example_provider
from plugins import example_provider
def test_example_provider_registration():
@@ -132,10 +132,10 @@ def test_example_provider_registration():
## References & examples
- Read `Provider/example_provider.py` for a compact example of a strict adapter and dynamic columns.
- Read `Provider/vimm.py` for a table-provider that uses `TableProviderMixin` and converts `SearchResult``ResultModel` for registration.
- Read `plugins/example_provider.py` for a compact example of a strict adapter and dynamic columns.
- Read `plugins/vimm/__init__.py` for a table-provider that uses `TableProviderMixin` and converts `SearchResult``ResultModel` for registration.
- See `docs/provider_guide.md` for a broader provider development checklist.
---
If you want, I can also add a small `Provider/myprovider_template.py` file and unit tests for it — say the word and I'll add them and wire up tests. 🎯
If you want, I can also add a small `plugins/myprovider_template.py` file and unit tests for it.
+7 -6
View File
@@ -3,7 +3,7 @@
## 🎯 Purpose
This guide describes how to write, test, and register a plugin so the application can discover and use it as a pluggable component.
> Keep plugin code small, focused, and well-tested. Built-in plugins live in `Provider/` and external drop-in plugins live under `plugins/`.
> Keep plugin code small, focused, and well-tested. Bundled plugins and drop-in plugins share the same `plugins/` layout.
---
@@ -125,8 +125,9 @@ pytest -q
---
## 📦 Registration & packaging
- Built-in plugins live under `Provider/` and are auto-discovered from that package.
- Bundled plugins live under `plugins/` and are auto-discovered from that package.
- External user plugins can be dropped into `plugins/` or any directory listed in `MM_PLUGIN_PATH` / `MEDEIA_PLUGIN_PATH`.
- Package directories are preferred so plugin-specific files can travel with the plugin.
- Plugin authors should import from `ProviderCore.*`.
---
@@ -146,19 +147,19 @@ pytest -q
- [ ] Provide `URL` / `URL_DOMAINS` or `url_patterns()` for routing
- [ ] Add `download()` or `download_url()` for piped/passed URL downloads
- [ ] Add tests under `tests/`
- [ ] Add the plugin module to `Provider/` for built-ins, or drop it into `plugins/` for plug-and-play user installs
- [ ] Add the plugin under `plugins/<name>/` for bundled or plug-and-play installs
---
## 🔗 Further reading
- See existing built-in plugins in `Provider/` for patterns and edge cases.
- See existing bundled plugins in `plugins/` for patterns and edge cases.
- Check `API/` helpers for HTTP and debrid clients.
---
If you'd like, I can:
- Add an example plugin file under `Provider/` as a template (see `Provider/hello_provider.py`), and
- Add an example plugin file under `plugins/` as a template (see `plugins/hello/__init__.py`), and
- Create unit tests for it (see `tests/test_provider_hello.py`).
I have added a minimal example provider and tests in this repository; use them as a starting point for new providers.
I have added a minimal example plugin and tests in this repository; use them as a starting point for new plugins.
+1 -1
View File
@@ -13,7 +13,7 @@ Example:
What plugins must implement
- An adapter that yields `ResultModel` objects (breaking API).
- Optionally supply a `columns` factory and `selection_fn` (see `Provider/example_provider.py`).
- Optionally supply a `columns` factory and `selection_fn` (see `plugins/example_provider.py`).
Implementation notes
- `plugin-table` emits dicts like `{ 'title': ..., 'path': ..., 'metadata': ..., '_selection_args': [...] }`.
+19 -7
View File
@@ -1,14 +1,28 @@
# External Plugins
# Plugins
Drop user plugins in this folder to make them available to the app without editing the built-in `Provider/` package.
This folder is the primary home for bundled plugins and also the default search
path for drop-in plugins.
Supported discovery paths:
Preferred layout:
- Put each plugin in its own folder under `plugins/<name>/` with an `__init__.py`.
- Keep plugin-specific assets beside the code in that same folder.
- Single-file `.py` plugins are still supported, but package folders are the
recommended plug-and-play format.
That means a plugin can ship as a drag-and-drop folder with extras such as:
- `cookies.txt`
- templates or fixture files
- helper modules
- small static assets
Built-in bundled plugins use the same layout as external plugins. Additional
drop-in plugin search paths are:
- `plugins/` in the repo root
- `plugins/` in the current working directory
- Any directory listed in `MM_PLUGIN_PATH`
- Any directory listed in `MEDEIA_PLUGIN_PATH`
Plugin module rules:
Plugin rules:
- A plugin can be a single `.py` file or a package directory with `__init__.py`.
- Define a class that inherits from `ProviderCore.base.Provider`.
- Give it a stable name using `PLUGIN_NAME` or the class name.
@@ -34,6 +48,4 @@ class MyPlugin(Provider):
path=f"https://example.com/{text}",
)
]
```
Built-in plugins still live in `Provider/`.
```
+6
View File
@@ -0,0 +1,6 @@
"""Built-in plugin package.
This package is the primary home for bundled plugins. Each built-in plugin
should live in its own module or package under ``plugins/`` so it can also be
distributed as a drag-and-drop drop-in with any sibling assets it needs.
"""
+7
View File
@@ -0,0 +1,7 @@
"""Plugin-namespace import shim for the strict adapter example module.
This keeps example docs pointing at the plugin namespace while the original
implementation remains in ``Provider.example_provider`` for compatibility.
"""
from Provider.example_provider import * # noqa: F401,F403
+1 -1
View File
@@ -18,7 +18,7 @@ from API.Tidal import (
)
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
from SYS.field_access import get_field
from Provider.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.logger import debug, debug_panel, log
+2 -2
View File
@@ -694,7 +694,7 @@ class InternetArchive(Provider):
def _download_via_openlibrary(self, url: str, output_dir: Path) -> Optional[Dict[str, Any]]:
try:
from Provider.openlibrary import OpenLibrary
from plugins.openlibrary import OpenLibrary
except Exception as exc:
log(f"[internetarchive] OpenLibrary borrow helper unavailable: {exc}", file=sys.stderr)
return None
@@ -766,7 +766,7 @@ class InternetArchive(Provider):
return None
try:
from Provider.openlibrary import OpenLibrary
from plugins.openlibrary import OpenLibrary
except Exception as exc:
log(f"[internetarchive] OpenLibrary auth helper unavailable: {exc}", file=sys.stderr)
return None
+1 -1
View File
@@ -383,7 +383,7 @@ def _enrich_book_tags_from_isbn(isbn: str,
# 2) isbnsearch metadata provider fallback.
try:
from Provider.metadata_provider import get_metadata_provider
from plugins.metadata_provider import get_metadata_provider
provider = get_metadata_provider("isbnsearch",
config or {})
+7
View File
@@ -0,0 +1,7 @@
"""Plugin-namespace import shim for metadata helper utilities.
The implementation currently lives in ``Provider.metadata_provider`` while the
legacy namespace is phased out. New imports should prefer ``plugins``.
"""
from Provider.metadata_provider import * # noqa: F401,F403
+1 -1
View File
@@ -22,7 +22,7 @@ from ProviderCore.base import Provider, SearchResult
from SYS.utils import sanitize_filename
from SYS.cli_syntax import get_field, get_free_text, parse_query
from SYS.logger import log
from Provider.metadata_provider import (
from plugins.metadata_provider import (
archive_item_metadata_to_tags,
fetch_archive_item_metadata,
)
+1 -1
View File
@@ -18,7 +18,7 @@ from API.Tidal import (
)
from ProviderCore.base import Provider, SearchResult
from SYS.field_access import get_field
from Provider.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.logger import debug, log
+7
View File
@@ -0,0 +1,7 @@
"""Plugin-namespace import shim for Tidal/HIFI manifest helpers.
The implementation currently lives in ``Provider.tidal_manifest`` while the
legacy namespace is phased out. New imports should prefer ``plugins``.
"""
from Provider.tidal_manifest import * # noqa: F401,F403