Compare commits

...

18 Commits

Author SHA1 Message Date
jxxghp
e6105fdab5 **v2.0.3**
- 修复了最新版本号获取错误的问题
- 修复了文件管理重命名失败的问题
- 修复了整理多季时 season.nfo 刮削错误的问题
- 修复了Rclone存储容量检测错误的问题
- 优化了自定义规则,剧集文件大小规则按平均每集大小过滤
- 移动文件整理时,自动删除空的父目录
- 增加了自动阅读和发送站点消息的开关
- 增加了数据库WAL模式开关,开启后提升数据库性能
2024-11-12 18:48:15 +08:00
jxxghp
df34c7e2da Merge pull request #3074 from InfinityPacer/feature/db 2024-11-12 17:30:34 +08:00
InfinityPacer
24cc36033f feat(db): add support for SQLite WAL mode 2024-11-12 17:17:16 +08:00
jxxghp
aafb2bc269 fix #3071 增加站点消息开关 2024-11-12 13:59:13 +08:00
jxxghp
9dde56467a 更新 __init__.py 2024-11-12 12:24:05 +08:00
jxxghp
f9d62e7451 fix Rclone存储容量检测问题 2024-11-12 10:10:37 +08:00
jxxghp
f1f379966a fix 修复V2最新版本号获取 2024-11-12 08:37:07 +08:00
jxxghp
942c9ae545 Merge pull request #3058 from wikrin/fix-scrape_metadata 2024-11-10 14:02:31 +08:00
jxxghp
89be4f6200 Merge pull request #3054 from wikrin/fix-rename 2024-11-10 14:02:01 +08:00
Attente
bcbf729fd4 修复整理多季时season.nfo刮削错误的问题 2024-11-10 13:43:59 +08:00
Attente
7fc5b7678e 更改判断顺序 2024-11-10 07:47:49 +08:00
Attente
e20578685a fix: 修复重命名失败的问题 2024-11-09 23:59:58 +08:00
jxxghp
40b82d9cb6 fix #3042 移动模式删除空文件夹 2024-11-09 18:23:08 +08:00
jxxghp
9b2fccee01 feat:剧集文件大小过滤按平均每集大小 2024-11-09 18:01:50 +08:00
jxxghp
87bbee8c36 Merge pull request #3038 from InfinityPacer/feature/setup 2024-11-08 18:16:32 +08:00
InfinityPacer
4412ce9f17 fix(playwright): add check for HTTPS proxy 2024-11-08 18:08:45 +08:00
jxxghp
35b78b0e66 Merge pull request #3034 from lybtt/fix_update_bash 2024-11-08 16:44:55 +08:00
lvyb
d97fcc4a96 修复update脚本,版本号比较问题 2024-11-08 16:37:36 +08:00
17 changed files with 175 additions and 85 deletions

2
.gitignore vendored
View File

