update and cleanup repo
This commit is contained in:
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Callable, Tuple
|
||||
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 PluginCore.base import Provider, SearchResult
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
@@ -197,47 +197,14 @@ def refresh_alldebrid_hoster_cache(*, force: bool = False) -> None:
|
||||
|
||||
|
||||
def _get_debrid_api_key(config: Dict[str, Any]) -> Optional[str]:
|
||||
"""Read AllDebrid API key from config.
|
||||
|
||||
Preferred formats:
|
||||
- config.conf provider block:
|
||||
[provider=alldebrid]
|
||||
api_key=...
|
||||
-> config["provider"]["alldebrid"]["api_key"]
|
||||
|
||||
- plugin-style debrid block:
|
||||
config["plugin"]["debrid"]["all-debrid"]["api_key"]
|
||||
|
||||
Falls back to some legacy keys if present.
|
||||
"""
|
||||
# 1) provider block: [provider=alldebrid]
|
||||
provider = config.get("provider")
|
||||
if isinstance(provider, dict):
|
||||
entry = provider.get("alldebrid")
|
||||
if isinstance(entry, dict):
|
||||
for k in ("api_key", "apikey", "API_KEY", "APIKEY"):
|
||||
val = entry.get(k)
|
||||
if isinstance(val, str) and val.strip():
|
||||
return val.strip()
|
||||
if isinstance(entry, str) and entry.strip():
|
||||
return entry.strip()
|
||||
|
||||
# 2) plugin debrid block
|
||||
"""Read the canonical AllDebrid API key from config."""
|
||||
try:
|
||||
from SYS.config import get_debrid_api_key
|
||||
|
||||
key = get_debrid_api_key(config, service="All-debrid")
|
||||
return key.strip() if key else None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Legacy fallback (kept permissive so older configs still work)
|
||||
for legacy_key in ("alldebrid_api_key", "AllDebrid", "all_debrid_api_key"):
|
||||
val = config.get(legacy_key)
|
||||
if isinstance(val, str) and val.strip():
|
||||
return val.strip()
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _consume_bencoded_value(data: bytes, pos: int) -> int:
|
||||
@@ -399,8 +366,8 @@ def _build_queued_magnet_item(
|
||||
|
||||
metadata: Dict[str, Any] = {
|
||||
"magnet_id": magnet_id,
|
||||
"provider": "alldebrid",
|
||||
"provider_view": "files",
|
||||
"plugin": "alldebrid",
|
||||
"plugin_view": "files",
|
||||
"magnet_spec": magnet_spec,
|
||||
"source_url": magnet_spec,
|
||||
"status": status_label,
|
||||
@@ -412,7 +379,6 @@ def _build_queued_magnet_item(
|
||||
|
||||
return {
|
||||
"table": "alldebrid",
|
||||
"provider": "alldebrid",
|
||||
"plugin": "alldebrid",
|
||||
"path": f"{_ALD_MAGNET_PREFIX}{magnet_id}",
|
||||
"title": title,
|
||||
@@ -539,7 +505,7 @@ def download_magnet(
|
||||
output_dir = target_path
|
||||
|
||||
try:
|
||||
result_obj = _download_direct_file(
|
||||
result_obj = download_direct_file(
|
||||
file_url,
|
||||
output_dir,
|
||||
quiet=quiet_mode,
|
||||
@@ -800,8 +766,8 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
"title": f"magnet-{magnet_id}",
|
||||
"metadata": {
|
||||
"magnet_id": magnet_id,
|
||||
"provider": "alldebrid",
|
||||
"provider_view": "files",
|
||||
"plugin": "alldebrid",
|
||||
"plugin_view": "files",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -952,7 +918,7 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
pipe_progress = None
|
||||
|
||||
try:
|
||||
dl_res = _download_direct_file(
|
||||
dl_res = download_direct_file(
|
||||
unlocked_url,
|
||||
Path(output_dir),
|
||||
quiet=quiet,
|
||||
@@ -965,7 +931,7 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
downloaded_path = Path(str(downloaded_path))
|
||||
except DownloadError as exc:
|
||||
log(
|
||||
f"[alldebrid] _download_direct_file rejected URL ({exc}); no further fallback", file=sys.stderr
|
||||
f"[alldebrid] download_direct_file rejected URL ({exc}); no further fallback", file=sys.stderr
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -1360,7 +1326,7 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
suggested_name = rel_path_obj.name or file_name or f"file-{file_idx}"
|
||||
|
||||
try:
|
||||
result_obj = _download_direct_file(
|
||||
result_obj = download_direct_file(
|
||||
file_url,
|
||||
target_path,
|
||||
quiet=quiet_mode,
|
||||
@@ -1482,8 +1448,8 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
full_metadata={
|
||||
"magnet": magnet_status,
|
||||
"magnet_id": magnet_id,
|
||||
"provider": "alldebrid",
|
||||
"provider_view": "files",
|
||||
"plugin": "alldebrid",
|
||||
"plugin_view": "files",
|
||||
"magnet_name": magnet_name,
|
||||
},
|
||||
)
|
||||
@@ -1535,8 +1501,8 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
"magnet_name": magnet_name,
|
||||
"relpath": relpath,
|
||||
"file": file_node,
|
||||
"provider": "alldebrid",
|
||||
"provider_view": "files",
|
||||
"plugin": "alldebrid",
|
||||
"plugin_view": "files",
|
||||
# Selection metadata for table system
|
||||
"_selection_args": ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||
"_selection_action": ["download-file", "-plugin", "alldebrid", "-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||
@@ -1649,8 +1615,8 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
full_metadata={
|
||||
"magnet": magnet,
|
||||
"magnet_id": magnet_id,
|
||||
"provider": "alldebrid",
|
||||
"provider_view": "folders",
|
||||
"plugin": "alldebrid",
|
||||
"plugin_view": "folders",
|
||||
"magnet_name": magnet_name,
|
||||
# Selection metadata: allow @N expansion to drive downloads directly
|
||||
"_selection_args": ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
|
||||
@@ -1752,7 +1718,7 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
table = Table(f"AllDebrid Files: {title}")._perseverance(True)
|
||||
table.set_table("alldebrid")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "alldebrid", "view": "files", "magnet_id": magnet_id})
|
||||
table.set_table_metadata({"plugin": "alldebrid", "view": "files", "magnet_id": magnet_id})
|
||||
except Exception:
|
||||
pass
|
||||
table.set_source_command("download-file", ["-plugin", "alldebrid"])
|
||||
@@ -1844,7 +1810,7 @@ class AllDebrid(TablePluginMixin, Provider):
|
||||
"Magnet": magnet_name or None,
|
||||
"Magnet ID": magnet_id,
|
||||
"Relative Path": relpath or None,
|
||||
"View": str(meta.get("provider_view") or meta.get("view") or (table_metadata or {}).get("view") or "").strip() or None,
|
||||
"View": str(meta.get("plugin_view") or meta.get("view") or (table_metadata or {}).get("view") or "").strip() or None,
|
||||
"Direct Url": direct_url or None,
|
||||
"Selection Url": selection_url or None,
|
||||
},
|
||||
@@ -1942,7 +1908,7 @@ try:
|
||||
if table_name:
|
||||
metadata.setdefault("table", table_name)
|
||||
metadata.setdefault("source", table_name)
|
||||
metadata.setdefault("provider", table_name)
|
||||
metadata.setdefault("plugin", table_name)
|
||||
|
||||
ext = payload.get("ext")
|
||||
if not ext and isinstance(path_val, str):
|
||||
@@ -2003,8 +1969,8 @@ try:
|
||||
cols.append(metadata_column("ready", "Ready"))
|
||||
if _has_metadata(rows, "relpath"):
|
||||
cols.append(metadata_column("relpath", "File Path"))
|
||||
if _has_metadata(rows, "provider_view"):
|
||||
cols.append(metadata_column("provider_view", "View"))
|
||||
if _has_metadata(rows, "plugin_view"):
|
||||
cols.append(metadata_column("plugin_view", "View"))
|
||||
if _has_metadata(rows, "size"):
|
||||
cols.append(metadata_column("size", "Size"))
|
||||
return cols
|
||||
@@ -2016,7 +1982,7 @@ try:
|
||||
Selection precedence:
|
||||
1. Explicit _selection_action (full command args)
|
||||
2. Explicit _selection_args (URL-specific args)
|
||||
3. Magic routing based on provider_view (files vs folders)
|
||||
3. Magic routing based on plugin_view (files vs folders)
|
||||
4. Magnet ID routing for folder-type rows (via alldebrid:magnet:<id>)
|
||||
5. Direct URL for file rows
|
||||
|
||||
@@ -2035,7 +2001,7 @@ try:
|
||||
return [str(x) for x in args if x is not None]
|
||||
|
||||
# Magic routing by view type
|
||||
view = metadata.get("provider_view") or metadata.get("view") or ""
|
||||
view = metadata.get("plugin_view") or metadata.get("view") or ""
|
||||
if view == "files":
|
||||
# File rows: pass direct URL for immediate download
|
||||
if row.path:
|
||||
|
||||
@@ -1027,7 +1027,7 @@ def unlock_link_cmdlet(result: Any, args: Sequence[str], config: Dict[str, Any])
|
||||
unlock-link # Uses URL from pipeline result
|
||||
|
||||
Requires:
|
||||
- AllDebrid API key in config under Debrid.All-debrid
|
||||
- AllDebrid API key in config under plugin.alldebrid.api_key
|
||||
|
||||
Args:
|
||||
result: Pipeline result object
|
||||
@@ -1054,28 +1054,14 @@ def unlock_link_cmdlet(result: Any, args: Sequence[str], config: Dict[str, Any])
|
||||
return None
|
||||
|
||||
def _get_alldebrid_api_key_from_config(cfg: Dict[str, Any]) -> Optional[str]:
|
||||
# Current config format
|
||||
try:
|
||||
provider_cfg = cfg.get("provider") if isinstance(cfg, dict) else None
|
||||
ad_cfg = provider_cfg.get("alldebrid"
|
||||
) if isinstance(provider_cfg,
|
||||
dict) else None
|
||||
api_key = ad_cfg.get("api_key") if isinstance(ad_cfg, dict) else None
|
||||
if isinstance(api_key, str) and api_key.strip():
|
||||
return api_key.strip()
|
||||
except Exception:
|
||||
pass
|
||||
from SYS.config import get_debrid_api_key
|
||||
|
||||
# Legacy config format fallback (best-effort)
|
||||
try:
|
||||
debrid_cfg = cfg.get("Debrid") if isinstance(cfg, dict) else None
|
||||
api_key = None
|
||||
if isinstance(debrid_cfg, dict):
|
||||
api_key = debrid_cfg.get("All-debrid") or debrid_cfg.get("AllDebrid")
|
||||
api_key = get_debrid_api_key(cfg, service="All-debrid")
|
||||
if isinstance(api_key, str) and api_key.strip():
|
||||
return api_key.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.logger import log, debug, debug_panel
|
||||
|
||||
from tool.playwright import PlaywrightTool
|
||||
from plugins.playwright import PlaywrightTool
|
||||
|
||||
|
||||
class Bandcamp(Provider):
|
||||
|
||||
@@ -11,10 +11,10 @@ from SYS.logger import log
|
||||
def _pick_provider_config(config: Any) -> Dict[str, Any]:
|
||||
if not isinstance(config, dict):
|
||||
return {}
|
||||
provider = config.get("provider")
|
||||
if not isinstance(provider, dict):
|
||||
plugin_cfg = config.get("plugin")
|
||||
if not isinstance(plugin_cfg, dict):
|
||||
return {}
|
||||
entry = provider.get("file.io")
|
||||
entry = plugin_cfg.get("file.io")
|
||||
if isinstance(entry, dict):
|
||||
return entry
|
||||
return {}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""FlorenceVision support module under the plugin namespace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"FlorenceVisionTool",
|
||||
"FlorenceVisionDefaults",
|
||||
"config_schema",
|
||||
]
|
||||
|
||||
_MODULE_ATTRS = {
|
||||
"FlorenceVisionTool": ".runtime",
|
||||
"FlorenceVisionDefaults": ".runtime",
|
||||
"config_schema": ".runtime",
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
submod = _MODULE_ATTRS.get(name)
|
||||
if submod is None:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
from importlib import import_module
|
||||
|
||||
mod = import_module(submod, package=__name__)
|
||||
obj = getattr(mod, name)
|
||||
globals()[name] = obj
|
||||
return obj
|
||||
File diff suppressed because it is too large
Load Diff
@@ -359,7 +359,7 @@ class FTP(Provider):
|
||||
table.set_table("ftp")
|
||||
try:
|
||||
table.set_table_metadata({
|
||||
"provider": "ftp",
|
||||
"plugin": "ftp",
|
||||
"instance": instance_name or None,
|
||||
"host": settings.get("host"),
|
||||
"path": target_path,
|
||||
@@ -792,7 +792,7 @@ class FTP(Provider):
|
||||
parent = posixpath.dirname(ftp_path.rstrip("/")) or "/"
|
||||
instance_name = str(settings.get("instance") or "").strip()
|
||||
metadata = {
|
||||
"provider": "ftp",
|
||||
"plugin": "ftp",
|
||||
"instance": instance_name or None,
|
||||
"host": settings.get("host"),
|
||||
"ftp_path": ftp_path,
|
||||
|
||||
@@ -153,7 +153,7 @@ class HelloProvider(Provider):
|
||||
table = Table(f"Hello Details: {title}")._perseverance(True)
|
||||
table.set_table("hello")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "hello", "view": "details", "example_index": idx})
|
||||
table.set_table_metadata({"plugin": "hello", "view": "details", "example_index": idx})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from plugins.tidal.api import (
|
||||
Tidal as TidalApiClient,
|
||||
Tidal,
|
||||
build_track_tags,
|
||||
coerce_duration_seconds,
|
||||
extract_artists,
|
||||
@@ -97,7 +97,7 @@ class HIFI(Provider):
|
||||
self.api_timeout = float(self.config.get("timeout", 10.0))
|
||||
except Exception:
|
||||
self.api_timeout = 10.0
|
||||
self.api_clients = [TidalApiClient(base_url=url, timeout=self.api_timeout) for url in self.api_urls]
|
||||
self.api_clients = [Tidal(base_url=url, timeout=self.api_timeout) for url in self.api_urls]
|
||||
|
||||
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
|
||||
normalized, parsed = parse_inline_query_arguments(query)
|
||||
@@ -744,7 +744,7 @@ class HIFI(Provider):
|
||||
try:
|
||||
table.set_table_metadata(
|
||||
{
|
||||
"provider": "hifi",
|
||||
"plugin": "hifi",
|
||||
"view": "track",
|
||||
"album_id": album_id,
|
||||
"album_title": album_title,
|
||||
@@ -1376,7 +1376,7 @@ class HIFI(Provider):
|
||||
|
||||
return False, None
|
||||
|
||||
def _get_api_client_for_base(self, base_url: str) -> Optional[TidalApiClient]:
|
||||
def _get_api_client_for_base(self, base_url: str) -> Optional[Tidal]:
|
||||
base = base_url.rstrip("/")
|
||||
for client in self.api_clients:
|
||||
if getattr(client, "base_url", "").rstrip("/") == base:
|
||||
@@ -1935,7 +1935,7 @@ class HIFI(Provider):
|
||||
table = Table(f"HIFI Albums: {artist_name}")._perseverance(False)
|
||||
table.set_table("hifi.album")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "hifi", "view": "album", "artist_id": artist_id, "artist_name": artist_name})
|
||||
table.set_table_metadata({"plugin": "hifi", "view": "album", "artist_id": artist_id, "artist_name": artist_name})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1997,7 +1997,7 @@ class HIFI(Provider):
|
||||
try:
|
||||
table.set_table_metadata(
|
||||
{
|
||||
"provider": "hifi",
|
||||
"plugin": "hifi",
|
||||
"view": "track",
|
||||
"album_id": album_id,
|
||||
"album_title": album_title,
|
||||
@@ -2061,7 +2061,7 @@ class HIFI(Provider):
|
||||
table = Table("HIFI Track")._perseverance(True)
|
||||
table.set_table("hifi.track")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "hifi", "view": "track", "resolved_manifest": True})
|
||||
table.set_table_metadata({"plugin": "hifi", "view": "track", "resolved_manifest": True})
|
||||
except Exception:
|
||||
pass
|
||||
results_payload: List[Dict[str, Any]] = []
|
||||
|
||||
@@ -10,11 +10,11 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
from API.HTTP import _download_direct_file
|
||||
from API.HTTP import download_direct_file
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.utils import sanitize_filename, unique_path
|
||||
from SYS.logger import log
|
||||
from SYS.config import get_provider_block
|
||||
from SYS.config import get_plugin_block
|
||||
|
||||
# Helper for download-file: render selectable formats for a details URL.
|
||||
def maybe_show_formats_table(
|
||||
@@ -177,26 +177,20 @@ def _ia() -> Any:
|
||||
def _pick_provider_config(config: Any) -> Dict[str, Any]:
|
||||
if not isinstance(config, dict):
|
||||
return {}
|
||||
provider = config.get("provider")
|
||||
if not isinstance(provider, dict):
|
||||
return {}
|
||||
entry = provider.get("internetarchive")
|
||||
if isinstance(entry, dict):
|
||||
return entry
|
||||
return {}
|
||||
return get_plugin_block(config, "internetarchive")
|
||||
|
||||
|
||||
def _pick_archive_credentials(config: Any) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Resolve Archive.org credentials.
|
||||
|
||||
Preference order:
|
||||
1) provider.internetarchive (email/username + password)
|
||||
2) provider.openlibrary (email + password)
|
||||
1) plugin.internetarchive (email/username + password)
|
||||
2) plugin.openlibrary (email + password)
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
return None, None
|
||||
|
||||
ia_block = get_provider_block(config, "internetarchive")
|
||||
ia_block = get_plugin_block(config, "internetarchive")
|
||||
if isinstance(ia_block, dict):
|
||||
email = (
|
||||
ia_block.get("email")
|
||||
@@ -209,7 +203,7 @@ def _pick_archive_credentials(config: Any) -> tuple[Optional[str], Optional[str]
|
||||
if email_text and password_text:
|
||||
return email_text, password_text
|
||||
|
||||
ol_block = get_provider_block(config, "openlibrary")
|
||||
ol_block = get_plugin_block(config, "openlibrary")
|
||||
if isinstance(ol_block, dict):
|
||||
email = ol_block.get("email")
|
||||
password = ol_block.get("password")
|
||||
@@ -744,7 +738,7 @@ class InternetArchive(Provider):
|
||||
if tags:
|
||||
normalized["tags"] = tags
|
||||
normalized["media_kind"] = "book"
|
||||
normalized["provider_action"] = "borrow"
|
||||
normalized["plugin_action"] = "borrow"
|
||||
return normalized
|
||||
|
||||
def validate(self) -> bool:
|
||||
@@ -993,7 +987,7 @@ class InternetArchive(Provider):
|
||||
pipeline_progress = None
|
||||
|
||||
try:
|
||||
direct_result = _download_direct_file(
|
||||
direct_result = download_direct_file(
|
||||
raw_path,
|
||||
output_dir,
|
||||
quiet=quiet_mode,
|
||||
|
||||
@@ -279,7 +279,7 @@ class Local(Provider):
|
||||
return {
|
||||
"hash": hash_value or "unknown",
|
||||
"store": "local",
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"path": str(target_path),
|
||||
"tag": tags,
|
||||
"title": title or target_path.name,
|
||||
|
||||
+13
-17
@@ -327,10 +327,10 @@ class Matrix(TablePluginMixin, Provider):
|
||||
self._init_reason: Optional[str] = None
|
||||
|
||||
matrix_conf = (
|
||||
self.config.get("provider",
|
||||
{}).get("matrix",
|
||||
{}) if isinstance(self.config,
|
||||
dict) else {}
|
||||
self.config.get("plugin",
|
||||
{}).get("matrix",
|
||||
{}) if isinstance(self.config,
|
||||
dict) else {}
|
||||
)
|
||||
homeserver = matrix_conf.get("homeserver")
|
||||
access_token = matrix_conf.get("access_token")
|
||||
@@ -362,16 +362,16 @@ class Matrix(TablePluginMixin, Provider):
|
||||
return False
|
||||
if self._init_ok is False:
|
||||
return False
|
||||
matrix_conf = self.config.get("provider",
|
||||
{}).get("matrix",
|
||||
{})
|
||||
matrix_conf = self.config.get("plugin",
|
||||
{}).get("matrix",
|
||||
{})
|
||||
return bool(
|
||||
matrix_conf.get("homeserver")
|
||||
and matrix_conf.get("access_token")
|
||||
)
|
||||
|
||||
def status_summary(self) -> Dict[str, Any]:
|
||||
matrix_conf = self.config.get("provider", {}).get("matrix", {}) if isinstance(self.config, dict) else {}
|
||||
matrix_conf = self.config.get("plugin", {}).get("matrix", {}) if isinstance(self.config, dict) else {}
|
||||
homeserver = str(matrix_conf.get("homeserver") or "").strip()
|
||||
room_id = str(matrix_conf.get("room_id") or "").strip()
|
||||
detail = homeserver
|
||||
@@ -439,7 +439,7 @@ class Matrix(TablePluginMixin, Provider):
|
||||
full_metadata={
|
||||
"room_id": room_id,
|
||||
"room_name": room_name,
|
||||
"provider": "matrix",
|
||||
"plugin": "matrix",
|
||||
# Selection metadata for table system and @N expansion
|
||||
"_selection_args": ["-room-id", room_id],
|
||||
},
|
||||
@@ -450,9 +450,9 @@ class Matrix(TablePluginMixin, Provider):
|
||||
|
||||
|
||||
def _get_homeserver_and_token(self) -> Tuple[str, str]:
|
||||
matrix_conf = self.config.get("provider",
|
||||
{}).get("matrix",
|
||||
{})
|
||||
matrix_conf = self.config.get("plugin",
|
||||
{}).get("matrix",
|
||||
{})
|
||||
homeserver = matrix_conf.get("homeserver")
|
||||
access_token = matrix_conf.get("access_token")
|
||||
if not homeserver:
|
||||
@@ -681,7 +681,7 @@ class Matrix(TablePluginMixin, Provider):
|
||||
)
|
||||
|
||||
def upload(self, file_path: str, **kwargs: Any) -> str:
|
||||
matrix_conf = self.config.get("provider",
|
||||
matrix_conf = self.config.get("plugin",
|
||||
{}).get("matrix",
|
||||
{})
|
||||
room_id = matrix_conf.get("room_id")
|
||||
@@ -877,7 +877,3 @@ try:
|
||||
except Exception:
|
||||
# best-effort registration
|
||||
pass
|
||||
|
||||
|
||||
# Backward-compatible alias: tests and callers may import `plugins.matrix.cmdnat`.
|
||||
from . import commands as cmdnat # noqa: E402
|
||||
|
||||
+35
-42
@@ -58,8 +58,30 @@ def _extract_set_value_arg(args: Sequence[str]) -> Optional[str]:
|
||||
return extract_arg_value(args, flags={"-set-value"})
|
||||
|
||||
|
||||
def _get_matrix_config_block(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not isinstance(config, dict):
|
||||
return {}
|
||||
plugins = config.get("plugin")
|
||||
if not isinstance(plugins, dict):
|
||||
return {}
|
||||
matrix_cfg = plugins.get("matrix")
|
||||
return matrix_cfg if isinstance(matrix_cfg, dict) else {}
|
||||
|
||||
|
||||
def _ensure_matrix_config_block(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
plugins = config.setdefault("plugin", {})
|
||||
if not isinstance(plugins, dict):
|
||||
plugins = {}
|
||||
config["plugin"] = plugins
|
||||
matrix_cfg = plugins.setdefault("matrix", {})
|
||||
if not isinstance(matrix_cfg, dict):
|
||||
matrix_cfg = {}
|
||||
plugins["matrix"] = matrix_cfg
|
||||
return matrix_cfg
|
||||
|
||||
|
||||
def _update_matrix_config(config: Dict[str, Any], key: str, value: Any) -> bool:
|
||||
"""Update the Matrix provider block in the shared config.
|
||||
"""Update the Matrix plugin block in the shared config.
|
||||
|
||||
This method writes to the unified config store so changes persist between
|
||||
sessions.
|
||||
@@ -71,29 +93,13 @@ def _update_matrix_config(config: Dict[str, Any], key: str, value: Any) -> bool:
|
||||
value_str = str(value)
|
||||
|
||||
current_cfg = load_config() or {}
|
||||
providers = current_cfg.setdefault("provider", {})
|
||||
if not isinstance(providers, dict):
|
||||
providers = {}
|
||||
current_cfg["provider"] = providers
|
||||
|
||||
matrix_cfg = providers.setdefault("matrix", {})
|
||||
if not isinstance(matrix_cfg, dict):
|
||||
matrix_cfg = {}
|
||||
providers["matrix"] = matrix_cfg
|
||||
|
||||
matrix_cfg = _ensure_matrix_config_block(current_cfg)
|
||||
matrix_cfg[key] = value_str
|
||||
|
||||
save_config(current_cfg)
|
||||
|
||||
# Keep the supplied config dict in sync for the running CLI
|
||||
target_providers = config.setdefault("provider", {})
|
||||
if not isinstance(target_providers, dict):
|
||||
target_providers = {}
|
||||
config["provider"] = target_providers
|
||||
target_matrix = target_providers.setdefault("matrix", {})
|
||||
if not isinstance(target_matrix, dict):
|
||||
target_matrix = {}
|
||||
target_providers["matrix"] = target_matrix
|
||||
target_matrix = _ensure_matrix_config_block(config)
|
||||
target_matrix[key] = value_str
|
||||
return True
|
||||
except Exception as exc:
|
||||
@@ -103,13 +109,8 @@ def _update_matrix_config(config: Dict[str, Any], key: str, value: Any) -> bool:
|
||||
|
||||
def _parse_config_room_filter_ids(config: Dict[str, Any]) -> List[str]:
|
||||
try:
|
||||
if not isinstance(config, dict):
|
||||
return []
|
||||
providers = config.get("provider")
|
||||
if not isinstance(providers, dict):
|
||||
return []
|
||||
matrix_conf = providers.get("matrix")
|
||||
if not isinstance(matrix_conf, dict):
|
||||
matrix_conf = _get_matrix_config_block(config)
|
||||
if not matrix_conf:
|
||||
return []
|
||||
raw = None
|
||||
# Support a few common spellings; `room` is the documented key.
|
||||
@@ -138,16 +139,11 @@ def _parse_config_room_filter_ids(config: Dict[str, Any]) -> List[str]:
|
||||
def _get_matrix_size_limit_bytes(config: Dict[str, Any]) -> Optional[int]:
|
||||
"""Return max allowed per-file size in bytes for Matrix uploads.
|
||||
|
||||
Config: [provider=Matrix] size_limit=50 # MB
|
||||
Config: [plugin=matrix] size_limit=50 # MB
|
||||
"""
|
||||
try:
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
providers = config.get("provider")
|
||||
if not isinstance(providers, dict):
|
||||
return None
|
||||
matrix_conf = providers.get("matrix")
|
||||
if not isinstance(matrix_conf, dict):
|
||||
matrix_conf = _get_matrix_config_block(config)
|
||||
if not matrix_conf:
|
||||
return None
|
||||
|
||||
raw = None
|
||||
@@ -236,7 +232,7 @@ def _resolve_room_identifier(value: str, config: Dict[str, Any]) -> Optional[str
|
||||
conf_ids = _parse_config_room_filter_ids(config)
|
||||
if conf_ids:
|
||||
# Attempt to fetch names for the configured IDs
|
||||
block = config.get("provider", {}).get("matrix", {})
|
||||
block = _get_matrix_config_block(config)
|
||||
if block and block.get("homeserver") and block.get("access_token"):
|
||||
try:
|
||||
m = _get_matrix_provider(config)
|
||||
@@ -252,7 +248,7 @@ def _resolve_room_identifier(value: str, config: Dict[str, Any]) -> Optional[str
|
||||
pass
|
||||
|
||||
# Last resort: attempt to ask the server for matching rooms (if possible)
|
||||
block = config.get("provider", {}).get("matrix", {})
|
||||
block = _get_matrix_config_block(config)
|
||||
if block and block.get("homeserver") and block.get("access_token"):
|
||||
try:
|
||||
m = _get_matrix_provider(config)
|
||||
@@ -631,7 +627,7 @@ def _resolve_upload_path(item: Any, config: Dict[str, Any]) -> Optional[str]:
|
||||
url = _resolve_plugin_url(url, config)
|
||||
|
||||
try:
|
||||
from API.HTTP import _download_direct_file
|
||||
from API.HTTP import download_direct_file
|
||||
|
||||
base_tmp = None
|
||||
if isinstance(config, dict):
|
||||
@@ -642,7 +638,7 @@ def _resolve_upload_path(item: Any, config: Dict[str, Any]) -> Optional[str]:
|
||||
)
|
||||
output_dir = output_dir / "matrix"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = _download_direct_file(url, output_dir, quiet=True)
|
||||
result = download_direct_file(url, output_dir, quiet=True)
|
||||
if (result and hasattr(result,
|
||||
"path") and isinstance(result.path,
|
||||
Path) and result.path.exists()):
|
||||
@@ -691,10 +687,7 @@ def _show_settings_table(config: Dict[str, Any]) -> int:
|
||||
|
||||
matrix_conf = {}
|
||||
try:
|
||||
if isinstance(config, dict):
|
||||
providers = config.get("provider")
|
||||
if isinstance(providers, dict):
|
||||
matrix_conf = providers.get("matrix") or {}
|
||||
matrix_conf = _get_matrix_config_block(config)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+11
-11
@@ -252,7 +252,7 @@ class ITunesMetadataPlugin(MetadataPlugin):
|
||||
"album": r.get("collectionName"),
|
||||
"year": str(r.get("releaseDate",
|
||||
""))[:4],
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"raw": r,
|
||||
}
|
||||
items.append(item)
|
||||
@@ -338,7 +338,7 @@ class OpenLibraryMetadataPlugin(MetadataPlugin):
|
||||
"artist": ", ".join(authors) if authors else "",
|
||||
"album": publisher,
|
||||
"year": str(doc.get("first_publish_year") or ""),
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"authors": authors,
|
||||
"publisher": publisher,
|
||||
"identifiers": {
|
||||
@@ -460,7 +460,7 @@ class GoogleBooksMetadataPlugin(MetadataPlugin):
|
||||
"artist": ", ".join(authors) if authors else "",
|
||||
"album": publisher,
|
||||
"year": year,
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"authors": authors,
|
||||
"publisher": publisher,
|
||||
"identifiers": identifiers,
|
||||
@@ -643,7 +643,7 @@ class ISBNsearchMetadataPlugin(MetadataPlugin):
|
||||
"artist": ", ".join(authors) if authors else "",
|
||||
"album": publisher or "",
|
||||
"year": year or "",
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"authors": authors,
|
||||
"publisher": publisher or "",
|
||||
"language": language or "",
|
||||
@@ -787,7 +787,7 @@ class MusicBrainzMetadataPlugin(MetadataPlugin):
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"year": year,
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"mbid": mbid,
|
||||
"raw": rec,
|
||||
}
|
||||
@@ -871,7 +871,7 @@ class ImdbMetadataPlugin(MetadataPlugin):
|
||||
"artist": "",
|
||||
"album": "",
|
||||
"year": str(year or ""),
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"imdb_id": imdb_id,
|
||||
"raw": data,
|
||||
}
|
||||
@@ -908,7 +908,7 @@ class ImdbMetadataPlugin(MetadataPlugin):
|
||||
"artist": "",
|
||||
"album": kind,
|
||||
"year": year,
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"imdb_id": imdb_id,
|
||||
"kind": kind,
|
||||
"rating": rating,
|
||||
@@ -1032,7 +1032,7 @@ class YtdlpMetadataPlugin(MetadataPlugin):
|
||||
"artist": str(artist or ""),
|
||||
"album": str(album or ""),
|
||||
"year": str(year or ""),
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"url": url,
|
||||
"raw": info,
|
||||
}
|
||||
@@ -1214,7 +1214,7 @@ class YtdlpMetadataPlugin(MetadataPlugin):
|
||||
return None
|
||||
|
||||
try:
|
||||
from tool.ytdlp import is_url_supported_by_ytdlp
|
||||
from plugins.ytdlp.tooling import is_url_supported_by_ytdlp
|
||||
|
||||
for text in candidates:
|
||||
try:
|
||||
@@ -1322,7 +1322,7 @@ class YtdlpMetadataPlugin(MetadataPlugin):
|
||||
"artist": str(info.get("artist") or info.get("uploader") or info.get("channel") or ""),
|
||||
"album": str(info.get("album") or info.get("playlist_title") or ""),
|
||||
"year": str((str(info.get("release_date") or "") or str(info.get("upload_date") or ""))[:4]),
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"url": str(url or "").strip(),
|
||||
"raw": info,
|
||||
}
|
||||
@@ -1927,7 +1927,7 @@ class TidalMetadataPlugin(MetadataPlugin):
|
||||
"year": year,
|
||||
"lyrics": lyrics,
|
||||
"tags": tags,
|
||||
"provider": self.name,
|
||||
"plugin": self.name,
|
||||
"path": getattr(result, "path", ""),
|
||||
"track_id": track_id,
|
||||
"full_metadata": metadata,
|
||||
|
||||
@@ -2878,7 +2878,7 @@ local function _start_screenshot_store_save(store, out_path, tags)
|
||||
screenshot_url = ''
|
||||
end
|
||||
local cmd = 'file -add -plugin hydrusnetwork -instance ' .. quote_pipeline_arg(store)
|
||||
.. ' -path ' .. quote_pipeline_arg(out_path)
|
||||
.. ' ' .. quote_pipeline_arg(out_path)
|
||||
if screenshot_url ~= '' then
|
||||
cmd = cmd .. ' -url ' .. quote_pipeline_arg(screenshot_url)
|
||||
end
|
||||
@@ -6367,7 +6367,7 @@ local function _start_trim_with_range(range)
|
||||
'tag -get -emit -store ' .. quote_pipeline_arg(store_hash.store) ..
|
||||
' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) ..
|
||||
' | file -add -plugin hydrusnetwork -instance ' .. quote_pipeline_arg(selected_store) ..
|
||||
' -path ' .. quote_pipeline_arg(output_path) ..
|
||||
' ' .. quote_pipeline_arg(output_path) ..
|
||||
' | add-relationship -store "' .. selected_store .. '"' ..
|
||||
' -to-hash ' .. quote_pipeline_arg(store_hash.hash)
|
||||
else
|
||||
@@ -6375,7 +6375,7 @@ local function _start_trim_with_range(range)
|
||||
'tag -get -emit -store ' .. quote_pipeline_arg(store_hash.store) ..
|
||||
' -query ' .. quote_pipeline_arg('hash:' .. store_hash.hash) ..
|
||||
' | file -add -plugin hydrusnetwork -instance ' .. quote_pipeline_arg(store_hash.store) ..
|
||||
' -path ' .. quote_pipeline_arg(output_path) ..
|
||||
' ' .. quote_pipeline_arg(output_path) ..
|
||||
' | add-relationship -store "' .. store_hash.store .. '"' ..
|
||||
' -to-hash ' .. quote_pipeline_arg(store_hash.hash)
|
||||
end
|
||||
@@ -6386,7 +6386,7 @@ local function _start_trim_with_range(range)
|
||||
_lua_log('trim: building file -add command to selected_store=' .. selected_store)
|
||||
-- Don't add title if empty - the file path will be used as title by default
|
||||
pipeline_cmd = 'file -add -plugin hydrusnetwork -instance ' .. quote_pipeline_arg(selected_store) ..
|
||||
' -path ' .. quote_pipeline_arg(output_path)
|
||||
' ' .. quote_pipeline_arg(output_path)
|
||||
_lua_log('trim: pipeline_cmd=' .. pipeline_cmd)
|
||||
else
|
||||
mp.osd_message('Trim complete: ' .. output_path, 5)
|
||||
|
||||
@@ -71,7 +71,7 @@ from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402
|
||||
from SYS.repl_queue import enqueue_repl_command, repl_state_is_alive # noqa: E402
|
||||
from SYS.utils import format_bytes # 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 plugins.ytdlp.tooling import get_display_format_id, get_selection_format_id # noqa: E402
|
||||
|
||||
REQUEST_PROP = "user-data/medeia-pipeline-request"
|
||||
RESPONSE_PROP = "user-data/medeia-pipeline-response"
|
||||
|
||||
@@ -674,7 +674,7 @@ class OpenLibrary(Provider):
|
||||
if not isinstance(config, dict):
|
||||
return _DEFAULT_PREFERRED_LANGUAGE
|
||||
|
||||
entry = config.get("provider", {}).get("openlibrary", {})
|
||||
entry = config.get("plugin", {}).get("openlibrary", {})
|
||||
if not isinstance(entry, dict):
|
||||
return _DEFAULT_PREFERRED_LANGUAGE
|
||||
|
||||
@@ -1118,7 +1118,7 @@ class OpenLibrary(Provider):
|
||||
table = Table(f"OpenLibrary Editions: {title}")._perseverance(True)
|
||||
table.set_table("openlibrary.edition")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "openlibrary", "view": "borrowable_editions"})
|
||||
table.set_table_metadata({"plugin": "openlibrary", "view": "borrowable_editions"})
|
||||
except Exception:
|
||||
pass
|
||||
table.set_source_command("search-file", ["-plugin", "openlibrary"])
|
||||
@@ -1274,7 +1274,7 @@ class OpenLibrary(Provider):
|
||||
if not isinstance(config, dict):
|
||||
return None, None
|
||||
|
||||
entry = config.get("provider", {}).get("openlibrary", {})
|
||||
entry = config.get("plugin", {}).get("openlibrary", {})
|
||||
if isinstance(entry, dict):
|
||||
email = entry.get("email")
|
||||
password = entry.get("password")
|
||||
@@ -1287,11 +1287,11 @@ class OpenLibrary(Provider):
|
||||
|
||||
@classmethod
|
||||
def _archive_scale_from_config(cls, config: Dict[str, Any]) -> int:
|
||||
"""Resolve Archive.org book-reader scale from provider config."""
|
||||
"""Resolve Archive.org book-reader scale from plugin config."""
|
||||
if not isinstance(config, dict):
|
||||
return _DEFAULT_ARCHIVE_SCALE
|
||||
|
||||
entry = config.get("provider", {}).get("openlibrary", {})
|
||||
entry = config.get("plugin", {}).get("openlibrary", {})
|
||||
if not isinstance(entry, dict):
|
||||
return _DEFAULT_ARCHIVE_SCALE
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Playwright support module under the plugin namespace.
|
||||
|
||||
This package provides shared browser automation defaults/helpers for cmdlets and
|
||||
plugins. It is intentionally lightweight at import time so plugin discovery can
|
||||
import `plugins.playwright` even when Playwright itself is not installed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"PlaywrightTimeoutError",
|
||||
"PlaywrightTool",
|
||||
"PlaywrightDefaults",
|
||||
"PlaywrightDownloadResult",
|
||||
"config_schema",
|
||||
]
|
||||
|
||||
_MODULE_ATTRS = {
|
||||
"PlaywrightTimeoutError": ".runtime",
|
||||
"PlaywrightTool": ".runtime",
|
||||
"PlaywrightDefaults": ".runtime",
|
||||
"PlaywrightDownloadResult": ".runtime",
|
||||
"config_schema": ".runtime",
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
submod = _MODULE_ATTRS.get(name)
|
||||
if submod is None:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
from importlib import import_module
|
||||
|
||||
mod = import_module(submod, package=__name__)
|
||||
obj = getattr(mod, name)
|
||||
globals()[name] = obj
|
||||
return obj
|
||||
@@ -0,0 +1,657 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, Optional, Union
|
||||
|
||||
from SYS.config import get_nested_config_value as _get_nested
|
||||
from SYS.logger import debug
|
||||
|
||||
try:
|
||||
from playwright.sync_api import TimeoutError as _SyncPlaywrightTimeoutError
|
||||
from playwright.sync_api import sync_playwright
|
||||
except Exception: # pragma: no cover - handled at runtime
|
||||
sync_playwright = None
|
||||
|
||||
class PlaywrightTimeoutError(RuntimeError):
|
||||
"""Fallback timeout type when Playwright is unavailable."""
|
||||
|
||||
else:
|
||||
PlaywrightTimeoutError = _SyncPlaywrightTimeoutError
|
||||
|
||||
# Re-export for consumers (e.g. cmdlets catching navigation timeouts)
|
||||
__all__ = [
|
||||
"PlaywrightTimeoutError",
|
||||
"PlaywrightTool",
|
||||
"PlaywrightDefaults",
|
||||
"PlaywrightDownloadResult",
|
||||
]
|
||||
|
||||
|
||||
def _resolve_out_dir(arg_outdir: Optional[Union[str, Path]]) -> Path:
|
||||
"""Resolve an output directory using config when possible."""
|
||||
if arg_outdir:
|
||||
p = Path(arg_outdir)
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
try:
|
||||
from SYS.config import load_config, resolve_output_dir
|
||||
|
||||
cfg = load_config()
|
||||
p = resolve_output_dir(cfg)
|
||||
try:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to create resolved output dir %s", p)
|
||||
return p
|
||||
except Exception:
|
||||
return Path(tempfile.mkdtemp(prefix="pwdl_"))
|
||||
|
||||
|
||||
def _find_filename_from_cd(cd: str) -> Optional[str]:
|
||||
if not cd:
|
||||
return None
|
||||
m = re.search(r"filename\*?=(?:UTF-8''\s*)?\"?([^\";]+)\"?", cd)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PlaywrightDefaults:
|
||||
browser: str = "chromium" # chromium|firefox|webkit
|
||||
headless: bool = True
|
||||
user_agent: str = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/120.0.0.0 Safari/537.36"
|
||||
)
|
||||
viewport_width: int = 1920
|
||||
viewport_height: int = 1080
|
||||
navigation_timeout_ms: int = 90_000
|
||||
ignore_https_errors: bool = True
|
||||
screenshot_quality: int = 8
|
||||
ffmpeg_path: Optional[str] = None # Path to ffmpeg executable; auto-detected if None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PlaywrightDownloadResult:
|
||||
ok: bool
|
||||
path: Optional[Path] = None
|
||||
url: Optional[str] = None
|
||||
mode: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"ok": bool(self.ok),
|
||||
"path": str(self.path) if self.path else None,
|
||||
"url": self.url,
|
||||
"mode": self.mode,
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
|
||||
class PlaywrightTool:
|
||||
"""Small wrapper to standardize Playwright defaults and lifecycle.
|
||||
|
||||
This is meant to keep cmdlets/providers from duplicating:
|
||||
- sync_playwright start/stop
|
||||
- browser launch/context creation
|
||||
- user-agent/viewport defaults
|
||||
- ffmpeg path resolution (for video recording)
|
||||
|
||||
Config overrides (plugin.playwright keys):
|
||||
- plugin.playwright.browser="chromium"
|
||||
- plugin.playwright.headless=true
|
||||
- plugin.playwright.user_agent="..."
|
||||
- plugin.playwright.viewport_width=1280
|
||||
- plugin.playwright.viewport_height=1200
|
||||
- plugin.playwright.navigation_timeout_ms=90000
|
||||
- plugin.playwright.ignore_https_errors=true
|
||||
- plugin.playwright.screenshot_quality=8
|
||||
- plugin.playwright.ffmpeg_path="/path/to/ffmpeg" (auto-detected if not set)
|
||||
|
||||
FFmpeg resolution (in order):
|
||||
1. Config key: playwright.ffmpeg_path
|
||||
2. Environment variable: FFMPEG_PATH (global shared env)
|
||||
3. Environment variable: PLAYWRIGHT_FFMPEG_PATH (legacy)
|
||||
4. Project bundled: MPV/ffmpeg/bin/ffmpeg[.exe]
|
||||
5. System PATH: which ffmpeg
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||
self._config: Dict[str,
|
||||
Any] = dict(config or {})
|
||||
self.defaults = self._load_defaults()
|
||||
|
||||
def _load_defaults(self) -> PlaywrightDefaults:
|
||||
cfg = self._config
|
||||
defaults = PlaywrightDefaults()
|
||||
pw_block = _get_nested(cfg, "plugin", "playwright")
|
||||
if not isinstance(pw_block, dict):
|
||||
pw_block = {}
|
||||
|
||||
def _get(name: str, fallback: Any) -> Any:
|
||||
val = pw_block.get(name)
|
||||
return fallback if val is None else val
|
||||
|
||||
browser = str(_get("browser", defaults.browser)).strip().lower() or "chromium"
|
||||
if browser not in {"chromium",
|
||||
"firefox",
|
||||
"webkit"}:
|
||||
browser = "chromium"
|
||||
|
||||
headless_raw = _get("headless", defaults.headless)
|
||||
headless = bool(headless_raw)
|
||||
|
||||
# Resolve user_agent configuration. Support a 'custom' token which reads
|
||||
# the actual UA from `user_agent_custom` so the UI can offer a simple
|
||||
# dropdown without scattering long UA strings to users by default.
|
||||
ua_raw = _get("user_agent", None)
|
||||
if ua_raw is None:
|
||||
ua = defaults.user_agent
|
||||
else:
|
||||
ua_s = str(ua_raw).strip()
|
||||
if ua_s.lower() == "custom":
|
||||
ua_custom = _get("user_agent_custom", "")
|
||||
ua = str(ua_custom).strip() or defaults.user_agent
|
||||
else:
|
||||
ua = ua_s
|
||||
|
||||
def _int(name: str, fallback: int) -> int:
|
||||
raw = _get(name, fallback)
|
||||
try:
|
||||
return int(raw)
|
||||
except Exception:
|
||||
return fallback
|
||||
|
||||
vw = _int("viewport_width", defaults.viewport_width)
|
||||
vh = _int("viewport_height", defaults.viewport_height)
|
||||
nav_timeout = _int("navigation_timeout_ms", defaults.navigation_timeout_ms)
|
||||
screenshot_quality = max(1, min(10, _int("screenshot_quality", defaults.screenshot_quality)))
|
||||
|
||||
ignore_https = bool(_get("ignore_https_errors", defaults.ignore_https_errors))
|
||||
|
||||
# Try to find ffmpeg: config override, global env FFMPEG_PATH, legacy
|
||||
# PLAYWRIGHT_FFMPEG_PATH, bundled, then system. This allows Playwright to
|
||||
# use the shared ffmpeg path used by other tools (FFMPEG_PATH env).
|
||||
ffmpeg_path: Optional[str] = None
|
||||
config_ffmpeg = _get("ffmpeg_path", None)
|
||||
|
||||
if config_ffmpeg:
|
||||
# User explicitly configured ffmpeg path
|
||||
candidate = str(config_ffmpeg).strip()
|
||||
if candidate and Path(candidate).exists():
|
||||
ffmpeg_path = candidate
|
||||
else:
|
||||
ffmpeg_path = None
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Prefer a global FFMPEG_PATH env var (shared by tools) before Playwright-specific one
|
||||
env_ffmpeg = os.environ.get("FFMPEG_PATH")
|
||||
if env_ffmpeg and Path(env_ffmpeg).exists():
|
||||
ffmpeg_path = env_ffmpeg
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Backward-compatible Playwright-specific env var
|
||||
env_ffmpeg2 = os.environ.get("PLAYWRIGHT_FFMPEG_PATH")
|
||||
if env_ffmpeg2 and Path(env_ffmpeg2).exists():
|
||||
ffmpeg_path = env_ffmpeg2
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Try to find bundled ffmpeg in the project (Windows-only, in MPV/ffmpeg/bin)
|
||||
try:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
bundled_ffmpeg = repo_root / "MPV" / "ffmpeg" / "bin"
|
||||
if bundled_ffmpeg.exists():
|
||||
ffmpeg_exe = bundled_ffmpeg / ("ffmpeg.exe" if os.name == "nt" else "ffmpeg")
|
||||
if ffmpeg_exe.exists():
|
||||
ffmpeg_path = str(ffmpeg_exe)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Try system ffmpeg if bundled not found
|
||||
system_ffmpeg = shutil.which("ffmpeg")
|
||||
if system_ffmpeg:
|
||||
ffmpeg_path = system_ffmpeg
|
||||
|
||||
return PlaywrightDefaults(
|
||||
browser=browser,
|
||||
headless=headless,
|
||||
user_agent=ua,
|
||||
viewport_width=vw,
|
||||
viewport_height=vh,
|
||||
navigation_timeout_ms=nav_timeout,
|
||||
ignore_https_errors=ignore_https,
|
||||
screenshot_quality=screenshot_quality,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
|
||||
|
||||
def require(self) -> None:
|
||||
"""Ensure Playwright is present; raise a helpful RuntimeError if not."""
|
||||
try:
|
||||
assert sync_playwright is not None
|
||||
except Exception:
|
||||
raise RuntimeError(
|
||||
"playwright is required; install with: pip install playwright; then: playwright install"
|
||||
)
|
||||
|
||||
def ffmpeg_available(self) -> bool:
|
||||
"""Check if ffmpeg is available on the system."""
|
||||
return bool(self.defaults.ffmpeg_path)
|
||||
|
||||
def require_ffmpeg(self) -> None:
|
||||
"""Require ffmpeg to be available; raise a helpful error if not.
|
||||
|
||||
This should be called before operations that need ffmpeg (e.g., video recording).
|
||||
"""
|
||||
if not self.ffmpeg_available():
|
||||
raise RuntimeError(
|
||||
"ffmpeg is required but not found on your system.\n"
|
||||
"Install it using:\n"
|
||||
" Windows: choco install ffmpeg (if using Chocolatey) or use the bundled version in MPV/ffmpeg\n"
|
||||
" macOS: brew install ffmpeg\n"
|
||||
" Linux: apt install ffmpeg (Ubuntu/Debian) or equivalent for your distribution\n"
|
||||
"\n"
|
||||
"Or set the PLAYWRIGHT_FFMPEG_PATH environment variable to point to your ffmpeg executable."
|
||||
)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open_page(
|
||||
self,
|
||||
*,
|
||||
headless: Optional[bool] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
viewport_width: Optional[int] = None,
|
||||
viewport_height: Optional[int] = None,
|
||||
ignore_https_errors: Optional[bool] = None,
|
||||
accept_downloads: bool = False,
|
||||
emulate_viewport: bool = True,
|
||||
start_maximized: bool = False,
|
||||
) -> Iterator[Any]:
|
||||
"""Context manager yielding a Playwright page with sane defaults."""
|
||||
self.require()
|
||||
|
||||
h = self.defaults.headless if headless is None else bool(headless)
|
||||
ua = self.defaults.user_agent if user_agent is None else str(user_agent)
|
||||
vw = self.defaults.viewport_width if viewport_width is None else int(
|
||||
viewport_width
|
||||
)
|
||||
vh = self.defaults.viewport_height if viewport_height is None else int(
|
||||
viewport_height
|
||||
)
|
||||
ihe = (
|
||||
self.defaults.ignore_https_errors
|
||||
if ignore_https_errors is None else bool(ignore_https_errors)
|
||||
)
|
||||
|
||||
# Support Playwright-native headers/user-agent.
|
||||
# If user_agent is unset/empty or explicitly set to one of these tokens,
|
||||
# we omit the user_agent override so Playwright uses its bundled Chromium UA.
|
||||
ua_value: Optional[str]
|
||||
ua_text = str(ua or "").strip()
|
||||
if not ua_text or ua_text.lower() in {"native",
|
||||
"playwright",
|
||||
"default"}:
|
||||
ua_value = None
|
||||
else:
|
||||
ua_value = ua_text
|
||||
|
||||
pw = None
|
||||
browser = None
|
||||
context = None
|
||||
try:
|
||||
assert sync_playwright is not None
|
||||
pw = sync_playwright().start()
|
||||
|
||||
browser_type = getattr(pw, self.defaults.browser, None)
|
||||
if browser_type is None:
|
||||
browser_type = pw.chromium
|
||||
|
||||
launch_args = ["--disable-blink-features=AutomationControlled"]
|
||||
if bool(start_maximized) and not h:
|
||||
launch_args.append("--start-maximized")
|
||||
|
||||
browser = browser_type.launch(
|
||||
headless=h,
|
||||
args=launch_args,
|
||||
)
|
||||
context_kwargs: Dict[str,
|
||||
Any] = {
|
||||
"ignore_https_errors": ihe,
|
||||
"accept_downloads": bool(accept_downloads),
|
||||
}
|
||||
if bool(emulate_viewport):
|
||||
context_kwargs["viewport"] = {
|
||||
"width": vw,
|
||||
"height": vh,
|
||||
}
|
||||
else:
|
||||
context_kwargs["no_viewport"] = True
|
||||
if ua_value is not None:
|
||||
context_kwargs["user_agent"] = ua_value
|
||||
|
||||
context = browser.new_context(**context_kwargs)
|
||||
page = context.new_page()
|
||||
yield page
|
||||
finally:
|
||||
try:
|
||||
if context is not None:
|
||||
context.close()
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to close Playwright context")
|
||||
try:
|
||||
if browser is not None:
|
||||
browser.close()
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to close Playwright browser")
|
||||
try:
|
||||
if pw is not None:
|
||||
pw.stop()
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to stop Playwright engine")
|
||||
|
||||
def goto(self, page: Any, url: str) -> None:
|
||||
"""Navigate with configured timeout."""
|
||||
try:
|
||||
page.goto(
|
||||
url,
|
||||
timeout=int(self.defaults.navigation_timeout_ms),
|
||||
wait_until="domcontentloaded"
|
||||
)
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
def download_file(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
selector: str = "form#dl_form button[type=submit]",
|
||||
out_dir: Optional[Union[str, Path]] = None,
|
||||
timeout_sec: int = 60,
|
||||
headless_first: bool = False,
|
||||
debug_mode: bool = False,
|
||||
) -> PlaywrightDownloadResult:
|
||||
"""Download a file by clicking a selector and capturing the response.
|
||||
|
||||
The helper mirrors the standalone `scripts/playwright_fetch.py` logic
|
||||
and tries multiple click strategies (expect_download, tooltip continue,
|
||||
submitDL, JS/mouse click) to coax stubborn sites.
|
||||
"""
|
||||
try:
|
||||
self.require()
|
||||
except Exception as exc:
|
||||
return PlaywrightDownloadResult(ok=False, error=str(exc))
|
||||
|
||||
out_path_base = _resolve_out_dir(out_dir)
|
||||
timeout_ms = max(10_000, int(timeout_sec) * 1000 if timeout_sec is not None else int(self.defaults.navigation_timeout_ms))
|
||||
nav_timeout_ms = max(timeout_ms, int(self.defaults.navigation_timeout_ms))
|
||||
selector_timeout_ms = 10_000
|
||||
|
||||
# Preserve legacy behaviour: headless_first=False tries headful then headless; True reverses the order.
|
||||
order = [True, False] if headless_first else [False, True]
|
||||
seen = set()
|
||||
modes = []
|
||||
for m in order:
|
||||
if m in seen:
|
||||
continue
|
||||
seen.add(m)
|
||||
modes.append(m)
|
||||
|
||||
last_error: Optional[str] = None
|
||||
|
||||
for mode in modes:
|
||||
try:
|
||||
if debug_mode:
|
||||
debug(f"[playwright] download url={url} selector={selector} headless={mode} out_dir={out_path_base}")
|
||||
|
||||
with self.open_page(headless=mode, accept_downloads=True) as page:
|
||||
page.goto(url, wait_until="networkidle", timeout=nav_timeout_ms)
|
||||
page.wait_for_selector(selector, timeout=selector_timeout_ms)
|
||||
self._wait_for_block_clear(page, timeout_ms=6000)
|
||||
|
||||
el = page.query_selector(selector)
|
||||
|
||||
# 1) Direct click with expect_download
|
||||
try:
|
||||
with page.expect_download(timeout=timeout_ms) as dl_info:
|
||||
if el:
|
||||
el.click()
|
||||
else:
|
||||
page.click(selector)
|
||||
dl = dl_info.value
|
||||
filename = dl.suggested_filename or Path(dl.url).name or "download"
|
||||
out_path = out_path_base / filename
|
||||
dl.save_as(str(out_path))
|
||||
return PlaywrightDownloadResult(ok=True, path=out_path, url=dl.url, mode="download")
|
||||
except PlaywrightTimeoutError:
|
||||
last_error = "download timeout"
|
||||
except Exception as click_exc:
|
||||
last_error = str(click_exc) or last_error
|
||||
|
||||
# 2) Tooltip continue flow
|
||||
try:
|
||||
btn = page.query_selector("#tooltip4 input[type=button]")
|
||||
if btn:
|
||||
btn.click()
|
||||
with page.expect_download(timeout=timeout_ms) as dl_info:
|
||||
if el:
|
||||
el.click()
|
||||
else:
|
||||
page.click(selector)
|
||||
dl = dl_info.value
|
||||
filename = dl.suggested_filename or Path(dl.url).name or "download"
|
||||
out_path = out_path_base / filename
|
||||
dl.save_as(str(out_path))
|
||||
return PlaywrightDownloadResult(ok=True, path=out_path, url=dl.url, mode="tooltip-download")
|
||||
except Exception as tooltip_exc:
|
||||
last_error = str(tooltip_exc) or last_error
|
||||
|
||||
# 3) Submit handler that respects tooltip flow
|
||||
try:
|
||||
page.evaluate("() => { try { submitDL(document.forms['dl_form'], 'tooltip4'); } catch (e) {} }")
|
||||
resp = page.wait_for_response(
|
||||
lambda r: r.status == 200 and any(k.lower() == 'content-disposition' for k in r.headers.keys()),
|
||||
timeout=timeout_ms,
|
||||
)
|
||||
if resp:
|
||||
out_path = self._save_response(resp, out_path_base)
|
||||
if out_path:
|
||||
return PlaywrightDownloadResult(ok=True, path=out_path, url=getattr(resp, "url", None), mode="response")
|
||||
except Exception as resp_exc:
|
||||
last_error = str(resp_exc) or last_error
|
||||
|
||||
# 4) JS/mouse click and capture response
|
||||
try:
|
||||
if el:
|
||||
try:
|
||||
page.evaluate("el => el.click()", el)
|
||||
except Exception:
|
||||
page.evaluate(f"() => document.querySelector('{selector}').click()")
|
||||
else:
|
||||
page.evaluate(f"() => document.querySelector('{selector}').click()")
|
||||
|
||||
if el:
|
||||
try:
|
||||
box = el.bounding_box()
|
||||
if box:
|
||||
page.mouse.move(box['x'] + box['width'] / 2, box['y'] + box['height'] / 2)
|
||||
page.mouse.click(box['x'] + box['width'] / 2, box['y'] + box['height'] / 2)
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to perform mouse click for selector '%s'", selector)
|
||||
|
||||
resp = page.wait_for_response(
|
||||
lambda r: r.status == 200 and any(k.lower() == 'content-disposition' for k in r.headers.keys()),
|
||||
timeout=timeout_ms,
|
||||
)
|
||||
if resp:
|
||||
out_path = self._save_response(resp, out_path_base)
|
||||
if out_path:
|
||||
return PlaywrightDownloadResult(ok=True, path=out_path, url=getattr(resp, "url", None), mode="response-fallback")
|
||||
except Exception as final_exc:
|
||||
last_error = str(final_exc) or last_error
|
||||
|
||||
except Exception as exc:
|
||||
last_error = str(exc)
|
||||
if debug_mode:
|
||||
try:
|
||||
debug(f"[playwright] attempt failed (headless={mode}): {traceback.format_exc()}")
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to emit debug info for Playwright attempt failure")
|
||||
continue
|
||||
|
||||
return PlaywrightDownloadResult(ok=False, error=last_error or "no download captured")
|
||||
|
||||
def debug_dump(self) -> None:
|
||||
try:
|
||||
debug(
|
||||
f"[playwright] browser={self.defaults.browser} headless={self.defaults.headless} "
|
||||
f"viewport={self.defaults.viewport_width}x{self.defaults.viewport_height} "
|
||||
f"nav_timeout_ms={self.defaults.navigation_timeout_ms}"
|
||||
)
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to debug_dump Playwright defaults")
|
||||
|
||||
def _wait_for_block_clear(self, page: Any, timeout_ms: int = 8000) -> bool:
|
||||
try:
|
||||
page.wait_for_function(
|
||||
"() => { for (const k in window) { if (Object.prototype.hasOwnProperty.call(window, k) && k.startsWith('blocked_')) { try { return window[k] === false; } catch(e) {} return false; } } return true; }",
|
||||
timeout=timeout_ms,
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _save_response(self, response: Any, out_dir: Path) -> Optional[Path]:
|
||||
try:
|
||||
cd = ""
|
||||
try:
|
||||
headers = getattr(response, "headers", {}) or {}
|
||||
cd = "".join([v for k, v in headers.items() if str(k).lower() == "content-disposition"])
|
||||
except Exception:
|
||||
cd = ""
|
||||
|
||||
filename = _find_filename_from_cd(cd) or Path(str(getattr(response, "url", "") or "")).name or "download"
|
||||
body = response.body()
|
||||
out_path = out_dir / filename
|
||||
out_path.write_bytes(body)
|
||||
return out_path
|
||||
except Exception as exc:
|
||||
try:
|
||||
debug(f"[playwright] failed to save response: {exc}")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def config_schema() -> List[Dict[str, Any]]:
|
||||
"""Return a schema describing editable Playwright tool defaults for the config UI.
|
||||
|
||||
Notes:
|
||||
- `user_agent` is a dropdown with a `custom` option; put the real UA in
|
||||
`user_agent_custom` when choosing `custom`.
|
||||
- Viewport dimensions are offered as convenient choices.
|
||||
- `ffmpeg_path` intentionally defaults to empty; Playwright will consult
|
||||
a global `FFMPEG_PATH` environment variable (or fallback to bundled/system).
|
||||
"""
|
||||
_defaults = PlaywrightDefaults()
|
||||
|
||||
browser_choices = ["chromium", "firefox", "webkit"]
|
||||
viewport_width_choices = [1920, 1366, 1280, 1024, 800]
|
||||
viewport_height_choices = [1080, 900, 768, 720, 600]
|
||||
|
||||
return [
|
||||
{
|
||||
"key": "browser",
|
||||
"label": "Playwright browser",
|
||||
"group": "Browser",
|
||||
"default": _defaults.browser,
|
||||
"choices": browser_choices,
|
||||
},
|
||||
{
|
||||
"key": "headless",
|
||||
"label": "Headless",
|
||||
"group": "Browser",
|
||||
"type": "boolean",
|
||||
"default": str(_defaults.headless),
|
||||
"choices": ["true", "false"],
|
||||
},
|
||||
{
|
||||
"key": "user_agent",
|
||||
"label": "User Agent",
|
||||
"group": "Browser",
|
||||
"default": "default",
|
||||
"choices": ["default", "native", "custom"],
|
||||
},
|
||||
{
|
||||
"key": "user_agent_custom",
|
||||
"label": "Custom User Agent (used when User Agent = custom)",
|
||||
"group": "Browser",
|
||||
"default": "",
|
||||
"placeholder": "Mozilla/5.0 ...",
|
||||
},
|
||||
{
|
||||
"key": "viewport_width",
|
||||
"label": "Viewport width",
|
||||
"group": "Capture",
|
||||
"type": "integer",
|
||||
"default": _defaults.viewport_width,
|
||||
"choices": viewport_width_choices,
|
||||
},
|
||||
{
|
||||
"key": "viewport_height",
|
||||
"label": "Viewport height",
|
||||
"group": "Capture",
|
||||
"type": "integer",
|
||||
"default": _defaults.viewport_height,
|
||||
"choices": viewport_height_choices,
|
||||
},
|
||||
{
|
||||
"key": "navigation_timeout_ms",
|
||||
"label": "Navigation timeout (ms)",
|
||||
"group": "Navigation",
|
||||
"type": "integer",
|
||||
"default": _defaults.navigation_timeout_ms,
|
||||
},
|
||||
{
|
||||
"key": "screenshot_quality",
|
||||
"label": "Default screenshot quality",
|
||||
"group": "Capture",
|
||||
"type": "integer",
|
||||
"default": _defaults.screenshot_quality,
|
||||
"choices": list(range(1, 11)),
|
||||
},
|
||||
{
|
||||
"key": "ignore_https_errors",
|
||||
"label": "Ignore HTTPS errors",
|
||||
"group": "Navigation",
|
||||
"type": "boolean",
|
||||
"default": str(_defaults.ignore_https_errors),
|
||||
"choices": ["true", "false"],
|
||||
},
|
||||
{
|
||||
"key": "ffmpeg_path",
|
||||
"label": "FFmpeg path (leave empty to use global/bundled)",
|
||||
"group": "Environment",
|
||||
"type": "path",
|
||||
"default": "",
|
||||
"placeholder": "C:/path/to/ffmpeg.exe",
|
||||
},
|
||||
]
|
||||
@@ -11,11 +11,11 @@ from SYS.utils import format_bytes
|
||||
|
||||
|
||||
def _get_podcastindex_credentials(config: Dict[str, Any]) -> Tuple[str, str]:
|
||||
provider = config.get("provider")
|
||||
if not isinstance(provider, dict):
|
||||
plugin_cfg = config.get("plugin")
|
||||
if not isinstance(plugin_cfg, dict):
|
||||
return "", ""
|
||||
|
||||
entry = provider.get("podcastindex")
|
||||
entry = plugin_cfg.get("podcastindex")
|
||||
if not isinstance(entry, dict):
|
||||
return "", ""
|
||||
|
||||
@@ -290,7 +290,7 @@ class PodcastIndex(Provider):
|
||||
pass
|
||||
|
||||
try:
|
||||
from API.HTTP import _download_direct_file
|
||||
from API.HTTP import download_direct_file
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
@@ -308,7 +308,7 @@ class PodcastIndex(Provider):
|
||||
title_hint = str(item.get("title") or md.get("title") or "episode").strip() or "episode"
|
||||
|
||||
try:
|
||||
result_obj = _download_direct_file(
|
||||
result_obj = download_direct_file(
|
||||
enc_url,
|
||||
Path(output_dir),
|
||||
quiet=False,
|
||||
@@ -357,12 +357,12 @@ class PodcastIndex(Provider):
|
||||
"path": str(local_path),
|
||||
"hash": sha256,
|
||||
"title": title_hint,
|
||||
"action": "provider:podcastindex.selector",
|
||||
"action": "plugin:podcastindex.selector",
|
||||
"download_mode": "file",
|
||||
"store": "local",
|
||||
"media_kind": "audio",
|
||||
"tag": tags,
|
||||
"provider": "podcastindex",
|
||||
"plugin": "podcastindex",
|
||||
"url": enc_url,
|
||||
}
|
||||
if isinstance(md, dict) and md:
|
||||
|
||||
@@ -390,7 +390,7 @@ class SCP(Provider):
|
||||
table.set_table("scp")
|
||||
try:
|
||||
table.set_table_metadata({
|
||||
"provider": "scp",
|
||||
"plugin": "scp",
|
||||
"instance": instance_name or None,
|
||||
"host": settings.get("host"),
|
||||
"path": target_path,
|
||||
@@ -958,7 +958,7 @@ class SCP(Provider):
|
||||
parent = posixpath.dirname(scp_path.rstrip("/")) or "/"
|
||||
instance_name = str(settings.get("instance") or "").strip()
|
||||
metadata = {
|
||||
"provider": "scp",
|
||||
"plugin": "scp",
|
||||
"instance": instance_name or None,
|
||||
"host": settings.get("host"),
|
||||
"scp_path": scp_path,
|
||||
|
||||
@@ -690,7 +690,7 @@ class Soulseek(Provider):
|
||||
"album": item["album"],
|
||||
"track_num": item["track_num"],
|
||||
"ext": item["ext"],
|
||||
"provider": "soulseek"
|
||||
"plugin": "soulseek"
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -194,7 +194,7 @@ class Telegram(Provider):
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(config)
|
||||
telegram_conf = (
|
||||
self.config.get("provider",
|
||||
self.config.get("plugin",
|
||||
{}).get("telegram",
|
||||
{}) if isinstance(self.config,
|
||||
dict) else {}
|
||||
@@ -1280,7 +1280,7 @@ class Telegram(Provider):
|
||||
|
||||
info: Dict[str,
|
||||
Any] = {
|
||||
"provider": "telegram",
|
||||
"plugin": "telegram",
|
||||
"source_url": url,
|
||||
"chat": {
|
||||
"key": chat,
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from plugins.tidal.api import (
|
||||
Tidal as TidalApiClient,
|
||||
Tidal,
|
||||
build_track_tags,
|
||||
coerce_duration_seconds,
|
||||
extract_artists,
|
||||
@@ -208,7 +208,7 @@ class Tidal(Provider):
|
||||
self.api_timeout = float(self.config.get("timeout", 10.0))
|
||||
except Exception:
|
||||
self.api_timeout = 10.0
|
||||
self.api_clients = [TidalApiClient(base_url=url, timeout=self.api_timeout) for url in self.api_urls]
|
||||
self.api_clients = [Tidal(base_url=url, timeout=self.api_timeout) for url in self.api_urls]
|
||||
|
||||
def resolve_playback_path(self, item: Any, **_kwargs: Any) -> Optional[str]:
|
||||
return resolve_tidal_manifest_path(item)
|
||||
@@ -960,7 +960,7 @@ class Tidal(Provider):
|
||||
try:
|
||||
table.set_table_metadata(
|
||||
{
|
||||
"provider": "tidal",
|
||||
"plugin": "tidal",
|
||||
"view": "track",
|
||||
"album_id": album_id,
|
||||
"album_title": album_title,
|
||||
@@ -1670,7 +1670,7 @@ class Tidal(Provider):
|
||||
|
||||
return downloaded_count
|
||||
|
||||
def _get_api_client_for_base(self, base_url: str) -> Optional[TidalApiClient]:
|
||||
def _get_api_client_for_base(self, base_url: str) -> Optional[Tidal]:
|
||||
base = base_url.rstrip("/")
|
||||
for client in self.api_clients:
|
||||
if getattr(client, "base_url", "").rstrip("/") == base:
|
||||
@@ -2206,7 +2206,7 @@ class Tidal(Provider):
|
||||
table = Table(f"Tidal Albums: {artist_name}")._perseverance(False)
|
||||
table.set_table("tidal.album")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "tidal", "view": "album", "artist_id": artist_id, "artist_name": artist_name})
|
||||
table.set_table_metadata({"plugin": "tidal", "view": "album", "artist_id": artist_id, "artist_name": artist_name})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -2268,7 +2268,7 @@ class Tidal(Provider):
|
||||
try:
|
||||
table.set_table_metadata(
|
||||
{
|
||||
"provider": "tidal",
|
||||
"plugin": "tidal",
|
||||
"view": "track",
|
||||
"album_id": album_id,
|
||||
"album_title": album_title,
|
||||
@@ -2338,7 +2338,7 @@ class Tidal(Provider):
|
||||
table = Table("Tidal Track")._perseverance(True)
|
||||
table.set_table("tidal.track")
|
||||
try:
|
||||
table.set_table_metadata({"provider": "tidal", "view": "track", "resolved_manifest": True})
|
||||
table.set_table_metadata({"plugin": "tidal", "view": "track", "resolved_manifest": True})
|
||||
except Exception:
|
||||
pass
|
||||
results_payload: List[Dict[str, Any]] = []
|
||||
|
||||
@@ -320,8 +320,3 @@ class Tidal(API):
|
||||
border_style="cyan",
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
# Legacy alias for TidalApiClient
|
||||
TidalApiClient = Tidal
|
||||
HifiApiClient = Tidal
|
||||
|
||||
@@ -20,7 +20,7 @@ from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from PluginCore.inline_utils import resolve_filter
|
||||
from SYS.logger import debug, debug_panel
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
from tool.playwright import PlaywrightTool
|
||||
from plugins.playwright import PlaywrightTool
|
||||
|
||||
|
||||
class Vimm(TablePluginMixin, Provider):
|
||||
|
||||
@@ -23,12 +23,13 @@ from SYS.result_table import Table
|
||||
from SYS.rich_display import stderr_console as get_stderr_console
|
||||
from SYS import pipeline as pipeline_context
|
||||
from SYS.utils import sha256_file
|
||||
from tool.ytdlp import (
|
||||
from .tooling import (
|
||||
YtDlpTool,
|
||||
_best_subtitle_sidecar,
|
||||
_SUBTITLE_EXTS,
|
||||
_download_with_timeout,
|
||||
_format_chapters_note,
|
||||
config_schema as _ytdlp_config_schema,
|
||||
_read_text_file,
|
||||
collapse_picker_formats,
|
||||
format_for_table_selection,
|
||||
@@ -508,6 +509,10 @@ class ytdlp(TablePluginMixin, Provider):
|
||||
PLUGIN_ALIASES = ("youtube",)
|
||||
SEARCH_QUERY_KEYS = ("search", "q")
|
||||
|
||||
@staticmethod
|
||||
def config_schema() -> List[Dict[str, Any]]:
|
||||
return _ytdlp_config_schema()
|
||||
|
||||
@classmethod
|
||||
def url_patterns(cls) -> Tuple[str, ...]:
|
||||
try:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user