Files
MoviePilot/app/api/endpoints/pluginclone.py

120 lines
5.2 KiB
Python

"""插件分身的创建、恢复与可恢复清单接口。
这些路由并入 ``plugin`` 路由器,路径与单文件时期完全一致;单独成篇只是让分身创建与
插件目录、市场、静态资源等关注点各自分开,与 ``pluginfolder``/``pluginloglevel``/
``plugintarget`` 的切分方式一致。
"""
from typing import Any, List
from fastapi import Depends, Response
from app.api.dependencies.auth import get_current_active_superuser
from app.api.endpoints.plugin import register_plugin
from app.api.principal import ApiPrincipal
from app.api.response import (
COLLECTION_TOTAL_HEADER,
COLLECTION_TOTAL_OPENAPI_KEY,
CompatibleCountParam,
CompatiblePageParam,
ResponseAPIRouter,
resolve_compatible_pagination,
)
from app.application.plugin.folders import add_clone_to_plugin_folder
from app.application.plugin.runtime import get_plugin_manager
from app.runtime.log import logger
from app.schemas.plugin import PluginCloneOutcome as _SchemaPluginCloneOutcome
from app.schemas.plugin import PluginCloneRequest as _SchemaPluginCloneRequest
from app.schemas.plugin import PluginRestorableInstance as _SchemaPluginRestorableInstance
from app.schemas.response import Response as _SchemaResponse
router = ResponseAPIRouter()
@router.get( # type: ignore[misc]
"/clone/{plugin_id}/restorable",
summary="列出可恢复的已停用分身",
response_model=List[_SchemaPluginRestorableInstance],
openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True},
)
def plugin_restorable_instances(
plugin_id: str,
_: ApiPrincipal = Depends(get_current_active_superuser),
page: CompatiblePageParam = None,
count: CompatibleCountParam = None,
response: Response = None,
) -> Any:
"""
列出该插件名下已停用、设置仍留存可被恢复的分身
停用只把启用位置假,业务参数与展示信息都还留在那一行上。启用中的分身不在此列
——它们的配置正被使用,摆进恢复选择器只会让人误以为能把一个活着的实例再建一遍。
未指定分页时返回完整清单:一个插件的历史分身数量有限,恢复选择器要一次看全。
"""
instances = [
_SchemaPluginRestorableInstance(**item)
for item in get_plugin_manager().get_restorable_plugin_instances(plugin_id)
]
if response is not None:
response.headers[COLLECTION_TOTAL_HEADER] = str(len(instances))
if page is not None or count is not None:
page, count = resolve_compatible_pagination(page, count)
assert page is not None and count is not None
return instances[(page - 1) * count: page * count]
return instances
@router.post( # type: ignore[misc]
"/clone/{plugin_id}",
summary="创建插件分身",
response_model=_SchemaResponse[_SchemaPluginCloneOutcome],
)
def clone_plugin(
plugin_id: str,
clone_data: _SchemaPluginCloneRequest,
_: ApiPrincipal = Depends(get_current_active_superuser),
) -> Any:
"""
创建插件分身
不填后缀时由服务端分配一个最小可用序号,因而实例 ID 只能由回执给出;该后缀名下
留有一个已停用的分身时,本次创建就是把那一行连同它的业务参数重新启用。
"""
plugin_manager = get_plugin_manager()
try:
with plugin_manager.mutation(f"创建插件 {plugin_id} 分身"):
success, message = plugin_manager.clone_plugin(
plugin_id=plugin_id,
suffix=clone_data.suffix,
name=clone_data.name,
description=clone_data.description,
version=clone_data.version,
icon=clone_data.icon,
restore_previous=clone_data.restore_previous,
)
if not success:
return _SchemaResponse(success=False, message=message)
# 分身此时已经创建并加载完成,后面只是补齐宿主注册。这一步失败不能报成
# 「创建失败」:分身确实已经存在,用户照提示重试只会撞上「分身已存在」,
# 真正的原因反而被那句话盖掉。
outcome = _SchemaPluginCloneOutcome(instance_id=message)
try:
register_plugin(message)
# 将分身插件添加到原插件所在的文件夹中
add_clone_to_plugin_folder(plugin_id, message)
except Exception as register_error: # noqa: BLE001
logger.error(f"插件分身 {message} 已创建,注册宿主能力失败:{register_error}")
return _SchemaResponse(
success=False,
message=(
f"插件分身 {message} 已创建,但注册定时任务或路由失败:"
f"{register_error};请检查该分身配置后重载插件"
),
data=outcome,
)
return _SchemaResponse(success=True, message="插件分身创建成功", data=outcome)
except Exception as e:
logger.error(f"创建插件分身失败:{str(e)}")
return _SchemaResponse(success=False, message=f"创建插件分身失败:{str(e)}")