154 lines
5.1 KiB
Python
Executable File
154 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Run live snapshot checks for plugins.
|
|
|
|
Use --update to refresh stored snapshots.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import importlib.util
|
|
import inspect
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parents[1]
|
|
PLUGIN_DIR = ROOT_DIR / "addon" / "plugins"
|
|
SNAPSHOT_PATH = ROOT_DIR / "qa" / "plugin_snapshots.json"
|
|
|
|
sys.path.insert(0, str(ROOT_DIR / "addon"))
|
|
|
|
try:
|
|
from plugin_interface import BasisPlugin # type: ignore
|
|
except Exception as exc: # pragma: no cover
|
|
raise SystemExit(f"Failed to import BasisPlugin: {exc}")
|
|
|
|
CONFIG = [
|
|
{"plugin": "Serienstream", "method": "search_titles", "args": ["trek"], "max_items": 20},
|
|
{"plugin": "Aniworld", "method": "search_titles", "args": ["naruto"], "max_items": 20},
|
|
{"plugin": "Topstreamfilm", "method": "search_titles", "args": ["matrix"], "max_items": 20},
|
|
{"plugin": "Einschalten", "method": "new_titles_page", "args": [1], "max_items": 20},
|
|
{"plugin": "Filmpalast", "method": "search_titles", "args": ["trek"], "max_items": 20},
|
|
{"plugin": "Doku-Streams", "method": "search_titles", "args": ["japan"], "max_items": 20},
|
|
]
|
|
|
|
|
|
def _import_module(path: Path):
|
|
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
if spec is None or spec.loader is None:
|
|
raise ImportError(f"Missing spec for {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _discover_plugins() -> dict[str, BasisPlugin]:
|
|
plugins: dict[str, BasisPlugin] = {}
|
|
for file_path in sorted(PLUGIN_DIR.glob("*.py")):
|
|
if file_path.name.startswith("_"):
|
|
continue
|
|
module = _import_module(file_path)
|
|
preferred = getattr(module, "Plugin", None)
|
|
if inspect.isclass(preferred) and issubclass(preferred, BasisPlugin) and preferred is not BasisPlugin:
|
|
classes = [preferred]
|
|
else:
|
|
classes = [
|
|
obj
|
|
for obj in module.__dict__.values()
|
|
if inspect.isclass(obj) and issubclass(obj, BasisPlugin) and obj is not BasisPlugin
|
|
]
|
|
classes.sort(key=lambda cls: cls.__name__.casefold())
|
|
for cls in classes:
|
|
instance = cls()
|
|
name = str(getattr(instance, "name", "") or "").strip()
|
|
if name and name not in plugins:
|
|
plugins[name] = instance
|
|
return plugins
|
|
|
|
|
|
def _normalize_titles(value: Any, max_items: int) -> list[str]:
|
|
if not value:
|
|
return []
|
|
titles = [str(item).strip() for item in list(value) if item and str(item).strip()]
|
|
seen = set()
|
|
normalized: list[str] = []
|
|
for title in titles:
|
|
key = title.casefold()
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
normalized.append(title)
|
|
if len(normalized) >= max_items:
|
|
break
|
|
return normalized
|
|
|
|
|
|
def _snapshot_key(entry: dict[str, Any]) -> str:
|
|
args = entry.get("args", [])
|
|
return f"{entry['plugin']}::{entry['method']}::{','.join(str(a) for a in args)}"
|
|
|
|
|
|
def _call_method(plugin: BasisPlugin, method_name: str, args: list[Any]):
|
|
method = getattr(plugin, method_name, None)
|
|
if not callable(method):
|
|
raise RuntimeError(f"Method missing: {method_name}")
|
|
result = method(*args)
|
|
if asyncio.iscoroutine(result):
|
|
return asyncio.run(result)
|
|
return result
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--update", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
snapshots: dict[str, Any] = {}
|
|
if SNAPSHOT_PATH.exists():
|
|
snapshots = json.loads(SNAPSHOT_PATH.read_text(encoding="utf-8"))
|
|
data = snapshots.get("snapshots", {}) if isinstance(snapshots, dict) else {}
|
|
if args.update:
|
|
data = {}
|
|
|
|
plugins = _discover_plugins()
|
|
errors = []
|
|
|
|
for entry in CONFIG:
|
|
plugin_name = entry["plugin"]
|
|
plugin = plugins.get(plugin_name)
|
|
if plugin is None:
|
|
errors.append(f"Plugin missing: {plugin_name}")
|
|
continue
|
|
key = _snapshot_key(entry)
|
|
try:
|
|
result = _call_method(plugin, entry["method"], entry.get("args", []))
|
|
normalized = _normalize_titles(result, entry.get("max_items", 20))
|
|
except Exception as exc:
|
|
errors.append(f"Snapshot error: {key} ({exc})")
|
|
if args.update:
|
|
data[key] = {"error": str(exc)}
|
|
continue
|
|
if args.update:
|
|
data[key] = normalized
|
|
else:
|
|
expected = data.get(key)
|
|
if expected != normalized:
|
|
errors.append(f"Snapshot mismatch: {key}\nExpected: {expected}\nActual: {normalized}")
|
|
|
|
if args.update:
|
|
SNAPSHOT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
SNAPSHOT_PATH.write_text(json.dumps({"snapshots": data}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
|
|
if errors:
|
|
for err in errors:
|
|
print(err)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|