@@ -12,7 +12,7 @@ app/helper/*.bin
app/plugins/**
!app/plugins/__init__.py
config/cookies/**
config/user.db
config/user.db*
config/sites/**
config/logs/
config/temp/

View File

@@ -149,8 +149,10 @@ def rename(fileitem: schemas.FileItem,
:param recursive: 是否递归修改
:param _: token
"""
if not fileitem.fileid or not new_name:
return schemas.Response(success=False)
if not new_name:
return schemas.Response(success=False, message="新名称为空")
if fileitem.storage != 'local' and not fileitem.fileid:
return schemas.Response(success=False, message="资源ID获取失败")
result = StorageChain().rename_file(fileitem, new_name)
if result:
if recursive:

View File

@@ -468,7 +468,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
season_meta = MetaInfo(filepath.name)
if season_meta.begin_season:
# 当前目录有季号生成季nfo
season_nfo = self.metadata_nfo(meta=meta, mediainfo=mediainfo, season=meta.begin_season)
season_nfo = self.metadata_nfo(meta=meta, mediainfo=mediainfo, season=season_meta.begin_season)
if not season_nfo:
logger.warn(f"无法生成电视剧季nfo文件{meta.name}")
return

View File

@@ -37,6 +37,12 @@ class StorageChain(ChainBase):
"""
return self.run_module("list_files", fileitem=fileitem, recursion=recursion)
def any_files(self, fileitem: schemas.FileItem, extensions: list = None) -> Optional[bool]:
"""
查询当前目录下是否存在指定扩展名任意文件
"""
return self.run_module("any_files", fileitem=fileitem, extensions=extensions)
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
"""
创建目录
@@ -116,18 +122,8 @@ class StorageChain(ChainBase):
else:
dir_item = self.get_parent_item(fileitem)
if dir_item:
files = self.list_files(dir_item, recursion=True)
# 是否存在其他媒体文件
media_file_exist = False
if files:
for file in files:
if file.extension and f".{file.extension.lower()}" in settings.RMT_MEDIAEXT:
media_file_exist = True
break
# 不存在其他媒体文件,删除空目录
if not media_file_exist:
# 返回空目录删除状态
if not self.any_files(dir_item, extensions=settings.RMT_MEDIAEXT):
return self.delete_file(dir_item)
# 存在媒体文件,返回文件删除状态

View File

@@ -1,6 +1,5 @@
import json
import re
from pathlib import Path
from typing import Union
from app.chain import ChainBase
@@ -10,6 +9,7 @@ from app.schemas import Notification, MessageChannel
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
from app.utils.system import SystemUtils
from version import FRONTEND_VERSION, APP_VERSION
class SystemChain(ChainBase, metaclass=Singleton):
@@ -98,77 +98,67 @@ class SystemChain(ChainBase, metaclass=Singleton):
@staticmethod
def __get_server_release_version():
"""
获取后端最新版本
获取后端V2最新版本
"""
try:
version_res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS).get_res(
"https://api.github.com/repos/jxxghp/MoviePilot/releases/latest")
if version_res:
ver_json = version_res.json()
version = f"{ver_json['tag_name']}"
return version
# 获取所有发布的版本列表
response = RequestUtils(
proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS
).get_res("https://api.github.com/repos/jxxghp/MoviePilot/releases")
if response:
releases = [release['tag_name'] for release in response.json()]
v2_releases = [tag for tag in releases if re.match(r"^v2\.", tag)]
if not v2_releases:
logger.warn("获取v2后端最新版本版本出错")
else:
# 找到最新的v2版本
latest_v2 = sorted(v2_releases, key=lambda s: list(map(int, re.findall(r'\d+', s))))[-1]
logger.info(f"获取到后端最新版本:{latest_v2}")
return latest_v2
else:
return None
logger.error("无法获取后端版本信息请检查网络连接或GitHub API请求。")
except Exception as err:
logger.error(f"获取后端最新版本失败:{str(err)}")
return None
return None
@staticmethod
def __get_front_release_version():
"""
获取前端最新版本
获取前端V2最新版本
"""
try:
version_res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS).get_res(
"https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest")
if version_res:
ver_json = version_res.json()
version = f"{ver_json['tag_name']}"
return version
# 获取所有发布的版本列表
response = RequestUtils(
proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS
).get_res("https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases")
if response:
releases = [release['tag_name'] for release in response.json()]
v2_releases = [tag for tag in releases if re.match(r"^v2\.", tag)]
if not v2_releases:
logger.warn("获取v2前端最新版本版本出错")
else:
# 找到最新的v2版本
latest_v2 = sorted(v2_releases, key=lambda s: list(map(int, re.findall(r'\d+', s))))[-1]
logger.info(f"获取到前端最新版本:{latest_v2}")
return latest_v2
else:
return None
logger.error("无法获取前端版本信息请检查网络连接或GitHub API请求。")
except Exception as err:
logger.error(f"获取前端最新版本失败:{str(err)}")
return None
return None
@staticmethod
def get_server_local_version():
"""
查看当前版本
"""
version_file = settings.ROOT_PATH / "version.py"
if version_file.exists():
try:
with open(version_file, 'rb') as f:
version = f.read()
pattern = r"'([^']*)'"
match = re.search(pattern, str(version))
if match:
version = match.group(1)
return version
else:
logger.warn("未找到版本号")
return None
except Exception as err:
logger.error(f"加载版本文件 {version_file} 出错:{str(err)}")
return APP_VERSION
@staticmethod
def get_frontend_version():
"""
获取前端版本
"""
if SystemUtils.is_frozen() and SystemUtils.is_windows():
version_file = settings.CONFIG_PATH.parent / "nginx" / "html" / "version.txt"
else:
version_file = Path(settings.FRONTEND_PATH) / "version.txt"
if version_file.exists():
try:
with open(version_file, 'r') as f:
version = str(f.read()).strip()
return version
except Exception as err:
logger.error(f"加载版本文件 {version_file} 出错:{str(err)}")
else:
logger.warn("未找到前端版本文件,请正确设置 FRONTEND_PATH")
return None
return FRONTEND_VERSION

View File

@@ -494,7 +494,13 @@ class TransferChain(ChainBase):
# 删除残留文件
if fileitem:
logger.warn(f"删除残留文件夹:【{fileitem.storage}{fileitem.path}")
self.storagechain.delete_file(fileitem)
if self.storagechain.delete_file(fileitem):
# 删除空的父目录
dir_item = self.storagechain.get_parent_item(fileitem)
if dir_item:
if not self.storagechain.any_files(dir_item, extensions=settings.RMT_MEDIAEXT):
logger.warn(f"正在删除空目录:【{dir_item.storage}{dir_item.path}")
return self.storagechain.delete_file(dir_item)
# 结束进度
logger.info(f"{fileitem.path} 整理完成,共 {total_num} 个文件,"

View File

@@ -69,6 +69,8 @@ class ConfigModel(BaseModel):
DB_MAX_OVERFLOW: int = 500
# SQLite 的 busy_timeout 参数,默认为 60 秒
DB_TIMEOUT: int = 60
# SQLite 是否启用 WAL 模式,默认关闭
DB_WAL_ENABLE: bool = False
# 配置文件目录
CONFIG_DIR: Optional[str] = None
# 超级管理员
@@ -151,6 +153,8 @@ class ConfigModel(BaseModel):
SEARCH_MULTIPLE_NAME: bool = False
# 站点数据刷新间隔(小时)
SITEDATA_REFRESH_INTERVAL: int = 6
# 读取和发送站点消息
SITE_MESSAGE: bool = True
# 种子标签
TORRENT_TAG: str = "MOVIEPILOT"
# 下载站点字幕

View File

@@ -1,22 +1,25 @@
from typing import Any, Generator, List, Optional, Self, Tuple
from sqlalchemy import NullPool, QueuePool, and_, create_engine, inspect
from sqlalchemy import NullPool, QueuePool, and_, create_engine, inspect, text
from sqlalchemy.orm import Session, as_declarative, declared_attr, scoped_session, sessionmaker
from app.core.config import settings
# 根据池类型设置 poolclass 和相关参数
pool_class = NullPool if settings.DB_POOL_TYPE == "NullPool" else QueuePool
connect_args = {
"timeout": settings.DB_TIMEOUT
}
# 启用 WAL 模式时的额外配置
if settings.DB_WAL_ENABLE:
connect_args["check_same_thread"] = False
kwargs = {
"url": f"sqlite:///{settings.CONFIG_PATH}/user.db",
"pool_pre_ping": settings.DB_POOL_PRE_PING,
"echo": settings.DB_ECHO,
"poolclass": pool_class,
"pool_recycle": settings.DB_POOL_RECYCLE,
"connect_args": {
# "check_same_thread": False,
"timeout": settings.DB_TIMEOUT
}
"connect_args": connect_args
}
# 当使用 QueuePool 时,添加 QueuePool 特有的参数
if pool_class == QueuePool:
@@ -27,6 +30,11 @@ if pool_class == QueuePool:
})
# 创建数据库引擎
Engine = create_engine(**kwargs)
# 根据配置设置日志模式
journal_mode = "WAL" if settings.DB_WAL_ENABLE else "DELETE"
with Engine.connect() as connection:
current_mode = connection.execute(text(f"PRAGMA journal_mode={journal_mode};")).scalar()
print(f"Database journal mode set to: {current_mode}")
# 会话工厂
SessionFactory = sessionmaker(bind=Engine)
@@ -49,11 +57,34 @@ def get_db() -> Generator:
db.close()
def perform_checkpoint(mode: str = "PASSIVE"):
"""
执行 SQLite 的 checkpoint 操作,将 WAL 文件内容写回主数据库
:param mode: checkpoint 模式,可选值包括 "PASSIVE""FULL""RESTART""TRUNCATE"
默认为 "PASSIVE",即不锁定 WAL 文件的轻量级同步
"""
if not settings.DB_WAL_ENABLE:
return
valid_modes = {"PASSIVE", "FULL", "RESTART", "TRUNCATE"}
if mode.upper() not in valid_modes:
raise ValueError(f"Invalid checkpoint mode '{mode}'. Must be one of {valid_modes}")
try:
# 使用指定的 checkpoint 模式,确保 WAL 文件数据被正确写回主数据库
with Engine.connect() as conn:
conn.execute(text(f"PRAGMA wal_checkpoint({mode.upper()});"))
except Exception as e:
print(f"Error during WAL checkpoint: {e}")
def close_database():
"""
关闭所有数据库连接
关闭所有数据库连接并清理资源
"""
Engine.dispose()
try:
# 释放连接池SQLite 会自动清空 WAL 文件,这里不单独再调用 checkpoint
Engine.dispose()
except Exception as e:
print(f"Error while disposing database connections: {e}")
def get_args_db(args: tuple, kwargs: dict) -> Optional[Session]:

View File

@@ -197,6 +197,35 @@ class FileManagerModule(_ModuleBase):
return result
def any_files(self, fileitem: FileItem, extensions: list = None) -> Optional[bool]:
"""
查询当前目录下是否存在指定扩展名任意文件
"""
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件浏览")
return None
def __any_file(_item: FileItem):
"""
递归处理
"""
_items = storage_oper.list(_item)
if _items:
if not extensions:
return True
for t in _items:
if (t.type == "file"
and t.extension
and f".{t.extension.lower()}" in extensions):
return True
elif t.type == "dir":
return __any_file(t)
return False
# 返回结果
return __any_file(fileitem)
def create_folder(self, fileitem: FileItem, name: str) -> Optional[FileItem]:
"""
创建目录

View File

@@ -331,11 +331,24 @@ class Rclone(StorageBase):
"""
存储使用情况
"""
conf = self.get_config()
if not conf:
return None
file_path = conf.config.get("filepath")
if not file_path or not Path(file_path).exists():
return None
# 读取rclone文件检查是否有[MP]节点配置
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
if not lines:
return None
if not any("[MP]" in line.strip() for line in lines):
return None
try:
ret = subprocess.run(
[
'rclone', 'about',
'/', '--json'
'MP:/', '--json'
],
capture_output=True,
startupinfo=self.__get_hidden_shell()

View File

@@ -432,26 +432,32 @@ class FilterModule(_ModuleBase):
@staticmethod
def __match_size(torrent: TorrentInfo, size_range: str) -> bool:
"""
判断种子是否匹配大小范围MB
判断种子是否匹配大小范围MB,剧集拆分为每集大小
"""
if not size_range:
return True
# 集数
meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
episode_count = meta.total_episode or 1
# 每集大小
torrent_size = torrent.size / episode_count
# 大小范围
size_range = size_range.strip()
if size_range.find("-") != -1:
# 区间
size_min, size_max = size_range.split("-")
size_min = float(size_min.strip()) * 1024 * 1024
size_max = float(size_max.strip()) * 1024 * 1024
if size_min <= torrent.size <= size_max:
if size_min <= torrent_size <= size_max:
return True
elif size_range.startswith(">"):
# 大于
size_min = float(size_range[1:].strip()) * 1024 * 1024
if torrent.size >= size_min:
if torrent_size >= size_min:
return True
elif size_range.startswith("<"):
# 小于
size_max = float(size_range[1:].strip()) * 1024 * 1024
if torrent.size <= size_max:
if torrent_size <= size_max:
return True
return False

View File

@@ -182,7 +182,8 @@ class SiteParserBase(metaclass=ABCMeta):
)
)
# 解析用户未读消息
self._pase_unread_msgs()
if settings.SITE_MESSAGE:
self._pase_unread_msgs()
# 解析用户上传、下载、分享率等信息
if self._user_traffic_page:
self._parse_user_traffic_info(

View File

@@ -481,7 +481,13 @@ class Monitor(metaclass=Singleton):
# 移动模式删除空目录
if transferinfo.transfer_type in ["move"]:
logger.info(f"正在删除: {file_item.storage} {file_item.path}")
self.storagechain.delete_file(file_item)
if self.storagechain.delete_file(file_item):
# 删除空的父目录
dir_item = self.storagechain.get_parent_item(file_item)
if dir_item:
if not self.storagechain.any_files(dir_item, extensions=settings.RMT_MEDIAEXT):
logger.warn(f"正在删除空目录: {dir_item.storage} {dir_item.path}")
return self.storagechain.delete_file(dir_item)
except Exception as e:
logger.error("目录监控发生错误:%s - %s" % (str(e), traceback.format_exc()))

View File

@@ -15,6 +15,8 @@ DB_POOL_SIZE=100
DB_MAX_OVERFLOW=500
# SQLite 的 busy_timeout 参数可适当增加如180以减少锁定错误
DB_TIMEOUT=60
# SQLite 是否启用 WAL 模式,启用可提升读写并发性能,但可能在异常情况下增加数据丢失的风险
DB_WAL_ENABLE=false
# 【*】超级管理员,设置后一但重启将固化到数据库中,修改将无效(初始化超级管理员密码仅会生成一次,请在日志中查看并自行登录系统修改)
SUPERUSER=admin
# 辅助认证,允许通过外部服务进行认证、单点登录以及自动创建用户

View File

@@ -21,7 +21,11 @@ chown -R moviepilot:moviepilot \
/var/log/nginx
chown moviepilot:moviepilot /etc/hosts /tmp
# 下载浏览器内核
HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$PROXY_HOST}}" gosu moviepilot:moviepilot playwright install chromium
if [[ "$HTTPS_PROXY" =~ ^https?:// ]] || [[ "$https_proxy" =~ ^https?:// ]] || [[ "$PROXY_HOST" =~ ^https?:// ]]; then
HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$PROXY_HOST}}" gosu moviepilot:moviepilot playwright install chromium
else
gosu moviepilot:moviepilot playwright install chromium
fi
# 启动前端nginx服务
nginx
# 启动docker http proxy nginx

6
update
View File

@@ -212,14 +212,14 @@ function compare_versions() {
return 1
elif (( current_ver < release_ver )); then
INFO "发现新版本,开始自动升级..."
install_backend_and_download_resources "tags/${release_ver}.zip"
install_backend_and_download_resources "tags/$2.zip"
return 0
else
WARN "当前版本已是最新版本,跳过更新步骤..."
return 1
continue
fi
fi
done
WARN "当前版本已是最新版本,跳过更新步骤..."
}
# 优先级转换

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v2.0.2'
FRONTEND_VERSION = 'v2.0.2'
APP_VERSION = 'v2.0.3'
FRONTEND_VERSION = 'v2.0.3'