Files
MoviePilot/app/main.py
T

206 lines
5.9 KiB
Python

import os
import sys
def _prepare_direct_execution_import_path() -> None:
"""
修正直接执行 ``app/main.py`` 时的模块搜索路径。
PyCharm 的脚本启动方式会把 ``app`` 目录放到 ``sys.path[0]``,使应用内部
目录可能被当成顶级模块并遮蔽标准库或第三方包。直接执行时只保留项目根目录
作为应用导入入口,模块方式启动则不做任何调整。
"""
if __package__:
return
script_dir = os.path.dirname(os.path.realpath(__file__))
project_root = os.path.dirname(script_dir)
sys.path[:] = [
entry
for entry in sys.path
if os.path.realpath(entry or os.curdir) != script_dir
]
if project_root in sys.path:
sys.path.remove(project_root)
sys.path.insert(0, project_root)
_prepare_direct_execution_import_path()
import signal
import threading
from pathlib import Path
from typing import Optional
import setproctitle
import uvicorn as uvicorn
from PIL import Image
from uvicorn import Config
from app.adapters.system.host import SystemUtils
from app.adapters.system.stdio import configure_rotating_stdio
stdio_log_file = os.getenv("MOVIEPILOT_STDIO_LOG_FILE")
if stdio_log_file:
# 本地 CLI 会把 stdout/stderr 切到滚动日志,避免无限追加单独的大文件。
configure_rotating_stdio(
log_file=Path(stdio_log_file),
max_bytes=max(int(os.getenv("MOVIEPILOT_STDIO_LOG_MAX_BYTES", "0") or 0), 1),
backup_count=max(
int(os.getenv("MOVIEPILOT_STDIO_LOG_BACKUP_COUNT", "0") or 0),
0,
),
)
elif SystemUtils.is_frozen():
sys.stdout = open(os.devnull, 'w')
sys.stderr = open(os.devnull, 'w')
from app.factory import app
from app.runtime.settings import get_runtime_setting
from app.runtime.config import global_vars
from app.runtime.stop import runtime_stop_state
from app.runtime.topology import (
UnsupportedProcessTopologyError,
validate_process_topology,
)
setproctitle.setproctitle(get_runtime_setting('PROJECT_NAME'))
class MoviePilotServer(uvicorn.Server):
"""在 Uvicorn 开始优雅退出前发布应用协作停止标志"""
def handle_exit(self, sig, frame) -> None:
getattr(global_vars, "stop_system")()
super().handle_exit(sig, frame)
APP_FACTORY = "app.factory:create_app"
Server: Optional[MoviePilotServer] = None
def create_server() -> MoviePilotServer:
"""创建不带 reload/multiprocess supervisor 的单进程生产服务器。"""
server = MoviePilotServer(
Config(
app,
host=get_runtime_setting('HOST'),
port=get_runtime_setting('PORT'),
reload=False,
workers=1,
timeout_graceful_shutdown=60,
)
)
# 数据库准备阶段收到的信号早于 Server 物化,创建后必须继承既有停止意图。
if runtime_stop_state.is_system_stopped:
server.should_exit = True
return server
def run_api_server() -> None:
"""按开发 reload、安全模式多进程或生产单进程选择 Uvicorn 入口。"""
global Server
supervised = get_runtime_setting('DEV') or get_runtime_setting('API_WORKERS') > 1
if supervised:
if get_runtime_setting('DEV') and get_runtime_setting('API_WORKERS') > 1:
raise UnsupportedProcessTopologyError(
"Uvicorn reload 与多 worker 不能同时启用;"
"开发模式请设置 API_WORKERS=1。"
)
Server = None
uvicorn.run(
APP_FACTORY,
factory=True,
host=get_runtime_setting('HOST'),
port=get_runtime_setting('PORT'),
reload=get_runtime_setting('DEV'),
# 运行插件及其恢复材料由插件生命周期管理,不属于宿主源码变更。
reload_excludes=[
str(get_runtime_setting('ROOT_PATH') / "app" / "plugins"),
str(get_runtime_setting('CONFIG_PATH')),
]
if get_runtime_setting('DEV')
else None,
workers=get_runtime_setting('API_WORKERS'),
timeout_graceful_shutdown=60,
)
return
Server = create_server()
Server.run()
def request_shutdown() -> None:
"""发布协作停止标志并请求 Uvicorn 退出"""
getattr(global_vars, "stop_system")()
if Server is not None:
Server.should_exit = True
def start_tray():
"""
启动托盘图标
"""
if not SystemUtils.is_frozen():
return
if not SystemUtils.is_windows():
return
def open_web():
"""
调用浏览器打开前端页面
"""
import webbrowser
webbrowser.open(f"http://localhost:{get_runtime_setting('NGINX_PORT')}")
def quit_app():
"""
退出程序
"""
request_shutdown()
TrayIcon.stop()
import pystray
TrayIcon = pystray.Icon(
get_runtime_setting('PROJECT_NAME'),
icon=Image.open(get_runtime_setting('ROOT_PATH') / 'app.ico'),
menu=pystray.Menu(
pystray.MenuItem(
'打开',
open_web,
),
pystray.MenuItem(
'退出',
quit_app,
)
)
)
threading.Thread(target=TrayIcon.run, daemon=True).start()
def signal_handler(signum, frame):
"""
信号处理函数,用于优雅停止服务
"""
print(f"收到信号 {signum},开始优雅停止服务...")
request_shutdown()
def run_application() -> None:
"""初始化进程并启动 API 服务"""
validate_process_topology(
workers=get_runtime_setting('API_WORKERS'),
safe_mode=get_runtime_setting('MOVIEPILOT_SAFE_MODE'),
)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
start_tray()
run_api_server()
if __name__ == '__main__':
run_application()