mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
Merge pull request #2863 from InfinityPacer/feature/setup
This commit is contained in:
+54
-34
@@ -1,14 +1,15 @@
|
|||||||
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
import tailer
|
import aiofiles
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Header, Response
|
from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
@@ -224,19 +225,22 @@ def set_env_setting(env: dict,
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/progress/{process_type}", summary="实时进度")
|
@router.get("/progress/{process_type}", summary="实时进度")
|
||||||
def get_progress(process_type: str, _: schemas.TokenPayload = Depends(verify_resource_token)):
|
async def get_progress(request: Request, process_type: str, _: schemas.TokenPayload = Depends(verify_resource_token)):
|
||||||
"""
|
"""
|
||||||
实时获取处理进度,返回格式为SSE
|
实时获取处理进度,返回格式为SSE
|
||||||
"""
|
"""
|
||||||
progress = ProgressHelper()
|
progress = ProgressHelper()
|
||||||
|
|
||||||
def event_generator():
|
async def event_generator():
|
||||||
while True:
|
try:
|
||||||
if global_vars.is_system_stopped:
|
while not global_vars.is_system_stopped:
|
||||||
break
|
if await request.is_disconnected():
|
||||||
detail = progress.get(process_type)
|
break
|
||||||
yield 'data: %s\n\n' % json.dumps(detail)
|
detail = progress.get(process_type)
|
||||||
time.sleep(0.2)
|
yield f"data: {json.dumps(detail)}\n\n"
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||||
|
|
||||||
@@ -273,26 +277,29 @@ def set_setting(key: str, value: Union[list, dict, bool, int, str] = None,
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/message", summary="实时消息")
|
@router.get("/message", summary="实时消息")
|
||||||
def get_message(role: str = "system", _: schemas.TokenPayload = Depends(verify_resource_token)):
|
async def get_message(request: Request, role: str = "system", _: schemas.TokenPayload = Depends(verify_resource_token)):
|
||||||
"""
|
"""
|
||||||
实时获取系统消息,返回格式为SSE
|
实时获取系统消息,返回格式为SSE
|
||||||
"""
|
"""
|
||||||
message = MessageHelper()
|
message = MessageHelper()
|
||||||
|
|
||||||
def event_generator():
|
async def event_generator():
|
||||||
while True:
|
try:
|
||||||
if global_vars.is_system_stopped:
|
while not global_vars.is_system_stopped:
|
||||||
break
|
if await request.is_disconnected():
|
||||||
detail = message.get(role)
|
break
|
||||||
yield 'data: %s\n\n' % (detail or '')
|
detail = message.get(role)
|
||||||
time.sleep(3)
|
yield f"data: {detail or ''}\n\n"
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logging", summary="实时日志")
|
@router.get("/logging", summary="实时日志")
|
||||||
def get_logging(length: int = 50, logfile: str = "moviepilot.log",
|
async def get_logging(request: Request, length: int = 50, logfile: str = "moviepilot.log",
|
||||||
_: schemas.TokenPayload = Depends(verify_resource_token)):
|
_: schemas.TokenPayload = Depends(verify_resource_token)):
|
||||||
"""
|
"""
|
||||||
实时获取系统日志
|
实时获取系统日志
|
||||||
length = -1 时, 返回text/plain
|
length = -1 时, 返回text/plain
|
||||||
@@ -306,27 +313,40 @@ def get_logging(length: int = 50, logfile: str = "moviepilot.log",
|
|||||||
if not log_path.exists() or not log_path.is_file():
|
if not log_path.exists() or not log_path.is_file():
|
||||||
raise HTTPException(status_code=404, detail="Not Found")
|
raise HTTPException(status_code=404, detail="Not Found")
|
||||||
|
|
||||||
def log_generator():
|
async def log_generator():
|
||||||
# 读取文件末尾50行,不使用tailer模块
|
try:
|
||||||
with open(log_path, 'r', encoding='utf-8') as f:
|
# 使用固定大小的双向队列来限制内存使用
|
||||||
for line in f.readlines()[-max(length, 50):]:
|
lines_queue = deque(maxlen=max(length, 50))
|
||||||
yield 'data: %s\n\n' % line
|
# 使用 aiofiles 异步读取文件
|
||||||
while True:
|
async with aiofiles.open(log_path, mode="r", encoding="utf-8") as f:
|
||||||
if global_vars.is_system_stopped:
|
# 逐行读取文件,将每一行存入队列
|
||||||
break
|
file_content = await f.read()
|
||||||
for t in tailer.follow(open(log_path, 'r', encoding='utf-8')):
|
for line in file_content.splitlines():
|
||||||
yield 'data: %s\n\n' % (t or '')
|
lines_queue.append(line)
|
||||||
time.sleep(1)
|
for line in lines_queue:
|
||||||
|
yield f"data: {line}\n\n"
|
||||||
|
# 移动文件指针到文件末尾,继续监听新增内容
|
||||||
|
await f.seek(0, 2)
|
||||||
|
while not global_vars.is_system_stopped:
|
||||||
|
if await request.is_disconnected():
|
||||||
|
break
|
||||||
|
line = await f.readline()
|
||||||
|
if not line:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
continue
|
||||||
|
yield f"data: {line}\n\n"
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
# 根据length参数返回不同的响应
|
# 根据length参数返回不同的响应
|
||||||
if length == -1:
|
if length == -1:
|
||||||
# 返回全部日志作为文本响应
|
# 返回全部日志作为文本响应
|
||||||
if not log_path.exists():
|
if not log_path.exists():
|
||||||
return Response(content="日志文件不存在!", media_type="text/plain")
|
return Response(content="日志文件不存在!", media_type="text/plain")
|
||||||
with open(log_path, 'r', encoding='utf-8') as file:
|
with open(log_path, "r", encoding='utf-8') as file:
|
||||||
text = file.read()
|
text = file.read()
|
||||||
# 倒序输出
|
# 倒序输出
|
||||||
text = '\n'.join(text.split('\n')[::-1])
|
text = "\n".join(text.split("\n")[::-1])
|
||||||
return Response(content=text, media_type="text/plain")
|
return Response(content=text, media_type="text/plain")
|
||||||
else:
|
else:
|
||||||
# 返回SSE流响应
|
# 返回SSE流响应
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import List, Union, Optional, Generator
|
|||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
|
from app.core.config import global_vars
|
||||||
from app.db.mediaserver_oper import MediaServerOper
|
from app.db.mediaserver_oper import MediaServerOper
|
||||||
from app.helper.service import ServiceConfigHelper
|
from app.helper.service import ServiceConfigHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
@@ -134,6 +135,8 @@ class MediaServerChain(ChainBase):
|
|||||||
logger.info(f"正在同步 {server_name} 媒体库 {library.name} ...")
|
logger.info(f"正在同步 {server_name} 媒体库 {library.name} ...")
|
||||||
library_count = 0
|
library_count = 0
|
||||||
for item in self.items(server=server_name, library_id=library.id):
|
for item in self.items(server=server_name, library_id=library.id):
|
||||||
|
if global_vars.is_system_stopped:
|
||||||
|
return
|
||||||
if not item or not item.item_id:
|
if not item or not item.item_id:
|
||||||
continue
|
continue
|
||||||
logger.debug(f"正在同步 {item.title} ...")
|
logger.debug(f"正在同步 {item.title} ...")
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ from app.utils.singleton import Singleton
|
|||||||
DEFAULT_EVENT_PRIORITY = 10 # 事件的默认优先级
|
DEFAULT_EVENT_PRIORITY = 10 # 事件的默认优先级
|
||||||
MIN_EVENT_CONSUMER_THREADS = 1 # 最小事件消费者线程数
|
MIN_EVENT_CONSUMER_THREADS = 1 # 最小事件消费者线程数
|
||||||
INITIAL_EVENT_QUEUE_IDLE_TIMEOUT_SECONDS = 1 # 事件队列空闲时的初始超时时间(秒)
|
INITIAL_EVENT_QUEUE_IDLE_TIMEOUT_SECONDS = 1 # 事件队列空闲时的初始超时时间(秒)
|
||||||
MAX_EVENT_QUEUE_IDLE_TIMEOUT_SECONDS = 60 # 事件队列空闲时的最大超时时间(秒)
|
MAX_EVENT_QUEUE_IDLE_TIMEOUT_SECONDS = 5 # 事件队列空闲时的最大超时时间(秒)
|
||||||
|
|
||||||
|
|
||||||
class Event:
|
class Event:
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,8 @@ from app.db.init import init_db, update_db
|
|||||||
|
|
||||||
# uvicorn服务
|
# 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()))
|
reload=settings.DEV, workers=multiprocessing.cpu_count(),
|
||||||
|
timeout_graceful_shutdown=5))
|
||||||
|
|
||||||
|
|
||||||
def start_tray():
|
def start_tray():
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Telegram:
|
|||||||
logger.error(f"Telegram消息接收服务异常:{str(err)}")
|
logger.error(f"Telegram消息接收服务异常:{str(err)}")
|
||||||
|
|
||||||
# 启动线程来运行 infinity_polling
|
# 启动线程来运行 infinity_polling
|
||||||
self._polling_thread = threading.Thread(target=run_polling)
|
self._polling_thread = threading.Thread(target=run_polling, daemon=True)
|
||||||
self._polling_thread.start()
|
self._polling_thread.start()
|
||||||
logger.info("Telegram消息接收服务启动")
|
logger.info("Telegram消息接收服务启动")
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ class Monitor(metaclass=Singleton):
|
|||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
# 启动文件整理线程
|
# 启动文件整理线程
|
||||||
self._transfer_thread = threading.Thread(target=self.__start_transfer)
|
self._transfer_thread = threading.Thread(target=self.__start_transfer, daemon=True)
|
||||||
self._transfer_thread.start()
|
self._transfer_thread.start()
|
||||||
|
|
||||||
# 读取目录配置
|
# 读取目录配置
|
||||||
|
|||||||
@@ -14,18 +14,24 @@ async def lifespan(app: FastAPI):
|
|||||||
定义应用的生命周期事件
|
定义应用的生命周期事件
|
||||||
"""
|
"""
|
||||||
print("Starting up...")
|
print("Starting up...")
|
||||||
|
# 启动模块
|
||||||
start_modules(app)
|
start_modules(app)
|
||||||
|
# 初始化路由
|
||||||
init_routers(app)
|
init_routers(app)
|
||||||
|
# 初始化插件
|
||||||
plugin_init_task = asyncio.create_task(init_plugins_async())
|
plugin_init_task = asyncio.create_task(init_plugins_async())
|
||||||
try:
|
try:
|
||||||
|
# 在此处 yield,表示应用已经启动,控制权交回 FastAPI 主事件循环
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
print("Shutting down...")
|
print("Shutting down...")
|
||||||
try:
|
try:
|
||||||
|
# 取消插件初始化
|
||||||
plugin_init_task.cancel()
|
plugin_init_task.cancel()
|
||||||
await plugin_init_task
|
await plugin_init_task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print("Plugin installation task cancelled.")
|
print("Plugin installation task cancelled.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during plugin installation shutdown: {e}")
|
print(f"Error during plugin installation shutdown: {e}")
|
||||||
|
# 清理模块
|
||||||
shutdown_modules(app)
|
shutdown_modules(app)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import signal
|
|
||||||
import sys
|
import sys
|
||||||
from types import FrameType
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from app.core.config import settings, global_vars
|
from app.core.config import global_vars, settings
|
||||||
from app.core.module import ModuleManager
|
from app.core.module import ModuleManager
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
@@ -89,27 +87,12 @@ def check_auth():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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(_: FastAPI):
|
def shutdown_modules(_: FastAPI):
|
||||||
"""
|
"""
|
||||||
服务关闭
|
服务关闭
|
||||||
"""
|
"""
|
||||||
|
# 停止信号
|
||||||
|
global_vars.stop_system()
|
||||||
# 停止模块
|
# 停止模块
|
||||||
ModuleManager().stop()
|
ModuleManager().stop()
|
||||||
# 停止插件
|
# 停止插件
|
||||||
@@ -159,5 +142,3 @@ def start_modules(_: FastAPI):
|
|||||||
start_frontend()
|
start_frontend()
|
||||||
# 检查认证状态
|
# 检查认证状态
|
||||||
check_auth()
|
check_auth()
|
||||||
# 监听停止信号
|
|
||||||
singal_handle()
|
|
||||||
|
|||||||
+2
-2
@@ -45,7 +45,6 @@ psutil~=5.9.4
|
|||||||
python-dotenv~=1.0.1
|
python-dotenv~=1.0.1
|
||||||
python-hosts~=1.0.7
|
python-hosts~=1.0.7
|
||||||
watchdog~=3.0.0
|
watchdog~=3.0.0
|
||||||
tailer~=0.4.1
|
|
||||||
openai~=0.27.2
|
openai~=0.27.2
|
||||||
cacheout~=0.14.1
|
cacheout~=0.14.1
|
||||||
click~=8.1.6
|
click~=8.1.6
|
||||||
@@ -61,4 +60,5 @@ Pinyin2Hanzi~=0.1.1
|
|||||||
pywebpush~=2.0.0
|
pywebpush~=2.0.0
|
||||||
py115j~=0.0.7
|
py115j~=0.0.7
|
||||||
oss2~=2.18.6
|
oss2~=2.18.6
|
||||||
aligo~=6.2.4
|
aligo~=6.2.4
|
||||||
|
aiofiles~=24.1.0
|
||||||
+18
-14
@@ -4,6 +4,8 @@
|
|||||||
#
|
#
|
||||||
# pip-compile requirements.in
|
# pip-compile requirements.in
|
||||||
#
|
#
|
||||||
|
aiofiles==24.1.0
|
||||||
|
# via -r requirements.in
|
||||||
aiohappyeyeballs==2.4.0
|
aiohappyeyeballs==2.4.0
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
aiohttp==3.10.5
|
aiohttp==3.10.5
|
||||||
@@ -63,6 +65,11 @@ click==8.1.7
|
|||||||
# uvicorn
|
# uvicorn
|
||||||
cn2an==0.5.22
|
cn2an==0.5.22
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
colorama==0.4.6
|
||||||
|
# via
|
||||||
|
# click
|
||||||
|
# qrcode
|
||||||
|
# tqdm
|
||||||
coloredlogs==15.0.1
|
coloredlogs==15.0.1
|
||||||
# via aligo
|
# via aligo
|
||||||
crcmod==1.7
|
crcmod==1.7
|
||||||
@@ -95,7 +102,9 @@ frozenlist==1.4.1
|
|||||||
func-timeout==4.3.5
|
func-timeout==4.3.5
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
greenlet==2.0.2
|
greenlet==2.0.2
|
||||||
# via playwright
|
# via
|
||||||
|
# playwright
|
||||||
|
# sqlalchemy
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
# via
|
# via
|
||||||
# httpcore
|
# httpcore
|
||||||
@@ -139,7 +148,9 @@ openai==0.27.10
|
|||||||
oss2==2.18.6
|
oss2==2.18.6
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
packaging==24.1
|
packaging==24.1
|
||||||
# via docker
|
# via
|
||||||
|
# docker
|
||||||
|
# qbittorrent-api
|
||||||
parse==1.19.1
|
parse==1.19.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
@@ -181,14 +192,6 @@ pyee==9.0.4
|
|||||||
# via playwright
|
# via playwright
|
||||||
pyjwt==2.7.0
|
pyjwt==2.7.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pyobjc-core==10.3.1
|
|
||||||
# via
|
|
||||||
# pyobjc-framework-cocoa
|
|
||||||
# pyobjc-framework-quartz
|
|
||||||
pyobjc-framework-cocoa==10.3.1
|
|
||||||
# via pyobjc-framework-quartz
|
|
||||||
pyobjc-framework-quartz==10.3.1
|
|
||||||
# via pystray
|
|
||||||
pyotp==2.9.0
|
pyotp==2.9.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pyparsing==3.0.9
|
pyparsing==3.0.9
|
||||||
@@ -197,6 +200,8 @@ pypng==0.20220715.0
|
|||||||
# via qrcode
|
# via qrcode
|
||||||
pyquery==2.0.0
|
pyquery==2.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
pyreadline3==3.5.4
|
||||||
|
# via humanfriendly
|
||||||
pysocks==1.7.1
|
pysocks==1.7.1
|
||||||
# via requests
|
# via requests
|
||||||
pystray==0.19.5
|
pystray==0.19.5
|
||||||
@@ -223,7 +228,7 @@ pyvirtualdisplay==3.0
|
|||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pywebpush==2.0.0
|
pywebpush==2.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
qbittorrent-api==2023.5.48
|
qbittorrent-api==2024.9.67
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
qrcode[pil]==7.4.2
|
qrcode[pil]==7.4.2
|
||||||
# via
|
# via
|
||||||
@@ -262,7 +267,6 @@ six==1.16.0
|
|||||||
# pystray
|
# pystray
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# pywebpush
|
# pywebpush
|
||||||
# qbittorrent-api
|
|
||||||
slack-bolt==1.18.0
|
slack-bolt==1.18.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
slack-sdk==3.21.3
|
slack-sdk==3.21.3
|
||||||
@@ -283,8 +287,6 @@ starlette==0.27.0
|
|||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# fastapi
|
# fastapi
|
||||||
tailer==0.4.1
|
|
||||||
# via -r requirements.in
|
|
||||||
torrentool==1.2.0
|
torrentool==1.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
tqdm==4.66.5
|
tqdm==4.66.5
|
||||||
@@ -301,6 +303,8 @@ typing-extensions==4.12.2
|
|||||||
# qrcode
|
# qrcode
|
||||||
# sqlalchemy
|
# sqlalchemy
|
||||||
# transmission-rpc
|
# transmission-rpc
|
||||||
|
tzdata==2024.2
|
||||||
|
# via tzlocal
|
||||||
tzlocal==5.2
|
tzlocal==5.2
|
||||||
# via
|
# via
|
||||||
# apscheduler
|
# apscheduler
|
||||||
|
|||||||
Reference in New Issue
Block a user