Files
MoviePilot/app/runtime/extensions/plugin/metadata.py
T

117 lines
4.4 KiB
Python

"""插件目录条目的运行态元数据映射。"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any, Optional
from app.runtime.extensions.plugin.contracts import supports_plugin_hook
from app.schemas.plugin import Plugin
class PluginMetadataMapper:
"""把市场或本地仓条目映射为包含运行态状态的插件 DTO。"""
def __init__(
self,
*,
plugin_instance: Callable[[str], Optional[Any]],
plugin_class: Callable[[str], Optional[Any]],
annotate_system_version: Callable[[dict], dict],
is_package_compatible: Callable[[dict, str], bool],
auth_checker: Callable[[Plugin, dict], bool],
version_compare: Callable[[str, str, str], bool],
log: Any,
) -> None:
"""保存注册表、兼容判断和权限判断端口。"""
self._plugin_instance = plugin_instance
self._plugin_class = plugin_class
self._annotate_system_version = annotate_system_version
self._is_package_compatible = is_package_compatible
self._auth_checker = auth_checker
self._version_compare = version_compare
self._logger = log
def map(
self,
plugin_id: str,
plugin_info: dict,
market: str,
installed_plugins: list[str],
add_time: int,
package_version: Optional[str] = None,
) -> Optional[Plugin]:
"""映射一个插件索引条目,不兼容或无权限时返回空。"""
if not isinstance(plugin_info, dict):
return None
info = self._annotate_system_version(plugin_info.copy())
if not self._is_package_compatible(info, package_version or ""):
return None
instance = self._plugin_instance(plugin_id)
plugin_class = self._plugin_class(plugin_id)
plugin = Plugin(id=plugin_id)
plugin.installed = plugin_id in installed_plugins and plugin_class is not None
plugin.has_update = False
if plugin_class:
installed_version = getattr(plugin_class, "plugin_version", None)
online_version = info.get("version")
if installed_version and online_version:
plugin.has_update = self._version_compare(
installed_version,
"<",
online_version,
)
plugin.system_version = info.get("system_version")
if info.get("system_version_compatible") is False:
plugin.system_version_compatible = False
plugin.system_version_message = info.get("system_version_message")
plugin.state = self._state(plugin_id, instance)
plugin.has_page = bool(
instance and supports_plugin_hook(instance, "get_page")
)
if info.get("key"):
plugin.plugin_public_key = info["key"]
if not self._auth_checker(plugin, info):
return None
plugin.plugin_name = info.get("name")
plugin.plugin_desc = info.get("description")
plugin.plugin_version = info.get("version")
plugin.plugin_icon = info.get("icon")
plugin.plugin_label = self.normalize_label(info.get("labels"))
plugin.plugin_author = info.get("author")
plugin.history = info.get("history") or {}
plugin.release = bool(info.get("release"))
plugin.repo_url = market
plugin.is_local = False
plugin.add_time = add_time
return plugin
def _state(self, plugin_id: str, instance: Optional[Any]) -> bool:
"""安全读取插件运行状态,插件异常时降级为未启用。"""
if not instance or not hasattr(instance, "get_state"):
return False
try:
return bool(instance.get_state())
except Exception as error: # noqa: BLE001
self._logger.error(f"获取插件 {plugin_id} 状态出错:{error}")
return False
@staticmethod
def normalize_label(labels: Any) -> Optional[str]:
"""兼容市场标签的旧字符串和新列表格式。"""
if isinstance(labels, str):
label = labels.strip()
return label or None
if isinstance(labels, list):
normalized = [
str(item).strip()
for item in labels
if str(item).strip()
]
return " ".join(normalized) or None
return None