mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 11:27:43 +08:00
226 lines
8.4 KiB
Python
226 lines
8.4 KiB
Python
"""插件更新说明和 Release 版本查询用例。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
from dataclasses import dataclass
|
|
from typing import Any, cast
|
|
|
|
from app.application.plugin.catalog import apply_declared_metadata_fallback
|
|
from app.application.plugin.identity import PluginIdentity, PluginPayloadSourceType
|
|
from app.schemas.plugin import Plugin
|
|
|
|
MarketPluginLoader = Callable[[str, str | None, bool], Awaitable[list[Plugin] | None]]
|
|
ReleaseCacheProbe = Callable[[str], Awaitable[bool]]
|
|
ReleaseLoader = Callable[[str, str], Awaitable[list[dict[str, Any]]]]
|
|
ReleaseRefresher = Callable[[str, str], Awaitable[object]]
|
|
IdentityReader = Callable[[str], Awaitable[PluginIdentity | None]]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginReleaseSnapshot:
|
|
"""描述指定插件仓库的 Release 展示快照。"""
|
|
|
|
release_supported: bool
|
|
latest_version: str | None
|
|
current_version: str | None
|
|
items: tuple[dict[str, Any], ...]
|
|
refresh_required: bool = False
|
|
|
|
|
|
class PluginReleaseService:
|
|
"""协调可信仓库元数据、Release 缓存和本地版本查询。"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
installed_plugins: Callable[[], Sequence[Plugin]],
|
|
local_repo_plugins: Callable[[], Sequence[Plugin]],
|
|
market_plugins: MarketPluginLoader,
|
|
local_version: Callable[[str], str | None],
|
|
identity: IdentityReader,
|
|
version_flag: Callable[[], str | None],
|
|
compatible_flags: Callable[[str | None], Sequence[str]],
|
|
has_release_cache: ReleaseCacheProbe,
|
|
releases: ReleaseLoader,
|
|
refresh_releases: ReleaseRefresher,
|
|
) -> None:
|
|
"""保存运行态、来源身份和市场读取窄端口。"""
|
|
self._installed_plugins = installed_plugins
|
|
self._local_repo_plugins = local_repo_plugins
|
|
self._market_plugins = market_plugins
|
|
self._local_version = local_version
|
|
self._identity = identity
|
|
self._version_flag = version_flag
|
|
self._compatible_flags = compatible_flags
|
|
self._has_release_cache = has_release_cache
|
|
self._releases = releases
|
|
self._refresh_releases = refresh_releases
|
|
|
|
async def history(self, plugin_id: str, *, force: bool = True) -> Plugin | None:
|
|
"""按可信绑定仓库读取单个已安装插件的更新说明。"""
|
|
installed_plugin = next(
|
|
(plugin for plugin in self._installed_plugins() if plugin.id == plugin_id),
|
|
None,
|
|
)
|
|
if installed_plugin is None:
|
|
return None
|
|
|
|
identity = await self._identity(plugin_id)
|
|
if identity is not None:
|
|
installed_plugin = apply_declared_metadata_fallback(
|
|
[installed_plugin],
|
|
{identity.normalized_plugin_id: identity},
|
|
)[0]
|
|
|
|
local_plugin = next(
|
|
(plugin for plugin in self._local_repo_plugins() if plugin.id == plugin_id),
|
|
None,
|
|
)
|
|
if identity is not None and identity.payload_source_type is PluginPayloadSourceType.LOCAL:
|
|
return (
|
|
_merge_market_metadata(installed_plugin, local_plugin)
|
|
if local_plugin is not None
|
|
else installed_plugin
|
|
)
|
|
|
|
repo_url = _trusted_repo_url(identity)
|
|
if repo_url is not None:
|
|
market_plugin = await self._market_plugin(plugin_id, repo_url, force)
|
|
return (
|
|
_merge_market_metadata(installed_plugin, market_plugin)
|
|
if market_plugin is not None
|
|
else installed_plugin
|
|
)
|
|
|
|
# 尚未建立来源身份的本地插件继续使用本地仓库元数据,兼容首次迁移前的运行态。
|
|
return (
|
|
_merge_market_metadata(installed_plugin, local_plugin)
|
|
if local_plugin is not None
|
|
else installed_plugin
|
|
)
|
|
|
|
async def versions(
|
|
self,
|
|
plugin_id: str,
|
|
repo_url: str,
|
|
*,
|
|
force: bool = False,
|
|
) -> PluginReleaseSnapshot:
|
|
"""读取 Release 快照,并标记是否需要后台强制刷新已有缓存。"""
|
|
if not repo_url:
|
|
return PluginReleaseSnapshot(False, None, None, ())
|
|
|
|
market_plugin = await self._market_plugin(plugin_id, repo_url, force)
|
|
latest_version = market_plugin.plugin_version if market_plugin else None
|
|
current_version = self._local_version(plugin_id)
|
|
if not getattr(market_plugin, "release", False):
|
|
return PluginReleaseSnapshot(
|
|
False,
|
|
latest_version,
|
|
current_version,
|
|
(),
|
|
)
|
|
|
|
refresh_required = force and await self._has_release_cache(repo_url)
|
|
release_items = await self._releases(plugin_id, repo_url)
|
|
items = tuple(
|
|
_project_release_item(item, latest_version, current_version)
|
|
for item in release_items
|
|
)
|
|
return PluginReleaseSnapshot(
|
|
bool(items),
|
|
latest_version,
|
|
current_version,
|
|
items,
|
|
refresh_required=refresh_required,
|
|
)
|
|
|
|
async def refresh(self, plugin_id: str, repo_url: str) -> None:
|
|
"""强制刷新指定仓库的 Release 缓存。"""
|
|
await self._refresh_releases(plugin_id, repo_url)
|
|
|
|
async def _market_plugin(
|
|
self, plugin_id: str, repo_url: str, force: bool
|
|
) -> Plugin | None:
|
|
"""按当前、兼容和基础索引顺序读取一个插件。"""
|
|
version_flag = self._version_flag()
|
|
package_versions: list[str | None] = [version_flag] if version_flag else []
|
|
package_versions.extend(self._compatible_flags(version_flag))
|
|
package_versions.append(None)
|
|
for package_version in dict.fromkeys(package_versions):
|
|
plugins = await self._market_plugins(repo_url, package_version, force)
|
|
matched = next(
|
|
(plugin for plugin in plugins or [] if plugin.id == plugin_id),
|
|
None,
|
|
)
|
|
if matched is not None:
|
|
return matched
|
|
return None
|
|
|
|
|
|
_release_service: PluginReleaseService | None = None
|
|
|
|
|
|
def configure_plugin_release_service(service: PluginReleaseService) -> None:
|
|
"""由启动组合根发布当前 lifespan 的 Release 查询服务。"""
|
|
global _release_service
|
|
_release_service = service
|
|
|
|
|
|
def get_plugin_release_service() -> PluginReleaseService:
|
|
"""返回已经由启动组合根装配的 Release 查询服务。"""
|
|
if _release_service is None:
|
|
raise RuntimeError("插件 Release 查询服务尚未完成初始化")
|
|
return _release_service
|
|
|
|
|
|
def reset_plugin_release_service() -> None:
|
|
"""清除当前 lifespan 的 Release 查询服务,供停机和隔离测试使用。"""
|
|
global _release_service
|
|
_release_service = None
|
|
|
|
|
|
def _trusted_repo_url(identity: PluginIdentity | None) -> str | None:
|
|
"""把持久化的 GitHub 来源键投影为仓库 URL。"""
|
|
if identity is None or not identity.trusted_source_key:
|
|
return None
|
|
owner_repo = identity.trusted_source_key.removeprefix("github:")
|
|
return f"https://github.com/{owner_repo}"
|
|
|
|
|
|
def _merge_market_metadata(plugin: Plugin, market_plugin: Plugin) -> Plugin:
|
|
"""在隔离副本上合并市场展示元数据。"""
|
|
return cast(
|
|
Plugin,
|
|
plugin.model_copy(
|
|
update={
|
|
"repo_url": market_plugin.repo_url or plugin.repo_url,
|
|
"history": market_plugin.history or {},
|
|
"release": market_plugin.release,
|
|
"has_update": market_plugin.has_update,
|
|
"system_version": market_plugin.system_version
|
|
or plugin.system_version,
|
|
"system_version_compatible": market_plugin.system_version_compatible,
|
|
"system_version_message": (
|
|
market_plugin.system_version_message
|
|
or plugin.system_version_message
|
|
),
|
|
}
|
|
),
|
|
)
|
|
|
|
|
|
def _project_release_item(
|
|
item: Mapping[str, Any],
|
|
latest_version: str | None,
|
|
current_version: str | None,
|
|
) -> dict[str, Any]:
|
|
"""复制并标记一个 Release 条目的最新和当前版本状态。"""
|
|
version = item.get("version")
|
|
return {
|
|
**item,
|
|
"is_latest": bool(latest_version and version == latest_version),
|
|
"is_current": bool(current_version and version == current_version),
|
|
}
|