diff --git a/app/factory.py b/app/factory.py new file mode 100644 index 00000000..78d43546 --- /dev/null +++ b/app/factory.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.core.config import settings +from app.startup.lifecycle import lifespan + + +def create_app() -> FastAPI: + """ + 创建并配置 FastAPI 应用实例。 + """ + app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json", + lifespan=lifespan + ) + + # 配置 CORS 中间件 + app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_HOSTS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + return app + + +# 创建 FastAPI 应用实例 +app = create_app() diff --git a/app/main.py b/app/main.py index 803c6094..f6de2180 100644 --- a/app/main.py +++ b/app/main.py @@ -1,17 +1,13 @@ import multiprocessing import os -import signal import sys import threading -from contextlib import asynccontextmanager -from types import FrameType import uvicorn as uvicorn from PIL import Image -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware from uvicorn import Config +from app.factory import app from app.utils.system import SystemUtils # 禁用输出 @@ -19,110 +15,14 @@ if SystemUtils.is_frozen(): sys.stdout = open(os.devnull, 'w') sys.stderr = open(os.devnull, 'w') -from app.core.config import settings, global_vars -from app.core.module import ModuleManager - -# SitesHelper涉及资源包拉取,提前引入并容错提示 -try: - from app.helper.sites import SitesHelper -except ImportError as e: - error_message = f"错误: {str(e)}\n站点认证及索引相关资源导入失败,请尝试重建容器或手动拉取资源" - print(error_message, file=sys.stderr) - sys.exit(1) - -from app.core.event import EventManager -from app.core.plugin import PluginManager +from app.core.config import settings from app.db.init import init_db, update_db -from app.helper.thread import ThreadHelper -from app.helper.display import DisplayHelper -from app.helper.resource import ResourceHelper -from app.helper.message import MessageHelper -from app.scheduler import Scheduler -from app.monitor import Monitor -from app.command import Command, CommandChian -from app.schemas import Notification, NotificationType - - -@asynccontextmanager -async def lifespan(app: FastAPI): - try: - print("Starting up...") - start_module() - yield - finally: - print("Shutting down...") - shutdown_server() - - -# App -App = FastAPI(title=settings.PROJECT_NAME, - openapi_url=f"{settings.API_V1_STR}/openapi.json", - lifespan=lifespan) - -# 跨域 -App.add_middleware( - CORSMiddleware, - allow_origins=settings.ALLOWED_HOSTS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) # uvicorn服务 -Server = uvicorn.Server(Config(App, host=settings.HOST, port=settings.PORT, +Server = uvicorn.Server(Config(app, host=settings.HOST, port=settings.PORT, reload=settings.DEV, workers=multiprocessing.cpu_count())) -def init_routers(): - """ - 初始化路由 - """ - from app.api.apiv1 import api_router - from app.api.servarr import arr_router - from app.api.servcookie import cookie_router - # API路由 - App.include_router(api_router, prefix=settings.API_V1_STR) - # Radarr、Sonarr路由 - App.include_router(arr_router, prefix="/api/v3") - # CookieCloud路由 - App.include_router(cookie_router, prefix="/cookiecloud") - - -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 start_tray(): """ 启动托盘图标 @@ -169,97 +69,6 @@ def start_tray(): threading.Thread(target=TrayIcon.run, daemon=True).start() -def check_auth(): - """ - 检查认证状态 - """ - if SitesHelper().auth_level < 2: - err_msg = "用户认证失败,站点相关功能将无法使用!" - MessageHelper().put(f"注意:{err_msg}", title="用户认证", role="system") - CommandChian().post_message( - Notification( - mtype=NotificationType.Manual, - title="MoviePilot用户认证", - text=err_msg, - link=settings.MP_DOMAIN('#/site') - ) - ) - - -def singal_handle(): - """ - 监听停止信号 - """ - - def stop_event(signum: int, _: FrameType): - """ - SIGTERM信号处理 - """ - print(f"接收到停止信号:{signum},正在停止系统...") - global_vars.stop_system() - - # 设置信号处理程序 - signal.signal(signal.SIGTERM, stop_event) - signal.signal(signal.SIGINT, stop_event) - - -def shutdown_server(): - """ - 服务关闭 - """ - # 停止模块 - ModuleManager().stop() - # 停止插件 - PluginManager().stop() - PluginManager().stop_monitor() - # 停止事件消费 - EventManager().stop() - # 停止虚拟显示 - DisplayHelper().stop() - # 停止定时服务 - Scheduler().stop() - # 停止监控 - Monitor().stop() - # 停止线程池 - ThreadHelper().shutdown() - # 停止前端服务 - stop_frontend() - - -def start_module(): - """ - 启动模块 - """ - # 虚拟显示 - DisplayHelper() - # 站点管理 - SitesHelper() - # 资源包检测 - ResourceHelper() - # 加载模块 - ModuleManager() - # 启动事件消费 - EventManager().start() - # 安装在线插件 - PluginManager().sync() - # 加载插件 - PluginManager().start() - # 启动监控任务 - Monitor() - # 启动定时服务 - Scheduler() - # 加载命令 - Command() - # 初始化路由 - init_routers() - # 启动前端服务 - start_frontend() - # 检查认证状态 - check_auth() - # 监听停止信号 - singal_handle() - - if __name__ == '__main__': # 启动托盘 start_tray() diff --git a/app/startup/__init__.py b/app/startup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/startup/lifecycle.py b/app/startup/lifecycle.py new file mode 100644 index 00000000..7bb0e7d3 --- /dev/null +++ b/app/startup/lifecycle.py @@ -0,0 +1,19 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from app.startup.module_initializer import start_modules, shutdown_modules +from app.startup.routers import init_routers + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + 定义应用的生命周期事件 + """ + print("Starting up...") + start_modules(app) + init_routers(app) + yield + print("Shutting down...") + shutdown_modules(app) diff --git a/app/startup/module_initializer.py b/app/startup/module_initializer.py new file mode 100644 index 00000000..608f76b0 --- /dev/null +++ b/app/startup/module_initializer.py @@ -0,0 +1,152 @@ +import signal +import sys +from types import FrameType + +from fastapi import FastAPI + +from app.core.config import settings, global_vars +from app.core.module import ModuleManager +from app.utils.system import SystemUtils + +# SitesHelper涉及资源包拉取,提前引入并容错提示 +try: + from app.helper.sites import SitesHelper +except ImportError as e: + error_message = f"错误: {str(e)}\n站点认证及索引相关资源导入失败,请尝试重建容器或手动拉取资源" + print(error_message, file=sys.stderr) + sys.exit(1) + +from app.core.event import EventManager +from app.core.plugin import PluginManager +from app.helper.thread import ThreadHelper +from app.helper.display import DisplayHelper +from app.helper.resource import ResourceHelper +from app.helper.message import MessageHelper +from app.scheduler import Scheduler +from app.monitor import Monitor +from app.command import Command, CommandChian +from app.schemas import Notification, NotificationType + + +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 check_auth(): + """ + 检查认证状态 + """ + if SitesHelper().auth_level < 2: + err_msg = "用户认证失败,站点相关功能将无法使用!" + MessageHelper().put(f"注意:{err_msg}", title="用户认证", role="system") + CommandChian().post_message( + Notification( + mtype=NotificationType.Manual, + title="MoviePilot用户认证", + text=err_msg, + link=settings.MP_DOMAIN('#/site') + ) + ) + + +def singal_handle(): + """ + 监听停止信号 + """ + + def stop_event(signum: int, _: FrameType): + """ + SIGTERM信号处理 + """ + print(f"接收到停止信号:{signum},正在停止系统...") + global_vars.stop_system() + + # 设置信号处理程序 + signal.signal(signal.SIGTERM, stop_event) + signal.signal(signal.SIGINT, stop_event) + + +def shutdown_modules(app: FastAPI): + """ + 服务关闭 + """ + # 停止模块 + ModuleManager().stop() + # 停止插件 + PluginManager().stop() + PluginManager().stop_monitor() + # 停止事件消费 + EventManager().stop() + # 停止虚拟显示 + DisplayHelper().stop() + # 停止定时服务 + Scheduler().stop() + # 停止监控 + Monitor().stop() + # 停止线程池 + ThreadHelper().shutdown() + # 停止前端服务 + stop_frontend() + + +def start_modules(app: FastAPI): + """ + 启动模块 + """ + # 虚拟显示 + DisplayHelper() + # 站点管理 + SitesHelper() + # 资源包检测 + ResourceHelper() + # 加载模块 + ModuleManager() + # 启动事件消费 + EventManager().start() + # 安装在线插件 + PluginManager().sync() + # 加载插件 + PluginManager().start() + # 启动监控任务 + Monitor() + # 启动定时服务 + Scheduler() + # 加载命令 + Command() + # 启动前端服务 + start_frontend() + # 检查认证状态 + check_auth() + # 监听停止信号 + singal_handle() diff --git a/app/startup/routers.py b/app/startup/routers.py new file mode 100644 index 00000000..101b8603 --- /dev/null +++ b/app/startup/routers.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI + +from app.core.config import settings + + +def init_routers(app: FastAPI): + """ + 初始化路由 + """ + from app.api.apiv1 import api_router + from app.api.servarr import arr_router + from app.api.servcookie import cookie_router + # API路由 + app.include_router(api_router, prefix=settings.API_V1_STR) + # Radarr、Sonarr路由 + app.include_router(arr_router, prefix="/api/v3") + # CookieCloud路由 + app.include_router(cookie_router, prefix="/cookiecloud")