mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 12:06:51 +08:00
147 lines
5.7 KiB
Python
147 lines
5.7 KiB
Python
"""插件运行目录、本地仓和联邦产物路径解析。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from collections.abc import Callable, Mapping
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from app.runtime.extensions.plugin.system import PluginSystemServices
|
|
|
|
|
|
class PluginPathResolver:
|
|
"""把文件事件解析为插件 ID、本地候选和联邦入口状态。"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
runtime_root: Path,
|
|
running: Callable[[], Mapping[str, Any]],
|
|
system: Callable[[], PluginSystemServices],
|
|
strict_system_version: Callable[[], bool],
|
|
log: Any,
|
|
) -> None:
|
|
"""保存运行目录和插件市场路径解析端口。"""
|
|
self._runtime_root = runtime_root.resolve()
|
|
self._running = running
|
|
self._system = system
|
|
self._strict_system_version = strict_system_version
|
|
self._logger = log
|
|
|
|
def federated_change(
|
|
self,
|
|
event_path: Path,
|
|
) -> Optional[tuple[str, Optional[dict], bool]]:
|
|
"""识别联邦构建产物变化并确认入口文件已完整生成。"""
|
|
try:
|
|
event_path = event_path.resolve()
|
|
candidate = self.local_candidate(event_path)
|
|
if candidate:
|
|
plugin_id = candidate.get("id")
|
|
plugin_dir = Path(candidate.get("path")).resolve()
|
|
else:
|
|
if not event_path.is_relative_to(self._runtime_root):
|
|
return None
|
|
relative_parts = event_path.relative_to(self._runtime_root).parts
|
|
if not relative_parts:
|
|
return None
|
|
plugin_dir = self._runtime_root / relative_parts[0]
|
|
plugin_id = next(
|
|
(
|
|
item
|
|
for item in self._running()
|
|
if item.lower() == relative_parts[0].lower()
|
|
),
|
|
None,
|
|
)
|
|
if not plugin_id:
|
|
return None
|
|
plugin = self._running().get(plugin_id)
|
|
if not plugin:
|
|
return None
|
|
render_mode, dist_path = plugin.get_render_mode()
|
|
if render_mode != "vue" or not isinstance(dist_path, str) or not dist_path:
|
|
return None
|
|
relative_dist_path = Path(dist_path)
|
|
if (
|
|
relative_dist_path.is_absolute()
|
|
or ".." in relative_dist_path.parts
|
|
or "\\" in dist_path
|
|
):
|
|
return None
|
|
plugin_dir = plugin_dir.resolve()
|
|
dist_dir = (plugin_dir / relative_dist_path).resolve()
|
|
if (
|
|
dist_dir == plugin_dir
|
|
or not dist_dir.is_relative_to(plugin_dir)
|
|
or not event_path.is_relative_to(dist_dir)
|
|
):
|
|
return None
|
|
remote_entry = dist_dir / "remoteEntry.js"
|
|
ready = remote_entry.is_file() and remote_entry.resolve().is_relative_to(
|
|
plugin_dir
|
|
)
|
|
return plugin_id, candidate, ready
|
|
except Exception as error:
|
|
self._logger.error(f"识别插件联邦构建产物变化时出错: {error}")
|
|
return None
|
|
|
|
def runtime_plugin(self, event_path: Path) -> Optional[str]:
|
|
"""从运行目录中的插件 ``__init__.py`` AST 解析插件类名。"""
|
|
try:
|
|
event_path = event_path.resolve()
|
|
if not event_path.is_relative_to(self._runtime_root):
|
|
return None
|
|
parts = event_path.relative_to(self._runtime_root).parts
|
|
if not parts:
|
|
return None
|
|
init_file = self._runtime_root / parts[0] / "__init__.py"
|
|
if not init_file.exists():
|
|
return None
|
|
tree = ast.parse(
|
|
init_file.read_text(encoding="utf-8", errors="replace")
|
|
)
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.ClassDef):
|
|
continue
|
|
if any(
|
|
isinstance(base, ast.Name) and base.id == "_PluginBase"
|
|
for base in node.bases
|
|
):
|
|
return node.name
|
|
return None
|
|
except Exception as error:
|
|
self._logger.error(f"从路径解析插件 ID 时出错: {error}")
|
|
return None
|
|
|
|
def local_candidate(self, event_path: Path) -> Optional[dict]:
|
|
"""按 ``plugins``、``plugins.v2``、``plugins.v3`` 目录解析候选。"""
|
|
try:
|
|
event_path = event_path.resolve()
|
|
for repo_path in self._system().local_repo_paths():
|
|
if not repo_path.exists() or not repo_path.is_dir():
|
|
continue
|
|
if not event_path.is_relative_to(repo_path):
|
|
continue
|
|
parts = event_path.relative_to(repo_path).parts
|
|
if len(parts) < 2:
|
|
continue
|
|
if parts[0] == "plugins":
|
|
package_version = ""
|
|
elif parts[0].startswith("plugins."):
|
|
package_version = parts[0].split(".", 1)[1]
|
|
else:
|
|
continue
|
|
return self._system().local_candidate(
|
|
parts[1],
|
|
package_version=package_version,
|
|
repo_path=repo_path,
|
|
strict_compat=False,
|
|
strict_system_version=self._strict_system_version(),
|
|
)
|
|
return None
|
|
except Exception as error:
|
|
self._logger.error(f"从本地插件仓路径解析候选时出错: {error}")
|
|
return None
|