mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: expand runtime contracts and debt ratchets
This commit is contained in:
@@ -16,7 +16,8 @@ from app.schemas.response import Response as _SchemaResponse
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.dashboard import DashboardChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.runtime.config import settings
|
||||
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
|
||||
from app.application.configuration import ApiRuntimeConfig
|
||||
from app.adapters.web.security.access import verify_apitoken
|
||||
from app.api.dependencies.auth import get_current_active_superuser
|
||||
from app.api.dependencies.history import get_dashboard_query_service
|
||||
@@ -53,7 +54,11 @@ def _build_storage() -> _SchemaStorage:
|
||||
return _SchemaStorage(total_storage=total, used_storage=total - available)
|
||||
|
||||
|
||||
def _build_downloader(name: Optional[str] = None) -> _SchemaDownloaderInfo:
|
||||
def _build_downloader(
|
||||
name: Optional[str] = None,
|
||||
*,
|
||||
btrfs_fsid_dedup: bool = False,
|
||||
) -> _SchemaDownloaderInfo:
|
||||
"""
|
||||
构建下载器统计信息。
|
||||
"""
|
||||
@@ -61,7 +66,7 @@ def _build_downloader(name: Optional[str] = None) -> _SchemaDownloaderInfo:
|
||||
download_dirs = DirectoryHelper().get_local_download_dirs()
|
||||
_, free_space = SystemUtils.space_usage(
|
||||
[Path(d.download_path) for d in download_dirs],
|
||||
btrfs_fsid_dedup=settings.BTRFS_FSID_DEDUP,
|
||||
btrfs_fsid_dedup=btrfs_fsid_dedup,
|
||||
)
|
||||
# 下载器信息
|
||||
downloader_info = _SchemaDownloaderInfo()
|
||||
@@ -137,12 +142,18 @@ def system_info(_: Any = Depends(get_current_active_superuser)) -> Any:
|
||||
|
||||
@router.get("/downloader", summary="下载器信息", response_model=_SchemaDownloaderInfo)
|
||||
def downloader(
|
||||
name: Optional[str] = None, _: Any = Depends(get_current_active_superuser)
|
||||
name: Optional[str] = None,
|
||||
runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config),
|
||||
_: Any = Depends(get_current_active_superuser),
|
||||
) -> Any:
|
||||
"""
|
||||
查询下载器信息
|
||||
"""
|
||||
return _build_downloader(name)
|
||||
runtime_config = resolve_api_runtime_config(runtime_config)
|
||||
return _build_downloader(
|
||||
name,
|
||||
btrfs_fsid_dedup=runtime_config.btrfs_fsid_dedup,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -150,11 +161,17 @@ def downloader(
|
||||
summary="下载器信息(API_TOKEN)",
|
||||
response_model=_SchemaDownloaderInfo,
|
||||
)
|
||||
def downloader2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
|
||||
def downloader2(
|
||||
_: Annotated[str, Depends(verify_apitoken)],
|
||||
runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config),
|
||||
) -> Any:
|
||||
"""
|
||||
查询下载器信息 API_TOKEN认证(?token=xxx)
|
||||
"""
|
||||
return _build_downloader()
|
||||
runtime_config = resolve_api_runtime_config(runtime_config)
|
||||
return _build_downloader(
|
||||
btrfs_fsid_dedup=runtime_config.btrfs_fsid_dedup,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/schedule", summary="后台服务", response_model=List[_SchemaScheduleInfo])
|
||||
|
||||
@@ -111,6 +111,74 @@ def _build_unrecognized_media_info(
|
||||
)
|
||||
|
||||
|
||||
def _resolve_add_media(
|
||||
torrent_in: _SchemaTorrentInfo,
|
||||
media_source: MediaSource | None,
|
||||
media_id: str | None,
|
||||
music_type: MusicTargetEntityType | None,
|
||||
allow_unrecognized: bool,
|
||||
) -> tuple[MetaBase | None, MediaInfo | MusicInfo | None, _SchemaResponse | None]:
|
||||
"""校验媒体身份并为无媒体信息下载构建识别上下文。"""
|
||||
normalized_music_type = normalize_music_type(music_type, allow_artist=False)
|
||||
if music_type is not None and not normalized_music_type:
|
||||
return None, None, _SchemaResponse(
|
||||
success=False,
|
||||
message="音乐实体类型无效,仅支持 recording 或 album",
|
||||
)
|
||||
if (media_source is None) != (media_id is None):
|
||||
return None, None, _SchemaResponse(
|
||||
success=False,
|
||||
message="媒体来源和媒体 ID 必须同时提供",
|
||||
)
|
||||
is_music = (
|
||||
torrent_in.category in (MediaType.MUSIC, MediaType.MUSIC.value, "music")
|
||||
or is_music_media_source(media_source)
|
||||
or normalized_music_type is not None
|
||||
)
|
||||
if is_music and media_source and not is_music_media_source(media_source):
|
||||
return None, None, _SchemaResponse(
|
||||
success=False,
|
||||
message="音乐下载只能使用音乐元数据源",
|
||||
)
|
||||
if is_music and not normalized_music_type:
|
||||
normalized_music_type = MUSIC_ENTITY_RECORDING
|
||||
metainfo = (
|
||||
MetaMusic.parse_query(torrent_in.title)
|
||||
if is_music
|
||||
else MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
|
||||
)
|
||||
if media_source and media_id:
|
||||
mediainfo = MediaChain().recognize_media(
|
||||
meta=metainfo,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
mtype=MediaType.MUSIC if is_music else None,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
else:
|
||||
mediainfo = MediaChain().recognize_by_meta(
|
||||
metainfo,
|
||||
media_source=media_source,
|
||||
obtain_images=False,
|
||||
mtype=MediaType.MUSIC if is_music else None,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
if mediainfo:
|
||||
return metainfo, mediainfo, None
|
||||
if not allow_unrecognized:
|
||||
return metainfo, None, _SchemaResponse(
|
||||
success=False,
|
||||
message="无法识别媒体信息",
|
||||
data=_SchemaDownloadAddedData(requires_confirmation=True),
|
||||
)
|
||||
return metainfo, _build_unrecognized_media_info(
|
||||
torrent_in,
|
||||
metainfo,
|
||||
is_music=is_music,
|
||||
music_type=normalized_music_type,
|
||||
), None
|
||||
|
||||
|
||||
@router.get("/", summary="正在下载", response_model=List[_SchemaDownloaderTorrent])
|
||||
def current(
|
||||
name: Optional[str] = None, _: _SchemaTokenPayload = Depends(verify_token)
|
||||
@@ -183,66 +251,17 @@ def add(
|
||||
"""
|
||||
添加下载任务(不含媒体信息)
|
||||
"""
|
||||
normalized_music_type = normalize_music_type(music_type, allow_artist=False)
|
||||
if music_type is not None and not normalized_music_type:
|
||||
return _SchemaResponse(
|
||||
success=False,
|
||||
message="音乐实体类型无效,仅支持 recording 或 album",
|
||||
)
|
||||
if (media_source is None) != (media_id is None):
|
||||
return _SchemaResponse(
|
||||
success=False,
|
||||
message="媒体来源和媒体 ID 必须同时提供",
|
||||
)
|
||||
is_music = (
|
||||
torrent_in.category in (MediaType.MUSIC, MediaType.MUSIC.value, "music")
|
||||
or is_music_media_source(media_source)
|
||||
or normalized_music_type is not None
|
||||
metainfo, mediainfo, error = _resolve_add_media(
|
||||
torrent_in,
|
||||
media_source,
|
||||
media_id,
|
||||
music_type,
|
||||
allow_unrecognized,
|
||||
)
|
||||
if is_music and media_source and not is_music_media_source(media_source):
|
||||
return _SchemaResponse(
|
||||
success=False,
|
||||
message="音乐下载只能使用音乐元数据源",
|
||||
)
|
||||
if is_music and not normalized_music_type:
|
||||
normalized_music_type = MUSIC_ENTITY_RECORDING
|
||||
# 元数据
|
||||
metainfo = (
|
||||
MetaMusic.parse_query(torrent_in.title)
|
||||
if is_music
|
||||
else MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
|
||||
)
|
||||
# 媒体信息
|
||||
if media_source and media_id:
|
||||
mediainfo = MediaChain().recognize_media(
|
||||
meta=metainfo,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
mtype=MediaType.MUSIC if is_music else None,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
else:
|
||||
mediainfo = MediaChain().recognize_by_meta(
|
||||
metainfo,
|
||||
media_source=media_source,
|
||||
obtain_images=False,
|
||||
mtype=MediaType.MUSIC if is_music else None,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
if not mediainfo:
|
||||
if not allow_unrecognized:
|
||||
return _SchemaResponse(
|
||||
success=False,
|
||||
message="无法识别媒体信息",
|
||||
data=_SchemaDownloadAddedData(requires_confirmation=True),
|
||||
)
|
||||
# 用户已确认:影视与音乐统一按种子元信息构造最小上下文继续下载
|
||||
mediainfo = _build_unrecognized_media_info(
|
||||
torrent_in,
|
||||
metainfo,
|
||||
is_music=is_music,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
if metainfo is None or mediainfo is None:
|
||||
return _SchemaResponse(success=False, message="无法识别媒体信息")
|
||||
# 种子信息
|
||||
torrentinfo = TorrentInfo()
|
||||
torrentinfo.from_dict(torrent_in.model_dump())
|
||||
|
||||
@@ -19,7 +19,9 @@ from app.agent.prompt.transfer_redo import (
|
||||
build_batch_manual_redo_prompt,
|
||||
build_manual_redo_prompt,
|
||||
)
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.runtime.config import global_vars
|
||||
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
|
||||
from app.application.configuration import ApiRuntimeConfig
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.api.dependencies.auth import (
|
||||
get_current_active_manage_user,
|
||||
@@ -244,12 +246,14 @@ def delete_transfer_history(
|
||||
async def ai_redo_transfer_history(
|
||||
history_id: int,
|
||||
query: HistoryQueryService = Depends(get_history_query_service),
|
||||
runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
手动触发单条历史记录的 AI 重新整理,并返回进度键。
|
||||
"""
|
||||
if not settings.AI_AGENT_ENABLE:
|
||||
runtime_config = resolve_api_runtime_config(runtime_config)
|
||||
if not runtime_config.ai_agent_enable:
|
||||
return _SchemaResponse(success=False, message="MoviePilot智能助手未启用")
|
||||
|
||||
history = await query.get_transfer(history_id)
|
||||
@@ -275,12 +279,14 @@ async def ai_redo_transfer_history(
|
||||
async def batch_ai_redo_transfer_history(
|
||||
payload: _SchemaBatchTransferHistoryRedoRequest,
|
||||
query: HistoryQueryService = Depends(get_history_query_service),
|
||||
runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
手动触发多条历史记录的 AI 批量重新整理,并返回进度键。
|
||||
"""
|
||||
if not settings.AI_AGENT_ENABLE:
|
||||
runtime_config = resolve_api_runtime_config(runtime_config)
|
||||
if not runtime_config.ai_agent_enable:
|
||||
return _SchemaResponse(success=False, message="MoviePilot智能助手未启用")
|
||||
|
||||
history_ids = normalize_history_ids(payload.history_ids)
|
||||
|
||||
@@ -13,8 +13,8 @@ from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
|
||||
from app.chain.user import MfaRequired, UserChain
|
||||
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
|
||||
from app.application.security.token import create_access_token
|
||||
from app.runtime.config import settings
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
|
||||
from app.application.configuration import ApiRuntimeConfig, get_configured_system_config
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.application.image import WallpaperHelper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
@@ -39,10 +39,12 @@ def login_access_token(
|
||||
response: Response,
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
otp_password: Annotated[str | None, Form()] = None,
|
||||
runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config),
|
||||
) -> Any:
|
||||
"""
|
||||
获取认证Token
|
||||
"""
|
||||
runtime_config = resolve_api_runtime_config(runtime_config)
|
||||
success, user_or_message = UserChain().user_authenticate(
|
||||
username=form_data.username, password=form_data.password, mfa_code=otp_password
|
||||
)
|
||||
@@ -69,13 +71,13 @@ def login_access_token(
|
||||
# 是否显示配置向导
|
||||
show_wizard = (
|
||||
not get_configured_system_config().get(SystemConfigKey.SetupWizardState)
|
||||
and not settings.ADVANCED_MODE
|
||||
and not runtime_config.advanced_mode
|
||||
)
|
||||
access_token = create_access_token(
|
||||
userid=user_or_message.id,
|
||||
username=user_or_message.name,
|
||||
super_user=user_or_message.is_superuser,
|
||||
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
||||
expires_delta=timedelta(minutes=runtime_config.access_token_expire_minutes),
|
||||
level=level,
|
||||
)
|
||||
set_or_refresh_resource_token_cookie(
|
||||
|
||||
+34
-41
@@ -84,6 +84,39 @@ def create_jsonrpc_error(
|
||||
return error
|
||||
|
||||
|
||||
async def _dispatch_jsonrpc_method(
|
||||
method: Any,
|
||||
params: Dict[str, Any],
|
||||
request_id: Union[str, int, None],
|
||||
) -> Union[JSONResponse, Response]:
|
||||
"""分派一个已经通过基础格式校验的 MCP JSON-RPC 方法。"""
|
||||
if method == "initialize":
|
||||
result = await handle_initialize(params)
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, result))
|
||||
if method == "notifications/initialized":
|
||||
if request_id is None:
|
||||
return Response(status_code=204)
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=create_jsonrpc_error(
|
||||
request_id, -32600, "initialized must be a notification"
|
||||
),
|
||||
)
|
||||
if method == "tools/list":
|
||||
result = await handle_tools_list()
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, result))
|
||||
if method == "tools/call":
|
||||
result = await handle_tools_call(params)
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, result))
|
||||
if method == "ping":
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, {}))
|
||||
return JSONResponse(
|
||||
content=create_jsonrpc_error(
|
||||
request_id, -32601, f"Method not found: {method}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
summary="MCP JSON-RPC 端点",
|
||||
@@ -130,48 +163,8 @@ async def mcp_jsonrpc(
|
||||
params = body.get("params", {})
|
||||
request_id = body.get("id")
|
||||
|
||||
# 如果有 id,则为请求;没有 id 则为通知
|
||||
is_notification = request_id is None
|
||||
|
||||
try:
|
||||
# 处理初始化请求
|
||||
if method == "initialize":
|
||||
result = await handle_initialize(params)
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, result))
|
||||
|
||||
# 处理已初始化通知
|
||||
elif method == "notifications/initialized":
|
||||
if is_notification:
|
||||
return Response(status_code=204)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=create_jsonrpc_error(
|
||||
request_id, -32600, "initialized must be a notification"
|
||||
),
|
||||
)
|
||||
|
||||
# 处理工具列表请求
|
||||
if method == "tools/list":
|
||||
result = await handle_tools_list()
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, result))
|
||||
|
||||
# 处理工具调用请求
|
||||
elif method == "tools/call":
|
||||
result = await handle_tools_call(params)
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, result))
|
||||
|
||||
# 处理 ping 请求
|
||||
elif method == "ping":
|
||||
return JSONResponse(content=create_jsonrpc_response(request_id, {}))
|
||||
|
||||
# 未知方法
|
||||
else:
|
||||
return JSONResponse(
|
||||
content=create_jsonrpc_error(
|
||||
request_id, -32601, f"Method not found: {method}"
|
||||
)
|
||||
)
|
||||
return await _dispatch_jsonrpc_method(method, params, request_id)
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"MCP 请求参数错误: {e}")
|
||||
|
||||
Reference in New Issue
Block a user