mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
refactor(api): split management endpoint routers
This commit is contained in:
@@ -417,7 +417,7 @@ def _clear_transfer_history(
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@router.delete( # type: ignore[misc]
|
||||
"/transfer/all",
|
||||
summary="清空旧整理记录",
|
||||
response_model=_SchemaResponse[None],
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""自定义识别词结构化读写端点。"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from app.api.context import get_host_runtime
|
||||
from app.api.dependencies.auth import get_current_active_superuser_async
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.application.configuration import (
|
||||
get_configured_system_config,
|
||||
get_runtime_settings,
|
||||
)
|
||||
from app.application.settings import SystemSettingConflictError, SystemSettingsService
|
||||
from app.schemas.common import JsonObject
|
||||
from app.schemas.response import Response
|
||||
from app.schemas.system import CustomIdentifiersUpdateRequest
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.startup.composition.context import HostRuntime
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/identifiers",
|
||||
summary="查询自定义识别词",
|
||||
response_model=Response[JsonObject],
|
||||
)
|
||||
async def query_custom_identifiers(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Response[Any]:
|
||||
"""返回完整的自定义识别词列表。"""
|
||||
identifiers = [
|
||||
item
|
||||
for item in (get_configured_system_config().get(SystemConfigKey.CustomIdentifiers) or [])
|
||||
if isinstance(item, str)
|
||||
]
|
||||
return Response(
|
||||
success=True,
|
||||
data={"count": len(identifiers), "identifiers": identifiers},
|
||||
)
|
||||
|
||||
|
||||
@router.post( # type: ignore[misc]
|
||||
"/identifiers",
|
||||
summary="更新自定义识别词",
|
||||
response_model=Response[JsonObject],
|
||||
)
|
||||
async def update_custom_identifiers(
|
||||
payload: CustomIdentifiersUpdateRequest,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
runtime: HostRuntime = Depends(get_host_runtime),
|
||||
) -> Response[Any]:
|
||||
"""完整替换自定义识别词,并拒绝基于过期快照的覆盖。"""
|
||||
identifiers = list(payload.identifiers)
|
||||
try:
|
||||
data = await SystemSettingsService(
|
||||
get_runtime_settings(),
|
||||
get_configured_system_config(),
|
||||
runtime.system.publish_config_changed,
|
||||
).update(
|
||||
setting_key=SystemConfigKey.CustomIdentifiers.value,
|
||||
value=identifiers or None,
|
||||
expected_value=payload.expected_identifiers,
|
||||
enforce_expected_value=payload.expected_identifiers is not None,
|
||||
)
|
||||
except SystemSettingConflictError as error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(error),
|
||||
) from error
|
||||
data.update({"count": len(identifiers), "identifiers": identifiers})
|
||||
return Response(success=True, message=data.get("message"), data=data)
|
||||
+4
-135
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
from typing import Annotated, Any, Dict, List, Optional, Union
|
||||
from typing import Annotated, Any, Dict, List, Optional
|
||||
|
||||
import aiofiles
|
||||
from anyio import Path as AsyncPath
|
||||
@@ -23,6 +23,7 @@ from app.api.dependencies.auth import (
|
||||
get_current_active_superuser_async,
|
||||
)
|
||||
from app.api.dependencies.plugin import get_plugin_config_command
|
||||
from app.api.endpoints.pluginfolder import router as plugin_folders_router
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import (
|
||||
COLLECTION_TOTAL_HEADER,
|
||||
@@ -37,11 +38,7 @@ from app.application.configuration import get_api_runtime_config_snapshot, get_c
|
||||
from app.application.plugin.catalog import get_plugin_catalog_query
|
||||
from app.application.plugin.config import PluginConfigCommand
|
||||
from app.application.plugin.data import PluginDataQueryService, PluginDataSummaryService
|
||||
from app.application.plugin.folders import (
|
||||
add_clone_to_plugin_folder,
|
||||
get_plugin_folder_service,
|
||||
remove_plugin_from_folders,
|
||||
)
|
||||
from app.application.plugin.folders import add_clone_to_plugin_folder, remove_plugin_from_folders
|
||||
from app.application.plugin.gateway import get_plugin_install_service
|
||||
from app.application.plugin.management import (
|
||||
get_plugin_snapshot,
|
||||
@@ -64,9 +61,6 @@ from app.schemas.plugin import PluginCloneRequest as _SchemaPluginCloneRequest
|
||||
from app.schemas.plugin import PluginDashboard as _SchemaPluginDashboard
|
||||
from app.schemas.plugin import PluginDashboardMetaItem as _SchemaPluginDashboardMetaItem
|
||||
from app.schemas.plugin import PluginDataSummary as _SchemaPluginDataSummary
|
||||
from app.schemas.plugin import PluginFolderPluginsUpdateRequest as _SchemaPluginFolderPluginsUpdateRequest
|
||||
from app.schemas.plugin import PluginFoldersData as _SchemaPluginFoldersData
|
||||
from app.schemas.plugin import PluginFolderUpdateRequest as _SchemaPluginFolderUpdateRequest
|
||||
from app.schemas.plugin import PluginInstallOutcome as _SchemaPluginInstallOutcome
|
||||
from app.schemas.plugin import PluginRating as _SchemaPluginRating
|
||||
from app.schemas.plugin import PluginRatingMap as _SchemaPluginRatingMap
|
||||
@@ -92,6 +86,7 @@ from app.schemas.types import SystemConfigKey
|
||||
from app.startup.composition.context import HostRuntime
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
router.include_router(plugin_folders_router)
|
||||
_plugin_release_refresh_tasks: set[asyncio.Task] = set()
|
||||
|
||||
|
||||
@@ -771,132 +766,6 @@ async def plugin_static_file(
|
||||
raise HTTPException(status_code=500, detail="Internal Server Error")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/folders",
|
||||
summary="获取插件文件夹配置",
|
||||
response_model=_SchemaPluginFoldersData,
|
||||
)
|
||||
async def get_plugin_folders(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> dict:
|
||||
"""
|
||||
获取插件文件夹分组配置
|
||||
"""
|
||||
return get_plugin_folder_service().get_or_empty()
|
||||
|
||||
|
||||
@router.post("/folders", summary="保存插件文件夹配置", response_model=_SchemaResponse[None])
|
||||
async def save_plugin_folders(
|
||||
folders: _SchemaPluginFoldersData,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
保存插件文件夹分组配置
|
||||
"""
|
||||
result = await get_plugin_folder_service().save(folders.root)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.post("/folders/{folder_name}", summary="创建插件文件夹", response_model=_SchemaResponse[None])
|
||||
async def create_plugin_folder(folder_name: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)) -> Any:
|
||||
"""
|
||||
创建新的插件文件夹
|
||||
"""
|
||||
result = await get_plugin_folder_service().create(folder_name)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.delete("/folders/{folder_name}", summary="删除插件文件夹", response_model=_SchemaResponse[None])
|
||||
async def delete_plugin_folder(folder_name: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)) -> Any:
|
||||
"""
|
||||
删除插件文件夹
|
||||
"""
|
||||
result = await get_plugin_folder_service().delete(folder_name)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/folders/{folder_name}",
|
||||
summary="更新插件文件夹",
|
||||
response_model=_SchemaResponse[None],
|
||||
)
|
||||
async def update_plugin_folder(
|
||||
folder_name: str,
|
||||
folder: _SchemaPluginFolderUpdateRequest,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""增量更新插件文件夹名称或展示配置。"""
|
||||
changes = folder.model_dump(
|
||||
by_alias=True,
|
||||
exclude={"new_name"},
|
||||
exclude_unset=True,
|
||||
)
|
||||
result = await get_plugin_folder_service().update_folder(
|
||||
folder_name,
|
||||
new_name=folder.new_name,
|
||||
changes=changes,
|
||||
)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/folders/{folder_name}/plugins",
|
||||
summary="更新文件夹中的插件",
|
||||
response_model=_SchemaResponse[None],
|
||||
)
|
||||
async def update_folder_plugins(
|
||||
folder_name: str,
|
||||
plugin_update: Union[List[str], _SchemaPluginFolderPluginsUpdateRequest],
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""条件替换指定文件夹中的插件列表,并兼容旧数组请求。"""
|
||||
if isinstance(plugin_update, list):
|
||||
plugin_ids = plugin_update
|
||||
expected_plugin_ids = None
|
||||
else:
|
||||
plugin_ids = plugin_update.plugins
|
||||
expected_plugin_ids = plugin_update.expected_plugins
|
||||
result = await get_plugin_folder_service().update_plugins(
|
||||
folder_name,
|
||||
plugin_ids,
|
||||
expected_plugin_ids,
|
||||
)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/folders/{folder_name}/plugins/{plugin_id}",
|
||||
summary="移动插件到文件夹",
|
||||
response_model=_SchemaResponse[None],
|
||||
)
|
||||
async def assign_plugin_to_folder(
|
||||
folder_name: str,
|
||||
plugin_id: str,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""把一个插件原子迁移到目标文件夹。"""
|
||||
result = await get_plugin_folder_service().assign_plugin(folder_name, plugin_id)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/folders/{folder_name}/plugins/{plugin_id}",
|
||||
summary="从文件夹移除插件",
|
||||
response_model=_SchemaResponse[None],
|
||||
)
|
||||
async def remove_plugin_from_folder(
|
||||
folder_name: str,
|
||||
plugin_id: str,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""只从指定文件夹移除一个插件。"""
|
||||
result = await get_plugin_folder_service().remove_plugin_from_folder(
|
||||
folder_name,
|
||||
plugin_id,
|
||||
)
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.post("/clone/{plugin_id}", summary="创建插件分身", response_model=_SchemaResponse[None])
|
||||
def clone_plugin(
|
||||
plugin_id: str,
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""插件文件夹增量管理端点。"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from app.api.dependencies.auth import get_current_active_superuser_async
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.application.plugin.folders import get_plugin_folder_service
|
||||
from app.schemas.plugin import (
|
||||
PluginFolderPluginsUpdateRequest,
|
||||
PluginFoldersData,
|
||||
PluginFolderUpdateRequest,
|
||||
)
|
||||
from app.schemas.response import Response
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/folders",
|
||||
summary="获取插件文件夹配置",
|
||||
response_model=PluginFoldersData,
|
||||
)
|
||||
async def get_plugin_folders(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> dict[str, Any]:
|
||||
"""获取插件文件夹分组配置。"""
|
||||
return get_plugin_folder_service().get_or_empty()
|
||||
|
||||
|
||||
@router.post( # type: ignore[misc]
|
||||
"/folders", summary="保存插件文件夹配置", response_model=Response[None]
|
||||
)
|
||||
async def save_plugin_folders(
|
||||
folders: PluginFoldersData,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""保存插件文件夹分组配置。"""
|
||||
result = await get_plugin_folder_service().save(folders.root)
|
||||
return Response(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.post( # type: ignore[misc]
|
||||
"/folders/{folder_name}",
|
||||
summary="创建插件文件夹",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def create_plugin_folder(
|
||||
folder_name: str,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""创建新的插件文件夹。"""
|
||||
result = await get_plugin_folder_service().create(folder_name)
|
||||
return Response(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.delete( # type: ignore[misc]
|
||||
"/folders/{folder_name}",
|
||||
summary="删除插件文件夹",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def delete_plugin_folder(
|
||||
folder_name: str,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""删除插件文件夹。"""
|
||||
result = await get_plugin_folder_service().delete(folder_name)
|
||||
return Response(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.patch( # type: ignore[misc]
|
||||
"/folders/{folder_name}",
|
||||
summary="更新插件文件夹",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def update_plugin_folder(
|
||||
folder_name: str,
|
||||
folder: PluginFolderUpdateRequest,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""增量更新插件文件夹名称或展示配置。"""
|
||||
changes = folder.model_dump(
|
||||
by_alias=True,
|
||||
exclude={"new_name"},
|
||||
exclude_unset=True,
|
||||
)
|
||||
result = await get_plugin_folder_service().update_folder(
|
||||
folder_name,
|
||||
new_name=folder.new_name,
|
||||
changes=changes,
|
||||
)
|
||||
return Response(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.put( # type: ignore[misc]
|
||||
"/folders/{folder_name}/plugins",
|
||||
summary="更新文件夹中的插件",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def update_folder_plugins(
|
||||
folder_name: str,
|
||||
plugin_update: list[str] | PluginFolderPluginsUpdateRequest,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""条件替换指定文件夹中的插件列表,并兼容旧数组请求。"""
|
||||
if isinstance(plugin_update, list):
|
||||
plugin_ids = plugin_update
|
||||
expected_plugin_ids = None
|
||||
else:
|
||||
plugin_ids = plugin_update.plugins
|
||||
expected_plugin_ids = plugin_update.expected_plugins
|
||||
result = await get_plugin_folder_service().update_plugins(
|
||||
folder_name,
|
||||
plugin_ids,
|
||||
expected_plugin_ids,
|
||||
)
|
||||
return Response(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.put( # type: ignore[misc]
|
||||
"/folders/{folder_name}/plugins/{plugin_id}",
|
||||
summary="移动插件到文件夹",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def assign_plugin_to_folder(
|
||||
folder_name: str,
|
||||
plugin_id: str,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""把一个插件原子迁移到目标文件夹。"""
|
||||
result = await get_plugin_folder_service().assign_plugin(folder_name, plugin_id)
|
||||
return Response(success=result.success, message=result.message)
|
||||
|
||||
|
||||
@router.delete( # type: ignore[misc]
|
||||
"/folders/{folder_name}/plugins/{plugin_id}",
|
||||
summary="从文件夹移除插件",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def remove_plugin_from_folder(
|
||||
folder_name: str,
|
||||
plugin_id: str,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""只从指定文件夹移除一个插件。"""
|
||||
result = await get_plugin_folder_service().remove_plugin_from_folder(
|
||||
folder_name,
|
||||
plugin_id,
|
||||
)
|
||||
return Response(success=result.success, message=result.message)
|
||||
@@ -300,7 +300,7 @@ async def update_site(
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post(
|
||||
@router.post( # type: ignore[misc]
|
||||
"/cookiecloud",
|
||||
summary="CookieCloud同步",
|
||||
response_model=_SchemaResponse[None],
|
||||
@@ -325,7 +325,7 @@ async def cookie_cloud_sync(
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post(
|
||||
@router.post( # type: ignore[misc]
|
||||
"/reset",
|
||||
summary="重置站点",
|
||||
response_model=_SchemaResponse[None],
|
||||
|
||||
@@ -103,7 +103,9 @@ def directory_settings(
|
||||
return _SchemaResponse(success=True, data=results)
|
||||
|
||||
|
||||
@router.get("/options", summary="查询可用存储选项", response_model=List[_SchemaStorageOption])
|
||||
@router.get( # type: ignore[misc]
|
||||
"/options", summary="查询可用存储选项", response_model=List[_SchemaStorageOption]
|
||||
)
|
||||
def storage_options(
|
||||
_: ApiPrincipal = Depends(get_current_active_user),
|
||||
page: CompatiblePageParam = None,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""订阅维护命令端点。"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from app.api.dependencies.auth import (
|
||||
get_current_active_user,
|
||||
get_current_active_user_async,
|
||||
)
|
||||
from app.api.dependencies.subscription import (
|
||||
get_search_subscriptions_command,
|
||||
get_subscription_mutation_service,
|
||||
)
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.application.scheduling import get_scheduler
|
||||
from app.application.subscription.mutation import (
|
||||
SubscriptionActor,
|
||||
SubscriptionMutationService,
|
||||
)
|
||||
from app.application.subscription.search import (
|
||||
SearchSubscriptionsCommand,
|
||||
SubscribeSearchActor,
|
||||
)
|
||||
from app.schemas.response import Response
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/refresh",
|
||||
summary="刷新订阅(兼容入口)",
|
||||
response_model=Response[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post( # type: ignore[misc]
|
||||
"/refresh", summary="刷新订阅", response_model=Response[None]
|
||||
)
|
||||
def refresh_subscribes(
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""刷新所有订阅。"""
|
||||
if not current_user.is_superuser:
|
||||
return Response(success=False, message="订阅不存在")
|
||||
get_scheduler().start("subscribe_refresh")
|
||||
return Response(success=True)
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/reset/{subid}",
|
||||
summary="重置订阅(兼容入口)",
|
||||
response_model=Response[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post( # type: ignore[misc]
|
||||
"/reset/{subid}", summary="重置订阅", response_model=Response[None]
|
||||
)
|
||||
async def reset_subscribes(
|
||||
subid: int,
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""重置一个订阅。"""
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
change = await mutation.reset(subid, actor)
|
||||
if change:
|
||||
return Response(success=True)
|
||||
return Response(success=False, message="订阅不存在")
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/check",
|
||||
summary="刷新订阅 TMDB 信息(兼容入口)",
|
||||
response_model=Response[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post( # type: ignore[misc]
|
||||
"/check", summary="刷新订阅 TMDB 信息", response_model=Response[None]
|
||||
)
|
||||
def check_subscribes(
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""刷新订阅 TMDB 信息。"""
|
||||
if not current_user.is_superuser:
|
||||
return Response(success=False, message="订阅不存在")
|
||||
get_scheduler().start("subscribe_tmdb")
|
||||
return Response(success=True)
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/search",
|
||||
summary="搜索所有订阅(兼容入口)",
|
||||
response_model=Response[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post( # type: ignore[misc]
|
||||
"/search", summary="搜索所有订阅", response_model=Response[None]
|
||||
)
|
||||
async def search_subscribes(
|
||||
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""搜索当前用户可管理的全部订阅。"""
|
||||
await command.execute(
|
||||
SubscribeSearchActor(
|
||||
username=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
)
|
||||
return Response(success=True)
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/search/{subscribe_id}",
|
||||
summary="搜索订阅(兼容入口)",
|
||||
response_model=Response[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post( # type: ignore[misc]
|
||||
"/search/{subscribe_id}",
|
||||
summary="搜索订阅",
|
||||
response_model=Response[None],
|
||||
)
|
||||
async def search_subscribe(
|
||||
subscribe_id: int,
|
||||
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""根据订阅编号搜索一个订阅。"""
|
||||
found = await command.execute(
|
||||
SubscribeSearchActor(
|
||||
username=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
),
|
||||
subscribe_id=subscribe_id,
|
||||
)
|
||||
if not found:
|
||||
return Response(success=False, message="订阅不存在")
|
||||
return Response(success=True)
|
||||
+57
-236
@@ -21,11 +21,20 @@ from app.api.dependencies.auth import (
|
||||
from app.api.dependencies.subscription import (
|
||||
get_delete_subscribe_command,
|
||||
get_delete_subscriptions_by_identity_command,
|
||||
get_search_subscriptions_command,
|
||||
get_subscription_execution_status_service,
|
||||
get_subscription_mutation_service,
|
||||
get_subscription_query_service,
|
||||
)
|
||||
from app.api.endpoints.submaintenance import (
|
||||
check_subscribes,
|
||||
refresh_subscribes,
|
||||
reset_subscribes,
|
||||
search_subscribe,
|
||||
search_subscribes,
|
||||
)
|
||||
from app.api.endpoints.submaintenance import (
|
||||
router as subscribe_maintenance_router,
|
||||
)
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import (
|
||||
COLLECTION_TOTAL_HEADER,
|
||||
@@ -39,7 +48,6 @@ from app.application.configuration import (
|
||||
get_api_runtime_config_snapshot,
|
||||
get_configured_system_config,
|
||||
)
|
||||
from app.application.scheduling import get_scheduler
|
||||
from app.application.subscription.contract import SubscriptionQueryPort
|
||||
from app.application.subscription.delete import (
|
||||
DeleteSubscribeCommand,
|
||||
@@ -53,10 +61,6 @@ from app.application.subscription.mutation import (
|
||||
SubscriptionMutationService,
|
||||
)
|
||||
from app.application.subscription.query import SubscriptionQueryService
|
||||
from app.application.subscription.search import (
|
||||
SearchSubscriptionsCommand,
|
||||
SubscribeSearchActor,
|
||||
)
|
||||
from app.application.subscription.status import SubscriptionExecutionStatusService
|
||||
from app.chain.subscribe.facade import SubscribeChain
|
||||
from app.domain.context import MediaInfo
|
||||
@@ -84,6 +88,15 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
|
||||
from app.schemas.workflow import Subscribe as _SchemaSubscribe
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
router.include_router(subscribe_maintenance_router)
|
||||
|
||||
__all__ = [
|
||||
"check_subscribes",
|
||||
"refresh_subscribes",
|
||||
"reset_subscribes",
|
||||
"search_subscribe",
|
||||
"search_subscribes",
|
||||
]
|
||||
|
||||
|
||||
def _public_subscription_message(message: Optional[object]) -> str:
|
||||
@@ -101,9 +114,7 @@ async def _attach_execution_status(
|
||||
loader = getattr(status_service, "for_subscriptions", None)
|
||||
if not callable(loader):
|
||||
return subscribes
|
||||
statuses = await loader(
|
||||
tuple(item.id for item in subscribes if item.id is not None)
|
||||
)
|
||||
statuses = await loader(tuple(item.id for item in subscribes if item.id is not None))
|
||||
for subscribe in subscribes:
|
||||
if subscribe.id is not None and (status := statuses.get(subscribe.id)) is not None:
|
||||
subscribe.execution_status = _SchemaSubscriptionExecutionStatus.model_validate(status)
|
||||
@@ -140,9 +151,7 @@ def build_subscribe_event_payload(subscribe: Any) -> dict:
|
||||
return subscribe.to_dict()
|
||||
|
||||
|
||||
def can_access_subscribe(
|
||||
subscribe: Any, current_user: ApiPrincipal
|
||||
) -> bool:
|
||||
def can_access_subscribe(subscribe: Any, current_user: ApiPrincipal) -> bool:
|
||||
"""
|
||||
判断当前用户是否可访问订阅及其历史记录。
|
||||
|
||||
@@ -157,9 +166,7 @@ def can_access_subscribe(
|
||||
return bool(username) and username == current_user.name
|
||||
|
||||
|
||||
def select_accessible_subscribe(
|
||||
subscribes: List[Any], current_user: ApiPrincipal
|
||||
) -> Any:
|
||||
def select_accessible_subscribe(subscribes: List[Any], current_user: ApiPrincipal) -> Any:
|
||||
"""
|
||||
从候选订阅中选择当前用户可访问的第一条记录。
|
||||
"""
|
||||
@@ -170,15 +177,14 @@ def select_accessible_subscribe(
|
||||
|
||||
|
||||
def matches_subscribe_music_type(
|
||||
subscribe: Any,
|
||||
music_type: Optional[str],
|
||||
subscribe: Any,
|
||||
music_type: Optional[str],
|
||||
) -> bool:
|
||||
"""匹配订阅音乐实体,并把迁移前未标注类型的历史记录兼容为单曲。"""
|
||||
if not music_type:
|
||||
return True
|
||||
subscribe_music_type = getattr(subscribe, "music_type", None)
|
||||
return subscribe_music_type == music_type \
|
||||
or (music_type == MUSIC_ENTITY_RECORDING and subscribe_music_type is None)
|
||||
return subscribe_music_type == music_type or (music_type == MUSIC_ENTITY_RECORDING and subscribe_music_type is None)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -190,9 +196,7 @@ def matches_subscribe_music_type(
|
||||
async def read_subscribes(
|
||||
response: Response = None,
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
status_service: SubscriptionExecutionStatusService = Depends(
|
||||
get_subscription_execution_status_service
|
||||
),
|
||||
status_service: SubscriptionExecutionStatusService = Depends(get_subscription_execution_status_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
page: CompatiblePageParam = None,
|
||||
count: CompatibleCountParam = None,
|
||||
@@ -203,9 +207,7 @@ async def read_subscribes(
|
||||
username = None if current_user.is_superuser else current_user.name
|
||||
page, count = resolve_compatible_pagination(page, count)
|
||||
if response is not None:
|
||||
response.headers[COLLECTION_TOTAL_HEADER] = str(
|
||||
await query.count_public(username)
|
||||
)
|
||||
response.headers[COLLECTION_TOTAL_HEADER] = str(await query.count_public(username))
|
||||
subscribes = await query.list_public(username, page=page, count=count)
|
||||
return await _attach_execution_status(subscribes, status_service)
|
||||
|
||||
@@ -219,9 +221,7 @@ async def read_subscribes(
|
||||
async def list_subscribes(
|
||||
response: Response = None,
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
status_service: SubscriptionExecutionStatusService = Depends(
|
||||
get_subscription_execution_status_service
|
||||
),
|
||||
status_service: SubscriptionExecutionStatusService = Depends(get_subscription_execution_status_service),
|
||||
_: Annotated[str, Depends(verify_apitoken)] = None,
|
||||
page: CompatiblePageParam = None,
|
||||
count: CompatibleCountParam = None,
|
||||
@@ -255,11 +255,7 @@ async def create_subscribe(
|
||||
else:
|
||||
mtype = None
|
||||
# 非 TMDB 来源的标题可能自带季标记,入库前统一拆分。
|
||||
if (
|
||||
mtype != MediaType.MUSIC
|
||||
and normalize_media_source(subscribe_in.media_source)
|
||||
not in (None, MediaSource.TMDB)
|
||||
):
|
||||
if mtype != MediaType.MUSIC and normalize_media_source(subscribe_in.media_source) not in (None, MediaSource.TMDB):
|
||||
meta = MetaInfo(subscribe_in.name)
|
||||
subscribe_in.name = meta.name
|
||||
if subscribe_in.season is None:
|
||||
@@ -267,9 +263,7 @@ async def create_subscribe(
|
||||
# 空标题由订阅识别链按显式媒体身份补全,但调用契约始终使用字符串。
|
||||
title = subscribe_in.name or ""
|
||||
subscribe_dict = subscribe_in.to_public_write_payload()
|
||||
identity_fields = {"media_source", "media_id"}.intersection(
|
||||
subscribe_in.model_fields_set
|
||||
)
|
||||
identity_fields = {"media_source", "media_id"}.intersection(subscribe_in.model_fields_set)
|
||||
if identity_fields:
|
||||
media_source, media_id = resolve_media_identity(
|
||||
media_source=subscribe_in.media_source,
|
||||
@@ -297,11 +291,7 @@ async def create_subscribe(
|
||||
)
|
||||
return _SchemaResponse(
|
||||
success=bool(sid),
|
||||
message=(
|
||||
_public_subscription_message(message)
|
||||
if message
|
||||
else ""
|
||||
),
|
||||
message=(_public_subscription_message(message) if message else ""),
|
||||
data={"id": sid},
|
||||
)
|
||||
|
||||
@@ -324,9 +314,7 @@ async def update_subscribe(
|
||||
if not subscribe:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
subscribe_dict = subscribe_in.to_public_write_payload(exclude_unset=True)
|
||||
identity_fields = {"media_source", "media_id"}.intersection(
|
||||
subscribe_in.model_fields_set
|
||||
)
|
||||
identity_fields = {"media_source", "media_id"}.intersection(subscribe_in.model_fields_set)
|
||||
if identity_fields:
|
||||
media_source, media_id = resolve_media_identity(
|
||||
media_source=subscribe_in.media_source,
|
||||
@@ -349,13 +337,12 @@ async def update_subscribe(
|
||||
# 音乐实体与曲目总数来自识别链,编辑接口不得把专辑改成单曲而提前完成订阅。
|
||||
subscribe_dict["type"] = subscribe.type
|
||||
subscribe_dict["music_type"] = subscribe.music_type
|
||||
subscribe_dict["total_tracks"] = subscribe.total_tracks \
|
||||
if subscribe.music_type == MUSIC_ENTITY_ALBUM else None
|
||||
subscribe_dict["total_tracks"] = subscribe.total_tracks if subscribe.music_type == MUSIC_ENTITY_ALBUM else None
|
||||
total_episode_updated = "total_episode" in subscribe_in.model_fields_set
|
||||
if (
|
||||
total_episode_updated
|
||||
and subscribe_in.total_episode
|
||||
and subscribe_in.total_episode > (subscribe.total_episode or 0)
|
||||
total_episode_updated
|
||||
and subscribe_in.total_episode
|
||||
and subscribe_in.total_episode > (subscribe.total_episode or 0)
|
||||
):
|
||||
# 扩大目标范围时,新增加的集数尚无下载事实,应同步计入缺失集数。
|
||||
subscribe_dict["lack_episode"] = (subscribe.lack_episode or 0) + (
|
||||
@@ -426,137 +413,13 @@ async def subscribe_media_identity(
|
||||
return result if result else _SchemaSubscribe()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/refresh",
|
||||
summary="刷新订阅(兼容入口)",
|
||||
response_model=_SchemaResponse[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post("/refresh", summary="刷新订阅", response_model=_SchemaResponse[None])
|
||||
def refresh_subscribes(
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
"""
|
||||
if not current_user.is_superuser:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
get_scheduler().start("subscribe_refresh")
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/reset/{subid}",
|
||||
summary="重置订阅(兼容入口)",
|
||||
response_model=_SchemaResponse[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post("/reset/{subid}", summary="重置订阅", response_model=_SchemaResponse[None])
|
||||
async def reset_subscribes(
|
||||
subid: int,
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
重置订阅
|
||||
"""
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
change = await mutation.reset(subid, actor)
|
||||
if change:
|
||||
return _SchemaResponse(success=True)
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/check",
|
||||
summary="刷新订阅 TMDB 信息(兼容入口)",
|
||||
response_model=_SchemaResponse[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post("/check", summary="刷新订阅 TMDB 信息", response_model=_SchemaResponse[None])
|
||||
def check_subscribes(
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
刷新订阅 TMDB 信息
|
||||
"""
|
||||
if not current_user.is_superuser:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
get_scheduler().start("subscribe_tmdb")
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search",
|
||||
summary="搜索所有订阅(兼容入口)",
|
||||
response_model=_SchemaResponse[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post("/search", summary="搜索所有订阅", response_model=_SchemaResponse[None])
|
||||
async def search_subscribes(
|
||||
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
搜索所有订阅
|
||||
"""
|
||||
await command.execute(
|
||||
SubscribeSearchActor(
|
||||
username=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search/{subscribe_id}",
|
||||
summary="搜索订阅(兼容入口)",
|
||||
response_model=_SchemaResponse[None],
|
||||
include_in_schema=False,
|
||||
deprecated=True,
|
||||
)
|
||||
@router.post(
|
||||
"/search/{subscribe_id}",
|
||||
summary="搜索订阅",
|
||||
response_model=_SchemaResponse[None],
|
||||
)
|
||||
async def search_subscribe(
|
||||
subscribe_id: int,
|
||||
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
根据订阅编号搜索订阅
|
||||
"""
|
||||
found = await command.execute(
|
||||
SubscribeSearchActor(
|
||||
username=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
),
|
||||
subscribe_id=subscribe_id,
|
||||
)
|
||||
if not found:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.delete("/media/{media_id}", summary="删除订阅", response_model=_SchemaResponse[None])
|
||||
async def delete_subscribe_by_media_identity(
|
||||
media_id: str,
|
||||
media_source: MediaSource,
|
||||
season: Optional[int] = None,
|
||||
music_type: Optional[str] = None,
|
||||
command: DeleteSubscriptionsByIdentityCommand = Depends(
|
||||
get_delete_subscriptions_by_identity_command
|
||||
),
|
||||
command: DeleteSubscriptionsByIdentityCommand = Depends(get_delete_subscriptions_by_identity_command),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
@@ -575,9 +438,7 @@ async def delete_subscribe_by_media_identity(
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=_SchemaResponse[None]
|
||||
)
|
||||
@router.post("/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=_SchemaResponse[None])
|
||||
async def seerr_subscribe(
|
||||
request: Request,
|
||||
task_registry: Annotated[TaskRegistry, Depends(get_background_task_registry)],
|
||||
@@ -603,11 +464,7 @@ async def seerr_subscribe(
|
||||
if notification_type not in ["MEDIA_APPROVED", "MEDIA_AUTO_APPROVED"]:
|
||||
return _SchemaResponse(success=False, message="不支持的通知类型")
|
||||
subject = req_json.get("subject")
|
||||
media_type = (
|
||||
MediaType.MOVIE
|
||||
if req_json.get("media", {}).get("media_type") == "movie"
|
||||
else MediaType.TV
|
||||
)
|
||||
media_type = MediaType.MOVIE if req_json.get("media", {}).get("media_type") == "movie" else MediaType.TV
|
||||
tmdbId = req_json.get("media", {}).get("tmdbId")
|
||||
if not media_type or not tmdbId or not subject:
|
||||
return _SchemaResponse(success=False, message="请求参数不正确")
|
||||
@@ -630,11 +487,7 @@ async def seerr_subscribe(
|
||||
seasons = []
|
||||
for extra in req_json.get("extra", []):
|
||||
if extra.get("name") == "Requested Seasons":
|
||||
seasons = [
|
||||
int(str(sea).strip())
|
||||
for sea in extra.get("value").split(", ")
|
||||
if str(sea).isdigit()
|
||||
]
|
||||
seasons = [int(str(sea).strip()) for sea in extra.get("value").split(", ") if str(sea).isdigit()]
|
||||
break
|
||||
for season in seasons:
|
||||
resolve_background_task_registry(task_registry).create_sync(
|
||||
@@ -677,9 +530,7 @@ async def subscribe_history(
|
||||
username=username,
|
||||
)
|
||||
if response is not None:
|
||||
response.headers[COLLECTION_TOTAL_HEADER] = str(
|
||||
await query.count_history(mtype, username=username)
|
||||
)
|
||||
response.headers[COLLECTION_TOTAL_HEADER] = str(await query.count_history(mtype, username=username))
|
||||
return results
|
||||
|
||||
|
||||
@@ -780,9 +631,7 @@ async def user_subscribes(
|
||||
username: str,
|
||||
response: Response = None,
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
status_service: SubscriptionExecutionStatusService = Depends(
|
||||
get_subscription_execution_status_service
|
||||
),
|
||||
status_service: SubscriptionExecutionStatusService = Depends(get_subscription_execution_status_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
page: CompatiblePageParam = None,
|
||||
count: CompatibleCountParam = None,
|
||||
@@ -794,9 +643,7 @@ async def user_subscribes(
|
||||
return []
|
||||
page, count = resolve_compatible_pagination(page, count)
|
||||
if response is not None:
|
||||
response.headers[COLLECTION_TOTAL_HEADER] = str(
|
||||
await query.count_public(username)
|
||||
)
|
||||
response.headers[COLLECTION_TOTAL_HEADER] = str(await query.count_public(username))
|
||||
subscribes = await query.list_public(username, page=page, count=count)
|
||||
return await _attach_execution_status(subscribes, status_service)
|
||||
|
||||
@@ -848,29 +695,19 @@ async def subscribe_share(
|
||||
)
|
||||
return _SchemaResponse(
|
||||
success=state,
|
||||
message=(
|
||||
_public_subscription_message(errmsg)
|
||||
if errmsg
|
||||
else ""
|
||||
),
|
||||
message=(_public_subscription_message(errmsg) if errmsg else ""),
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/share/{share_id}", summary="删除分享", response_model=_SchemaResponse[None])
|
||||
async def subscribe_share_delete(
|
||||
share_id: int, _: _SchemaTokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
async def subscribe_share_delete(share_id: int, _: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
删除分享
|
||||
"""
|
||||
state, errmsg = await MoviePilotServerHelper.async_share_delete(share_id=share_id)
|
||||
return _SchemaResponse(
|
||||
success=state,
|
||||
message=(
|
||||
_public_subscription_message(errmsg)
|
||||
if errmsg
|
||||
else ""
|
||||
),
|
||||
message=(_public_subscription_message(errmsg) if errmsg else ""),
|
||||
)
|
||||
|
||||
|
||||
@@ -887,16 +724,16 @@ async def subscribe_fork(
|
||||
for key in list(sub_dict.keys()):
|
||||
if not hasattr(_SchemaSubscribe(), key):
|
||||
sub_dict.pop(key)
|
||||
result = await create_subscribe(
|
||||
subscribe_in=_SchemaSubscribe(**sub_dict), current_user=current_user
|
||||
)
|
||||
result = await create_subscribe(subscribe_in=_SchemaSubscribe(**sub_dict), current_user=current_user)
|
||||
if result.success:
|
||||
await MoviePilotServerHelper.async_sub_fork(share_id=sub.id)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/follow", summary="查询已Follow的订阅分享人", response_model=List[str])
|
||||
async def followed_subscribers(_: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None) -> Any:
|
||||
async def followed_subscribers(
|
||||
_: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None
|
||||
) -> Any:
|
||||
"""
|
||||
查询已Follow的订阅分享人
|
||||
"""
|
||||
@@ -904,42 +741,30 @@ async def followed_subscribers(_: _SchemaTokenPayload = Depends(verify_token), p
|
||||
|
||||
|
||||
@router.post("/follow", summary="Follow订阅分享人", response_model=_SchemaResponse[None])
|
||||
async def follow_subscriber(
|
||||
share_uid: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
async def follow_subscriber(share_uid: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
Follow订阅分享人
|
||||
"""
|
||||
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
|
||||
if share_uid and share_uid not in subscribers:
|
||||
subscribers.append(share_uid)
|
||||
await get_configured_system_config().async_set(
|
||||
SystemConfigKey.FollowSubscribers, subscribers
|
||||
)
|
||||
await get_configured_system_config().async_set(SystemConfigKey.FollowSubscribers, subscribers)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/follow", summary="取消Follow订阅分享人", response_model=_SchemaResponse[None]
|
||||
)
|
||||
async def unfollow_subscriber(
|
||||
share_uid: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
@router.delete("/follow", summary="取消Follow订阅分享人", response_model=_SchemaResponse[None])
|
||||
async def unfollow_subscriber(share_uid: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
取消Follow订阅分享人
|
||||
"""
|
||||
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
|
||||
if share_uid and share_uid in subscribers:
|
||||
subscribers.remove(share_uid)
|
||||
await get_configured_system_config().async_set(
|
||||
SystemConfigKey.FollowSubscribers, subscribers
|
||||
)
|
||||
await get_configured_system_config().async_set(SystemConfigKey.FollowSubscribers, subscribers)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/shares", summary="查询分享的订阅", response_model=List[_SchemaSubscribeShare]
|
||||
)
|
||||
@router.get("/shares", summary="查询分享的订阅", response_model=List[_SchemaSubscribeShare])
|
||||
async def subscribe_shares(
|
||||
name: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
@@ -993,11 +818,7 @@ async def read_subscribe(
|
||||
if not subscribe_id:
|
||||
return _SchemaSubscribe()
|
||||
subscribe = await query.get_public(subscribe_id)
|
||||
return (
|
||||
subscribe
|
||||
if subscribe and can_access_subscribe(subscribe, current_user)
|
||||
else _SchemaSubscribe()
|
||||
)
|
||||
return subscribe if subscribe and can_access_subscribe(subscribe, current_user) else _SchemaSubscribe()
|
||||
|
||||
|
||||
@router.delete(
|
||||
|
||||
+13
-58
@@ -7,7 +7,7 @@ from typing import Annotated, Any, Optional, Union
|
||||
|
||||
import anyio
|
||||
import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用
|
||||
from fastapi import Body, Depends, Header, HTTPException, Query, Request, Response, status
|
||||
from fastapi import Body, Depends, Header, HTTPException, Query, Request, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token
|
||||
@@ -17,6 +17,7 @@ from app.api.dependencies.auth import (
|
||||
get_current_active_superuser_async,
|
||||
get_current_active_user_async,
|
||||
)
|
||||
from app.api.endpoints.identifier import router as system_identifiers_router
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import CompatibleCountParam, CompatiblePageParam, ResponseAPIRouter
|
||||
from app.application.backup import DatabaseBackupInProgressError
|
||||
@@ -32,7 +33,7 @@ from app.application.network import get_configured_network_test_service
|
||||
from app.application.rules import RuleHelper
|
||||
from app.application.scheduling import get_scheduler
|
||||
from app.application.security.url import SecurityUtils
|
||||
from app.application.settings import SystemSettingConflictError, SystemSettingsService
|
||||
from app.application.settings import SystemSettingsService
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
|
||||
from app.application.system import LogFileData, LogNotFoundError
|
||||
from app.chain.media import MediaChain
|
||||
@@ -53,7 +54,6 @@ from app.schemas.common import JsonObjectList as _SchemaJsonObjectList
|
||||
from app.schemas.common import TimeData as _SchemaTimeData
|
||||
from app.schemas.common import ValueData as _SchemaValueData
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.schemas.system import CustomIdentifiersUpdateRequest as _SchemaCustomIdentifiersUpdateRequest
|
||||
from app.schemas.system import DatabaseBackupArtifactData as _SchemaDatabaseBackupArtifactData
|
||||
from app.schemas.system import DatabaseBackupVerificationData as _SchemaDatabaseBackupVerificationData
|
||||
from app.schemas.system import NetTestTarget as _SchemaNetTestTarget
|
||||
@@ -70,6 +70,7 @@ from app.schemas.types import SystemConfigKey
|
||||
from app.startup.composition.context import HostRuntime
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
router.include_router(system_identifiers_router)
|
||||
|
||||
_PUBLIC_SYSTEM_CONFIG_KEYS = {
|
||||
item.value: item
|
||||
@@ -699,58 +700,6 @@ async def update_settings(
|
||||
return _SchemaResponse(success=True, message=data.get("message"), data=data)
|
||||
|
||||
|
||||
@router.get( # type: ignore[misc]
|
||||
"/identifiers",
|
||||
summary="查询自定义识别词",
|
||||
response_model=_SchemaResponse[_SchemaJsonObject],
|
||||
)
|
||||
async def query_custom_identifiers(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse[Any]:
|
||||
"""返回完整的自定义识别词列表。"""
|
||||
identifiers = [
|
||||
item
|
||||
for item in (get_configured_system_config().get(SystemConfigKey.CustomIdentifiers) or [])
|
||||
if isinstance(item, str)
|
||||
]
|
||||
return _SchemaResponse(
|
||||
success=True,
|
||||
data={"count": len(identifiers), "identifiers": identifiers},
|
||||
)
|
||||
|
||||
|
||||
@router.post( # type: ignore[misc]
|
||||
"/identifiers",
|
||||
summary="更新自定义识别词",
|
||||
response_model=_SchemaResponse[_SchemaJsonObject],
|
||||
)
|
||||
async def update_custom_identifiers(
|
||||
payload: _SchemaCustomIdentifiersUpdateRequest,
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
runtime: HostRuntime = Depends(get_host_runtime),
|
||||
) -> _SchemaResponse[Any]:
|
||||
"""完整替换自定义识别词,并可拒绝基于过期快照的覆盖。"""
|
||||
identifiers = list(payload.identifiers)
|
||||
try:
|
||||
data = await SystemSettingsService(
|
||||
get_runtime_settings(),
|
||||
get_configured_system_config(),
|
||||
runtime.system.publish_config_changed,
|
||||
).update(
|
||||
setting_key=SystemConfigKey.CustomIdentifiers.value,
|
||||
value=identifiers or None,
|
||||
expected_value=payload.expected_identifiers,
|
||||
enforce_expected_value=payload.expected_identifiers is not None,
|
||||
)
|
||||
except SystemSettingConflictError as error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(error),
|
||||
) from error
|
||||
data.update({"count": len(identifiers), "identifiers": identifiers})
|
||||
return _SchemaResponse(success=True, message=data.get("message"), data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/message",
|
||||
summary="实时消息",
|
||||
@@ -976,7 +925,9 @@ def ruletest(
|
||||
summary="获取网络测试目标",
|
||||
response_model=_SchemaResponse[list[_SchemaNetTestTarget]],
|
||||
)
|
||||
async def nettest_targets(_: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None):
|
||||
async def nettest_targets(
|
||||
_: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None
|
||||
):
|
||||
"""
|
||||
获取网络测试目标。
|
||||
|
||||
@@ -1106,7 +1057,9 @@ def check_system_update(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update/download", summary="后台下载系统更新", response_model=_SchemaResponse[_SchemaSystemUpdateStatus],
|
||||
"/update/download",
|
||||
summary="后台下载系统更新",
|
||||
response_model=_SchemaResponse[_SchemaSystemUpdateStatus],
|
||||
)
|
||||
def download_system_update(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
@@ -1119,7 +1072,9 @@ def download_system_update(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update/install", summary="确认重启安装系统更新", response_model=_SchemaResponse[None],
|
||||
"/update/install",
|
||||
summary="确认重启安装系统更新",
|
||||
response_model=_SchemaResponse[None],
|
||||
)
|
||||
def install_system_update(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
|
||||
@@ -154,7 +154,7 @@ class PluginRuntimeSummary(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class PluginRuntimeCommandCapability(BaseModel):
|
||||
class PluginRuntimeCommandCapability(BaseModel): # type: ignore[misc]
|
||||
"""插件运行时注册命令的安全只读投影。"""
|
||||
|
||||
cmd: str = Field(description="命令标识")
|
||||
@@ -162,14 +162,14 @@ class PluginRuntimeCommandCapability(BaseModel):
|
||||
plugin_id: Optional[str] = Field(default=None, description="注册命令的插件 ID")
|
||||
|
||||
|
||||
class PluginRuntimeActionCapability(BaseModel):
|
||||
class PluginRuntimeActionCapability(BaseModel): # type: ignore[misc]
|
||||
"""插件运行时注册动作的安全只读投影。"""
|
||||
|
||||
id: str = Field(description="动作标识")
|
||||
name: Optional[str] = Field(default=None, description="动作名称")
|
||||
|
||||
|
||||
class PluginRuntimeActionGroup(BaseModel):
|
||||
class PluginRuntimeActionGroup(BaseModel): # type: ignore[misc]
|
||||
"""按插件归组的运行时动作投影。"""
|
||||
|
||||
plugin_id: Optional[str] = Field(default=None, description="注册动作的插件 ID")
|
||||
@@ -177,7 +177,7 @@ class PluginRuntimeActionGroup(BaseModel):
|
||||
actions: List[PluginRuntimeActionCapability] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PluginRuntimeServiceCapability(BaseModel):
|
||||
class PluginRuntimeServiceCapability(BaseModel): # type: ignore[misc]
|
||||
"""插件定时服务的安全只读投影。"""
|
||||
|
||||
id: str = Field(description="服务标识")
|
||||
@@ -185,7 +185,7 @@ class PluginRuntimeServiceCapability(BaseModel):
|
||||
trigger: Optional[str] = Field(default=None, description="定时触发器说明")
|
||||
|
||||
|
||||
class PluginRuntimeCapabilities(BaseModel):
|
||||
class PluginRuntimeCapabilities(BaseModel): # type: ignore[misc]
|
||||
"""插件命令、动作和定时服务的公共安全能力快照。"""
|
||||
|
||||
commands: List[PluginRuntimeCommandCapability] = Field(default_factory=list)
|
||||
@@ -193,7 +193,7 @@ class PluginRuntimeCapabilities(BaseModel):
|
||||
services: List[PluginRuntimeServiceCapability] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PluginDataKeySummary(BaseModel):
|
||||
class PluginDataKeySummary(BaseModel): # type: ignore[misc]
|
||||
"""单个插件持久化键的不含值诊断摘要。"""
|
||||
|
||||
key: str = Field(description="持久化数据键")
|
||||
@@ -208,7 +208,7 @@ class PluginDataKeySummary(BaseModel):
|
||||
sensitive: bool = Field(description="键名是否符合凭据字段规则")
|
||||
|
||||
|
||||
class PluginDataSummary(BaseModel):
|
||||
class PluginDataSummary(BaseModel): # type: ignore[misc]
|
||||
"""插件持久化数据的不含原值诊断摘要。"""
|
||||
|
||||
plugin_id: str = Field(description="插件 ID")
|
||||
@@ -490,7 +490,7 @@ class PluginFoldersData(RootModel[Dict[str, Union[List[str], PluginFolderConfigD
|
||||
"""插件文件夹与插件配置映射,兼容旧版数组格式与新版对象格式。"""
|
||||
|
||||
|
||||
class PluginFolderUpdateRequest(BaseModel):
|
||||
class PluginFolderUpdateRequest(BaseModel): # type: ignore[misc]
|
||||
"""插件文件夹名称和展示字段的增量更新请求。"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
@@ -521,7 +521,7 @@ class PluginFolderUpdateRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class PluginFolderPluginsUpdateRequest(BaseModel):
|
||||
class PluginFolderPluginsUpdateRequest(BaseModel): # type: ignore[misc]
|
||||
"""插件文件夹成员顺序的条件替换请求。"""
|
||||
|
||||
plugins: List[str] = Field(description="Ordered installed plugin IDs assigned to this folder.")
|
||||
|
||||
@@ -26,7 +26,7 @@ class StorageLoginStatusData(BaseModel):
|
||||
tip: str = Field(description="状态提示")
|
||||
|
||||
|
||||
class StorageOption(BaseModel):
|
||||
class StorageOption(BaseModel): # type: ignore[misc]
|
||||
"""前端选择控件可安全消费的存储摘要。"""
|
||||
|
||||
name: str = Field(description="存储显示名称")
|
||||
|
||||
@@ -754,8 +754,8 @@ flowchart LR
|
||||
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| Python 模块 | 968 |
|
||||
| 内部导入边 | 8,161 |
|
||||
| Python 模块 | 972 |
|
||||
| 内部导入边 | 8,206 |
|
||||
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
||||
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
||||
| Direct egress | 53(债务已清零,53 条精确 containment) |
|
||||
|
||||
@@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
|
||||
|
||||
| 指标 | 当前值 | 解释 |
|
||||
|---|---:|---|
|
||||
| 宿主 Python 模块 / 内部依赖边 | 969 / 8,161 | `dependency-baseline.json` 当前快照 |
|
||||
| 宿主 Python 模块 / 内部依赖边 | 972 / 8,206 | `dependency-baseline.json` 当前快照 |
|
||||
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||
@@ -102,8 +102,8 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
|
||||
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
|
||||
| Python 源码量 | 305,884 行 | 排除 `app/plugins/**`;61 个文件超过 1,000 行,11 个超过 2,000 行 |
|
||||
| 长方法 | 290 个超过 80 行 | AST 统计排除 `app/plugins/**`;65 个超过 150 行,21 个超过 250 行 |
|
||||
| 全量 mypy 历史债务 | 9,538 / 513 文件 | Agent API 重构后的现状基线;canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降 |
|
||||
| Ruff 历史诊断 | 548 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 全量 mypy 历史债务 | 9,528 / 513 文件 | Agent API 重构后的现状基线;canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降 |
|
||||
| Ruff 历史诊断 | 547 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率固定基线 | Application 80.00%,Domain 80.00% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
|
||||
### 3.3 热点文件
|
||||
|
||||
+53
-5
@@ -1089,8 +1089,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 8161,
|
||||
"edge_sha256": "25ffe69a9b335cc3eeb13ed5e6d4b55eda256eae747df6d0612c6ab5145786b3",
|
||||
"edge_count": 8206,
|
||||
"edge_sha256": "c1bac8193a102557080e2ff07d49b8c7f4e34ad09831ffee1ac2d0cce5b89463",
|
||||
"edges": [
|
||||
"app -> app.foundation",
|
||||
"app -> app.foundation.environment",
|
||||
@@ -2285,6 +2285,23 @@
|
||||
"app.api.endpoints.history -> app.schemas.history",
|
||||
"app.api.endpoints.history -> app.schemas.response",
|
||||
"app.api.endpoints.history -> app.schemas.token",
|
||||
"app.api.endpoints.identifier -> app.api",
|
||||
"app.api.endpoints.identifier -> app.api.context",
|
||||
"app.api.endpoints.identifier -> app.api.dependencies",
|
||||
"app.api.endpoints.identifier -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.identifier -> app.api.principal",
|
||||
"app.api.endpoints.identifier -> app.api.response",
|
||||
"app.api.endpoints.identifier -> app.application",
|
||||
"app.api.endpoints.identifier -> app.application.configuration",
|
||||
"app.api.endpoints.identifier -> app.application.settings",
|
||||
"app.api.endpoints.identifier -> app.schemas",
|
||||
"app.api.endpoints.identifier -> app.schemas.common",
|
||||
"app.api.endpoints.identifier -> app.schemas.response",
|
||||
"app.api.endpoints.identifier -> app.schemas.system",
|
||||
"app.api.endpoints.identifier -> app.schemas.types",
|
||||
"app.api.endpoints.identifier -> app.startup",
|
||||
"app.api.endpoints.identifier -> app.startup.composition",
|
||||
"app.api.endpoints.identifier -> app.startup.composition.context",
|
||||
"app.api.endpoints.llm -> app.agent",
|
||||
"app.api.endpoints.llm -> app.agent.llm",
|
||||
"app.api.endpoints.llm -> app.agent.llm.auth",
|
||||
@@ -2510,6 +2527,8 @@
|
||||
"app.api.endpoints.plugin -> app.api.dependencies",
|
||||
"app.api.endpoints.plugin -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.plugin -> app.api.dependencies.plugin",
|
||||
"app.api.endpoints.plugin -> app.api.endpoints",
|
||||
"app.api.endpoints.plugin -> app.api.endpoints.pluginfolder",
|
||||
"app.api.endpoints.plugin -> app.api.principal",
|
||||
"app.api.endpoints.plugin -> app.api.response",
|
||||
"app.api.endpoints.plugin -> app.application",
|
||||
@@ -2544,6 +2563,17 @@
|
||||
"app.api.endpoints.plugin -> app.startup",
|
||||
"app.api.endpoints.plugin -> app.startup.composition",
|
||||
"app.api.endpoints.plugin -> app.startup.composition.context",
|
||||
"app.api.endpoints.pluginfolder -> app.api",
|
||||
"app.api.endpoints.pluginfolder -> app.api.dependencies",
|
||||
"app.api.endpoints.pluginfolder -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.pluginfolder -> app.api.principal",
|
||||
"app.api.endpoints.pluginfolder -> app.api.response",
|
||||
"app.api.endpoints.pluginfolder -> app.application",
|
||||
"app.api.endpoints.pluginfolder -> app.application.plugin",
|
||||
"app.api.endpoints.pluginfolder -> app.application.plugin.folders",
|
||||
"app.api.endpoints.pluginfolder -> app.schemas",
|
||||
"app.api.endpoints.pluginfolder -> app.schemas.plugin",
|
||||
"app.api.endpoints.pluginfolder -> app.schemas.response",
|
||||
"app.api.endpoints.recommend -> app.adapters",
|
||||
"app.api.endpoints.recommend -> app.adapters.web",
|
||||
"app.api.endpoints.recommend -> app.adapters.web.security",
|
||||
@@ -2685,6 +2715,19 @@
|
||||
"app.api.endpoints.subexecution -> app.schemas",
|
||||
"app.api.endpoints.subexecution -> app.schemas.response",
|
||||
"app.api.endpoints.subexecution -> app.schemas.subscribe",
|
||||
"app.api.endpoints.submaintenance -> app.api",
|
||||
"app.api.endpoints.submaintenance -> app.api.dependencies",
|
||||
"app.api.endpoints.submaintenance -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.submaintenance -> app.api.dependencies.subscription",
|
||||
"app.api.endpoints.submaintenance -> app.api.principal",
|
||||
"app.api.endpoints.submaintenance -> app.api.response",
|
||||
"app.api.endpoints.submaintenance -> app.application",
|
||||
"app.api.endpoints.submaintenance -> app.application.scheduling",
|
||||
"app.api.endpoints.submaintenance -> app.application.subscription",
|
||||
"app.api.endpoints.submaintenance -> app.application.subscription.mutation",
|
||||
"app.api.endpoints.submaintenance -> app.application.subscription.search",
|
||||
"app.api.endpoints.submaintenance -> app.schemas",
|
||||
"app.api.endpoints.submaintenance -> app.schemas.response",
|
||||
"app.api.endpoints.subscribe -> app.adapters",
|
||||
"app.api.endpoints.subscribe -> app.adapters.external",
|
||||
"app.api.endpoints.subscribe -> app.adapters.external.server",
|
||||
@@ -2696,18 +2739,18 @@
|
||||
"app.api.endpoints.subscribe -> app.api.dependencies",
|
||||
"app.api.endpoints.subscribe -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.subscribe -> app.api.dependencies.subscription",
|
||||
"app.api.endpoints.subscribe -> app.api.endpoints",
|
||||
"app.api.endpoints.subscribe -> app.api.endpoints.submaintenance",
|
||||
"app.api.endpoints.subscribe -> app.api.principal",
|
||||
"app.api.endpoints.subscribe -> app.api.response",
|
||||
"app.api.endpoints.subscribe -> app.application",
|
||||
"app.api.endpoints.subscribe -> app.application.configuration",
|
||||
"app.api.endpoints.subscribe -> app.application.scheduling",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.contract",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.delete",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.identity",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.mutation",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.query",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.search",
|
||||
"app.api.endpoints.subscribe -> app.application.subscription.status",
|
||||
"app.api.endpoints.subscribe -> app.chain",
|
||||
"app.api.endpoints.subscribe -> app.chain.subscribe",
|
||||
@@ -2736,6 +2779,8 @@
|
||||
"app.api.endpoints.system -> app.api.context",
|
||||
"app.api.endpoints.system -> app.api.dependencies",
|
||||
"app.api.endpoints.system -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.system -> app.api.endpoints",
|
||||
"app.api.endpoints.system -> app.api.endpoints.identifier",
|
||||
"app.api.endpoints.system -> app.api.principal",
|
||||
"app.api.endpoints.system -> app.api.response",
|
||||
"app.api.endpoints.system -> app.application",
|
||||
@@ -9254,7 +9299,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 969,
|
||||
"module_count": 972,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -9414,6 +9459,7 @@
|
||||
"app.api.endpoints.douban",
|
||||
"app.api.endpoints.download",
|
||||
"app.api.endpoints.history",
|
||||
"app.api.endpoints.identifier",
|
||||
"app.api.endpoints.llm",
|
||||
"app.api.endpoints.login",
|
||||
"app.api.endpoints.mcp",
|
||||
@@ -9425,12 +9471,14 @@
|
||||
"app.api.endpoints.notification",
|
||||
"app.api.endpoints.openai",
|
||||
"app.api.endpoints.plugin",
|
||||
"app.api.endpoints.pluginfolder",
|
||||
"app.api.endpoints.recommend",
|
||||
"app.api.endpoints.rule",
|
||||
"app.api.endpoints.search",
|
||||
"app.api.endpoints.site",
|
||||
"app.api.endpoints.storage",
|
||||
"app.api.endpoints.subexecution",
|
||||
"app.api.endpoints.submaintenance",
|
||||
"app.api.endpoints.subscribe",
|
||||
"app.api.endpoints.system",
|
||||
"app.api.endpoints.tmdb",
|
||||
|
||||
+3
-3
@@ -511,11 +511,11 @@
|
||||
"app/api/endpoints/plugin.py": {
|
||||
"assignment": 1,
|
||||
"import-untyped": 1,
|
||||
"misc": 33,
|
||||
"misc": 29,
|
||||
"no-any-return": 5,
|
||||
"no-untyped-call": 1,
|
||||
"no-untyped-def": 2,
|
||||
"type-arg": 10
|
||||
"type-arg": 9
|
||||
},
|
||||
"app/api/endpoints/recommend.py": {
|
||||
"attr-defined": 2,
|
||||
@@ -549,7 +549,7 @@
|
||||
"arg-type": 11,
|
||||
"assignment": 3,
|
||||
"attr-defined": 2,
|
||||
"misc": 28,
|
||||
"misc": 23,
|
||||
"no-any-return": 1,
|
||||
"no-untyped-call": 1,
|
||||
"no-untyped-def": 1,
|
||||
|
||||
@@ -635,9 +635,6 @@
|
||||
"tests/test_agent_tool_timeouts.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_api_authorization.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_async_db_pooling.py": {
|
||||
"I001": 1
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"repeat": 3,
|
||||
"targets": {
|
||||
"app.startup.lifecycle": {
|
||||
"loaded_app_module_count": 535,
|
||||
"loaded_app_module_count": 536,
|
||||
"max_ms": 1293.338,
|
||||
"median_ms": 1156.239,
|
||||
"min_ms": 1102.806,
|
||||
@@ -17,7 +17,7 @@
|
||||
]
|
||||
},
|
||||
"app.factory": {
|
||||
"loaded_app_module_count": 547,
|
||||
"loaded_app_module_count": 548,
|
||||
"max_ms": 1127.911,
|
||||
"median_ms": 1122.382,
|
||||
"min_ms": 1119.221,
|
||||
@@ -28,7 +28,7 @@
|
||||
]
|
||||
},
|
||||
"app.main": {
|
||||
"loaded_app_module_count": 549,
|
||||
"loaded_app_module_count": 550,
|
||||
"max_ms": 1188.652,
|
||||
"median_ms": 1183.509,
|
||||
"min_ms": 1174.522,
|
||||
|
||||
@@ -9,6 +9,7 @@ from starlette.responses import Response
|
||||
|
||||
from app import schemas
|
||||
from app.api.endpoints import plugin as plugin_endpoint
|
||||
from app.api.endpoints import pluginfolder as plugin_folders_endpoint
|
||||
from app.api.endpoints.plugin import (
|
||||
plugin_capabilities,
|
||||
plugin_data_summary,
|
||||
@@ -1265,7 +1266,7 @@ def test_sealed_http_folder_update_rejects_before_config_access(monkeypatch):
|
||||
config_provider,
|
||||
)
|
||||
|
||||
result = asyncio.run(plugin_endpoint.update_folder_plugins("常用", ["DemoPlugin"], None))
|
||||
result = asyncio.run(plugin_folders_endpoint.update_folder_plugins("常用", ["DemoPlugin"], None))
|
||||
|
||||
assert result.success is False
|
||||
assert "停机阶段" in result.message
|
||||
@@ -1279,10 +1280,14 @@ def test_plugin_folder_incremental_endpoints_delegate_structured_payloads(monkey
|
||||
service.update_plugins = AsyncMock(return_value=SimpleNamespace(success=True, message=""))
|
||||
service.assign_plugin = AsyncMock(return_value=SimpleNamespace(success=True, message=""))
|
||||
service.remove_plugin_from_folder = AsyncMock(return_value=SimpleNamespace(success=True, message=""))
|
||||
monkeypatch.setattr(plugin_endpoint, "get_plugin_folder_service", lambda: service)
|
||||
monkeypatch.setattr(
|
||||
plugin_folders_endpoint,
|
||||
"get_plugin_folder_service",
|
||||
lambda: service,
|
||||
)
|
||||
|
||||
patch_result = asyncio.run(
|
||||
plugin_endpoint.update_plugin_folder(
|
||||
plugin_folders_endpoint.update_plugin_folder(
|
||||
"常用",
|
||||
schemas.PluginFolderUpdateRequest(
|
||||
new_name="工具",
|
||||
@@ -1293,7 +1298,7 @@ def test_plugin_folder_incremental_endpoints_delegate_structured_payloads(monkey
|
||||
)
|
||||
)
|
||||
members_result = asyncio.run(
|
||||
plugin_endpoint.update_folder_plugins(
|
||||
plugin_folders_endpoint.update_folder_plugins(
|
||||
"工具",
|
||||
schemas.PluginFolderPluginsUpdateRequest(
|
||||
plugins=["DemoPlugin"],
|
||||
@@ -1302,8 +1307,8 @@ def test_plugin_folder_incremental_endpoints_delegate_structured_payloads(monkey
|
||||
None,
|
||||
)
|
||||
)
|
||||
assign_result = asyncio.run(plugin_endpoint.assign_plugin_to_folder("工具", "DemoPlugin", None))
|
||||
remove_result = asyncio.run(plugin_endpoint.remove_plugin_from_folder("工具", "DemoPlugin", None))
|
||||
assign_result = asyncio.run(plugin_folders_endpoint.assign_plugin_to_folder("工具", "DemoPlugin", None))
|
||||
remove_result = asyncio.run(plugin_folders_endpoint.remove_plugin_from_folder("工具", "DemoPlugin", None))
|
||||
|
||||
assert all(result.success for result in (patch_result, members_result, assign_result, remove_result))
|
||||
service.update_folder.assert_awaited_once_with(
|
||||
@@ -1327,10 +1332,14 @@ def test_plugin_folder_member_endpoint_keeps_legacy_array_request(monkeypatch):
|
||||
"""旧客户端发送裸插件数组时仍应按无条件替换语义工作。"""
|
||||
service = MagicMock()
|
||||
service.update_plugins = AsyncMock(return_value=SimpleNamespace(success=True, message=""))
|
||||
monkeypatch.setattr(plugin_endpoint, "get_plugin_folder_service", lambda: service)
|
||||
monkeypatch.setattr(
|
||||
plugin_folders_endpoint,
|
||||
"get_plugin_folder_service",
|
||||
lambda: service,
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
plugin_endpoint.update_folder_plugins(
|
||||
plugin_folders_endpoint.update_folder_plugins(
|
||||
"常用",
|
||||
["DemoPlugin"],
|
||||
None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -819,9 +819,7 @@ class TestSubscribeEndpoint:
|
||||
1,
|
||||
3,
|
||||
)
|
||||
history_repository.async_count_by_type.assert_awaited_once_with(
|
||||
MediaType.MOVIE.value
|
||||
)
|
||||
history_repository.async_count_by_type.assert_awaited_once_with(MediaType.MOVIE.value)
|
||||
history_repository.async_list_by_type_and_username.assert_not_awaited()
|
||||
|
||||
def test_delete_subscribe_history_rejects_other_user(self):
|
||||
@@ -859,7 +857,7 @@ class TestSubscribeEndpoint:
|
||||
superuser = _EndpointUser(name="admin", is_superuser=True)
|
||||
|
||||
for endpoint in [refresh_subscribes, check_subscribes]:
|
||||
with patch("app.api.endpoints.subscribe.get_scheduler") as scheduler:
|
||||
with patch("app.api.endpoints.submaintenance.get_scheduler") as scheduler:
|
||||
response = endpoint(current_user=regular_user)
|
||||
|
||||
assert not response.success
|
||||
@@ -870,7 +868,7 @@ class TestSubscribeEndpoint:
|
||||
(refresh_subscribes, "subscribe_refresh"),
|
||||
(check_subscribes, "subscribe_tmdb"),
|
||||
]:
|
||||
with patch("app.api.endpoints.subscribe.get_scheduler") as scheduler:
|
||||
with patch("app.api.endpoints.submaintenance.get_scheduler") as scheduler:
|
||||
response = endpoint(current_user=superuser)
|
||||
|
||||
assert response.success
|
||||
@@ -1390,9 +1388,7 @@ class _SubscriptionHistoryRepositoryFake:
|
||||
self.async_list_by_type = AsyncMock(side_effect=self._async_list_by_type)
|
||||
self.async_list_by_type_and_username = AsyncMock(side_effect=self._async_list_by_type_and_username)
|
||||
self.async_count_by_type = AsyncMock(side_effect=self._async_count_by_type)
|
||||
self.async_count_by_type_and_username = AsyncMock(
|
||||
side_effect=self._async_count_by_type_and_username
|
||||
)
|
||||
self.async_count_by_type_and_username = AsyncMock(side_effect=self._async_count_by_type_and_username)
|
||||
self.stage_delete = AsyncMock(side_effect=self._stage_delete)
|
||||
|
||||
async def _async_get(self, history_id: int) -> SubscriptionHistorySnapshot | None:
|
||||
@@ -1432,10 +1428,7 @@ class _SubscriptionHistoryRepositoryFake:
|
||||
username: str,
|
||||
) -> int:
|
||||
"""异步统计指定类型和 owner 的历史快照。"""
|
||||
return sum(
|
||||
row.type == mtype and row.username == username
|
||||
for row in self.rows.values()
|
||||
)
|
||||
return sum(row.type == mtype and row.username == username for row in self.rows.values())
|
||||
|
||||
async def _stage_delete(self, history_id: int) -> None:
|
||||
"""暂存删除等价为从内存集合移除历史快照。"""
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
import app.api.endpoints.system as system_endpoint
|
||||
import app.api.endpoints.identifier as system_endpoint
|
||||
from app.application.settings import SystemSettingConflictError
|
||||
from app.schemas.system import CustomIdentifiersUpdateRequest
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
Reference in New Issue
Block a user