import inspect import sys from typing import Callable from app.infrastructure.redis import RedisHelper, AsyncRedisHelper from app.chain.mediaserver import MediaServerChain from app.chain.tmdb import TmdbChain # SitesHelper涉及资源包拉取,提前引入并容错提示 try: from app.infrastructure.sites import SitesHelper # noqa except ImportError as e: SitesHelper = None error_message = f"错误: {str(e)}\n站点认证及索引相关资源导入失败,请尝试重建容器或手动拉取资源" print(error_message, file=sys.stderr) sys.exit(1) from app.infrastructure.system import SystemUtils from app.platform.log import logger from app.platform.config import settings from app.extensions.module_manager import ModuleManager from app.platform.events import EventManager from app.platform.runtime import SystemHelper from app.platform.thread import ThreadHelper from app.infrastructure.display import DisplayHelper from app.infrastructure.doh import DohHelper from app.infrastructure.resource import ResourceHelper from app.messaging.message import MessageHelper, stop_message from app.integrations.server import MoviePilotServerHelper from app.db import close_database from app.db.systemconfig_oper import SystemConfigOper from app.command import CommandChain from app.schemas import Notification, NotificationType from app.schemas.types import SystemConfigKey from app.startup.agent_initializer import init_agent, stop_agent from app.security.access import set_superuser_token_payload_provider from app.security.auth import build_superuser_token_payload from app.services.image import configure_wallpaper_providers def configure_wallpaper_services() -> None: """把需要 Chain 编排的壁纸来源注入图片服务。""" configure_wallpaper_providers( tmdb_wallpaper=lambda: TmdbChain().get_random_wallpager(), tmdb_wallpapers=lambda count: TmdbChain().get_trending_wallpapers(count), mediaserver_wallpaper=lambda: MediaServerChain().get_latest_wallpaper(), mediaserver_wallpapers=lambda count: MediaServerChain().get_latest_wallpapers( count=count ), ) def notify_event_error(title: str, message: str) -> None: """将事件总线错误转发到系统消息通道。""" MessageHelper().put( title=title, message=message, role="system", ) def start_frontend(): """ 启动前端服务 """ # 仅Windows可执行文件支持内嵌nginx if not SystemUtils.is_frozen() \ or not SystemUtils.is_windows(): return # 临时Nginx目录 nginx_path = settings.ROOT_PATH / 'nginx' if not nginx_path.exists(): return # 配置目录下的Nginx目录 run_nginx_dir = settings.CONFIG_PATH.with_name('nginx') if not run_nginx_dir.exists(): # 移动到配置目录 SystemUtils.move(nginx_path, run_nginx_dir) # 启动Nginx import subprocess subprocess.Popen("start nginx.exe", cwd=run_nginx_dir, shell=True) def stop_frontend(): """ 停止前端服务 """ if not SystemUtils.is_frozen() \ or not SystemUtils.is_windows(): return import subprocess subprocess.Popen(f"taskkill /f /im nginx.exe", shell=True) def clear_temp(): """ 清理临时文件和图片缓存 """ # 清理临时目录中3天前的文件 SystemUtils.clear(settings.TEMP_PATH, days=settings.TEMP_FILE_DAYS) # 清理图片缓存目录中7天前的文件 SystemUtils.clear(settings.CACHE_PATH / "images", days=settings.GLOBAL_IMAGE_CACHE_DAYS) # 清理 pip/uv 包下载缓存,不接管整个 .cache 目录。 clear_package_tool_cache() def clear_package_tool_cache(): """ 清理 pip/uv 包下载缓存,只处理 MoviePilot 管理的工具子目录。 """ days = settings.PACKAGE_CACHE_DAYS if days <= 0: return tool_cache_root = settings.PACKAGE_CACHE_PATH for child in ("pip", "uv"): cache_path = tool_cache_root / child try: SystemUtils.clear(cache_path, days=days) except Exception as err: logger.warning("清理包下载缓存失败:%s - %s", cache_path, err) def user_auth(): """ 用户认证检查 """ sites_helper = SitesHelper() if sites_helper.auth_level >= 2: return auth_conf = SystemConfigOper().get(SystemConfigKey.UserSiteAuthParams) status, msg = sites_helper.check_user(**auth_conf) if auth_conf else sites_helper.check_user() if status: logger.info(f"{msg} 用户认证成功") else: logger.info(f"用户认证失败,{msg}") def check_auth(): """ 检查认证状态 """ if SitesHelper().auth_level < 2: err_msg = "用户认证失败,站点相关功能将无法使用!" MessageHelper().put(f"注意:{err_msg}", title="用户认证", role="system") CommandChain().post_message( Notification( mtype=NotificationType.Manual, title="MoviePilot用户认证", text=err_msg, link=settings.MP_DOMAIN('#/site') ) ) def update_resources() -> None: """安装可用资源更新,并由组合根统一决定是否重启进程。""" if ResourceHelper().check() is not True: return restarted, message = SystemHelper.restart() if not restarted: logger.error(f"资源更新完成但自动重启失败:{message}") async def stop_modules(): """ 服务关闭 """ async def run_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}") await run_step("AI智能体", stop_agent) await run_step("模块", lambda: ModuleManager().stop()) await run_step("事件消费", lambda: EventManager().stop()) await run_step("虚拟显示", lambda: DisplayHelper().stop()) await run_step("DoH服务", lambda: DohHelper().shutdown()) await run_step("线程池", lambda: ThreadHelper().shutdown()) await run_step("消息服务", stop_message) await run_step("Redis缓存连接", lambda: RedisHelper().close()) await run_step("异步Redis缓存连接", lambda: AsyncRedisHelper().close()) await run_step("数据库连接", close_database) await run_step("前端服务", stop_frontend) await run_step("临时文件", clear_temp) async def init_modules(): """ 启动模块 """ # 应用服务不反向依赖 Chain,由启动组合层注入壁纸来源。 configure_wallpaper_services() # 认证访问层不反向依赖数据库实现,由启动组合层注入载荷提供器。 set_superuser_token_payload_provider(build_superuser_token_payload) # 虚拟显示 DisplayHelper() # DoH DohHelper() # 站点管理 SitesHelper() # 资源适配器只负责下载安装,是否重启由启动组合层决定。 update_resources() # 用户认证 user_auth() # 事件错误通知由启动组合层接入消息服务。 EventManager().set_error_notifier(notify_event_error) # 加载模块 ModuleManager() # 启动事件消费 EventManager().start() # 初始化共享服务端状态 MoviePilotServerHelper.init_plugin_report() MoviePilotServerHelper.init_subscribe_report() MoviePilotServerHelper.get_user_uuid() MoviePilotServerHelper.get_github_user() # 初始化AI智能体 await init_agent() # 启动前端服务 start_frontend() # 检查认证状态 check_auth()