mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 19:14:01 +08:00
* wip(v3): 移植监控与整理韧性修复到 v3 基线 包含:监控看门狗隔离/挂载探测、整理队列持久化、文件系统子进程代理、 写入原子化。迁移重挂到 v3 链 8a4c7e1d2f90 -> 7f5c1d2e3a4b -> e3d9f4b7c806。 tmdb 相关测试尚未通过,待定位。 * fix(v3): 修正移植引入的 16 项测试失败 - poller.py:合并时我方保留的行仍用旧变量名 merged_snapshot,而 v3 已统一 改名为 current_snapshot,导致 NameError 被外层 except 吞掉、快照从未保存 - smb.py:采纳 f-string 拆分写法,恢复 Python 3.11 可解析 - dispatcher 测试:历史查重由 _should_skip_by_history 统一承担,mock 点随之调整 - tmdb 缓存测试:补充 v3 新增的 media_source/media_id 字段 - tmdb 重试测试:为 fake 补充 match_multi/async_match_multi 尚余 3 项与 v3 识别流程的连接失败处理有关,待单独判断。 * fix(v3): 测试适配 v3 的 media_source/media_id 重构 v3 将媒体标识从 tmdbid 统一重构为 media_source + media_id,recognize_media 的 tmdbid 参数已被 **kwargs 静默吞掉——传了也不生效,流程会误降级到名称搜索。 tmdb 重试用例改用新参数后恢复正确路径。 同时修正 fake 的 match_multi 语义:真实实现(tmdbapi.match_multi)吞掉所有 异常并返回 None,连接失败与「未找到」在该路径上本就不可区分,fake 需保持一致。 至此移植引入的 19 项失败全部清零。 --------- Co-authored-by: Aqr-K <Aqr-K@users.noreply.github.com>
135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
import asyncio
|
||
import inspect
|
||
from contextlib import asynccontextmanager
|
||
from typing import Callable
|
||
|
||
from fastapi import FastAPI
|
||
|
||
# urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁
|
||
try:
|
||
import urllib3.fields as _urllib3_fields
|
||
|
||
if not hasattr(_urllib3_fields, "format_header_param") and hasattr(
|
||
_urllib3_fields, "format_header_param_rfc2231"
|
||
):
|
||
_urllib3_fields.format_header_param = (
|
||
_urllib3_fields.format_header_param_rfc2231
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
from app.chain.system import SystemChain
|
||
from app.core.config import global_vars, settings
|
||
from app.helper.server import MoviePilotServerHelper
|
||
from app.helper.system import SystemHelper
|
||
from app.log import logger, LoggerManager
|
||
from app.startup.command_initializer import init_command, stop_command, restart_command
|
||
from app.startup.modules_initializer import init_modules, stop_modules
|
||
from app.startup.monitor_initializer import stop_monitor, init_monitor
|
||
from app.startup.plugins_initializer import init_plugins, stop_plugins, sync_plugins
|
||
from app.startup.routers_initializer import init_routers
|
||
from app.startup.scheduler_initializer import (
|
||
stop_scheduler,
|
||
init_scheduler,
|
||
init_plugin_scheduler,
|
||
)
|
||
from app.startup.transfer_initializer import replay_pending_transfers
|
||
from app.startup.workflow_initializer import init_workflow, stop_workflow
|
||
from app.utils.http import aclose_shared_async_transports
|
||
|
||
|
||
async def init_extra():
|
||
"""
|
||
同步插件及重启相关依赖服务
|
||
"""
|
||
if settings.MOVIEPILOT_SAFE_MODE:
|
||
SystemHelper().set_system_modified()
|
||
SystemChain().restart_finish()
|
||
return
|
||
if await sync_plugins():
|
||
# 重新注册插件定时服务
|
||
init_plugin_scheduler()
|
||
# 重新注册命令
|
||
restart_command()
|
||
# 设置系统已修改标志
|
||
SystemHelper().set_system_modified()
|
||
# 重启完成
|
||
SystemChain().restart_finish()
|
||
# 上报当前安装版本
|
||
await MoviePilotServerHelper.async_report_usage()
|
||
|
||
|
||
async def run_shutdown_step(name: str, callback: Callable[[], object]) -> None:
|
||
"""隔离单个关闭阶段的异常,确保后续资源仍有机会释放"""
|
||
try:
|
||
result = callback()
|
||
if inspect.isawaitable(result):
|
||
await result
|
||
except Exception as err:
|
||
logger.error(f"关闭{name}失败:{err}")
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
"""
|
||
定义应用的生命周期事件
|
||
"""
|
||
print("Starting up...")
|
||
# 存储当前循环
|
||
global_vars.set_loop(asyncio.get_event_loop())
|
||
# 初始化路由
|
||
init_routers(app)
|
||
# 初始化模块
|
||
init_modules()
|
||
if settings.MOVIEPILOT_SAFE_MODE:
|
||
print("MoviePilot safe mode enabled: skip plugins, scheduler, monitor, commands and workflow.")
|
||
else:
|
||
# 恢复插件备份
|
||
SystemChain().restore_plugins()
|
||
# 初始化插件
|
||
init_plugins()
|
||
# 初始化定时器
|
||
init_scheduler()
|
||
# 初始化监控器
|
||
init_monitor()
|
||
# 回放上次未整理完的文件(后台线程,不阻塞启动)
|
||
replay_pending_transfers()
|
||
# 初始化命令
|
||
init_command()
|
||
# 初始化工作流
|
||
init_workflow()
|
||
# 插件同步到本地
|
||
sync_plugins_task = asyncio.create_task(init_extra())
|
||
try:
|
||
# 在此处 yield,表示应用已经启动,控制权交回 FastAPI 主事件循环
|
||
yield
|
||
finally:
|
||
print("Shutting down...")
|
||
global_vars.stop_system()
|
||
# 取消同步插件任务
|
||
try:
|
||
sync_plugins_task.cancel()
|
||
await sync_plugins_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
except Exception as e:
|
||
print(str(e))
|
||
try:
|
||
if not settings.MOVIEPILOT_SAFE_MODE:
|
||
await run_shutdown_step(
|
||
"插件备份", lambda: SystemChain().backup_plugins()
|
||
)
|
||
await run_shutdown_step("工作流", stop_workflow)
|
||
await run_shutdown_step("命令服务", stop_command)
|
||
await run_shutdown_step("监控器", stop_monitor)
|
||
await run_shutdown_step("定时器", stop_scheduler)
|
||
await run_shutdown_step("插件", stop_plugins)
|
||
await run_shutdown_step("模块服务", stop_modules)
|
||
await run_shutdown_step(
|
||
"共享异步 HTTP 连接池",
|
||
aclose_shared_async_transports,
|
||
)
|
||
finally:
|
||
# 日志最后关闭,确保其他组件的收尾信息已写入文件
|
||
LoggerManager.shutdown()
|