fix 目录监控v2.0

This commit is contained in:
thsrite
2024-05-24 10:35:39 +08:00
parent c07d88ae2e
commit 28c27bdaa6
2 changed files with 154 additions and 103 deletions

View File

@@ -442,5 +442,18 @@
"v1.1": "支持更多返回值、插件展示数据",
"v1.0": "HomePage自定义API"
}
}
},
"DirMonitor": {
"name": "目录监控",
"description": "监控目录文件发生变化时实时整理到媒体库。",
"labels": "文件整理",
"version": "2.0",
"icon": "directory.png",
"author": "jxxghp",
"level": 1,
"history": {
"v2.0": "增强API安全性",
"v1.9": "修复目录监控不能正确获取下载历史记录进行识别的问题"
}
}
}

View File

@@ -24,7 +24,7 @@ from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.transferhistory_oper import TransferHistoryOper
from app.log import logger
from app.plugins import _PluginBase
from app.schemas import Notification, NotificationType, TransferInfo
from app.schemas import NotificationType, TransferInfo
from app.schemas.types import EventType, MediaType, SystemConfigKey
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
@@ -55,11 +55,11 @@ class DirMonitor(_PluginBase):
# 插件名称
plugin_name = "目录监控"
# 插件描述
plugin_desc = "监控目录文件发生变化时实时整理到媒体库(统一入库消息)"
plugin_desc = "监控目录文件发生变化时实时整理到媒体库。"
# 插件图标
plugin_icon = "directory.png"
# 插件版本
plugin_version = "1.3"
plugin_version = "2.0"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -82,6 +82,7 @@ class DirMonitor(_PluginBase):
_notify = False
_onlyonce = False
_cron = None
_size = 0
# 模式 compatibility/fast
_mode = "fast"
# 转移方式
@@ -117,6 +118,7 @@ class DirMonitor(_PluginBase):
self._exclude_keywords = config.get("exclude_keywords") or ""
self._interval = config.get("interval") or 10
self._cron = config.get("cron")
self._size = config.get("size") or 0
# 停止现有任务
self.stop_service()
@@ -169,7 +171,7 @@ class DirMonitor(_PluginBase):
# 检查媒体库目录是不是下载目录的子目录
try:
if target_path and target_path.is_relative_to(Path(mon_path)):
logger.warn(f"{target_path}下载目录 {mon_path} 的子目录,无法监控")
logger.warn(f"{target_path}监控目录 {mon_path} 的子目录,无法监控")
self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控")
continue
except Exception as e:
@@ -214,17 +216,6 @@ class DirMonitor(_PluginBase):
# 保存配置
self.__update_config()
# 全量同步定时
if self._enabled and self._cron:
try:
self._scheduler.add_job(func=self.sync_all,
trigger=CronTrigger.from_crontab(self._cron),
name="目录监控全量同步")
except Exception as err:
logger.error(f"定时任务配置错误:{str(err)}")
# 推送实时消息
self.systemmessage.put(f"执行周期配置错误:{str(err)}")
# 启动定时服务
if self._scheduler.get_jobs():
self._scheduler.print_jobs()
@@ -243,7 +234,8 @@ class DirMonitor(_PluginBase):
"monitor_dirs": self._monitor_dirs,
"exclude_keywords": self._exclude_keywords,
"interval": self._interval,
"cron": self._cron
"cron": self._cron,
"size": self._size
})
@eventmanager.register(EventType.PluginAction)
@@ -336,14 +328,17 @@ class DirMonitor(_PluginBase):
return
# 判断是不是蓝光目录
bluray_flag = False
if re.search(r"BDMV[/\\]STREAM", event_path, re.IGNORECASE):
bluray_flag = True
# 截取BDMV前面的路径
event_path = event_path[:event_path.find("BDMV")]
file_path = Path(event_path)
blurray_dir = event_path[:event_path.find("BDMV")]
file_path = Path(blurray_dir)
logger.info(f"{event_path} 是蓝光目录,更正文件路径为:{str(file_path)}")
# 查询历史记录,已转移的不处理
if self.transferhis.get_by_src(event_path):
logger.info(f"{event_path} 已整理过")
if self.transferhis.get_by_src(str(file_path)):
logger.info(f"{file_path} 已整理过")
return
# 元数据
@@ -352,15 +347,32 @@ class DirMonitor(_PluginBase):
logger.error(f"{file_path.name} 无法识别有效信息")
return
# 判断文件大小
if self._size and float(self._size) > 0 and file_path.stat().st_size < float(self._size) * 1024 ** 3:
logger.info(f"{file_path} 文件大小小于监控文件大小,不处理")
return
# 查询转移目的目录
target: Path = self._dirconf.get(mon_path)
# 查询转移方式
transfer_type = self._transferconf.get(mon_path)
# 根据父路径获取下载历史
download_history = self.downloadhis.get_by_path(Path(event_path).parent)
download_history = None
if bluray_flag:
# 蓝光原盘,按目录名查询
# FIXME 理论上DownloadHistory表中的path应该是全路径但实际表中登记的数据只有目录名暂按目录名查询
download_history = self.downloadhis.get_by_path(file_path.name)
else:
# 按文件全路径查询
download_file = self.downloadhis.get_file_by_fullpath(str(file_path))
if download_file:
download_history = self.downloadhis.get_by_hash(download_file.download_hash)
# 识别媒体信息
mediainfo: MediaInfo = self.chain.recognize_media(meta=file_meta,
mtype=MediaType(
download_history.type) if download_history else None,
tmdbid=download_history.tmdbid if download_history else None)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{file_meta.name}')
@@ -371,11 +383,11 @@ class DirMonitor(_PluginBase):
meta=file_meta
)
if self._notify:
self.chain.post_message(Notification(
self.post_message(
mtype=NotificationType.Manual,
title=f"{file_path.name} 未识别到媒体信息,无法入库!\n"
f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。"
))
)
return
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
@@ -396,8 +408,10 @@ class DirMonitor(_PluginBase):
else:
episodes_info = None
# 获取downloadhash
download_hash = self.get_download_hash(src=str(file_path))
# 获取下载Hash
download_hash = None
if download_history:
download_hash = download_history.download_hash
# 转移
transferinfo: TransferInfo = self.chain.transfer(mediainfo=mediainfo,
@@ -410,6 +424,7 @@ class DirMonitor(_PluginBase):
if not transferinfo:
logger.error("文件转移模块运行失败")
return
if not transferinfo.success:
# 转移失败
logger.warn(f"{file_path.name} 入库失败:{transferinfo.message}")
@@ -423,12 +438,12 @@ class DirMonitor(_PluginBase):
transferinfo=transferinfo
)
if self._notify:
self.chain.post_message(Notification(
self.post_message(
mtype=NotificationType.Manual,
title=f"{mediainfo.title_year}{file_meta.season_episode} 入库失败!",
text=f"原因:{transferinfo.message or '未知'}",
image=mediainfo.get_message_image()
))
)
return
# 新增转移成功历史记录
@@ -458,8 +473,7 @@ class DirMonitor(_PluginBase):
"transferinfo":
}
],
"time": "2023-08-24 23:23:23.332",
"all_files_cnt": 20
"time": "2023-08-24 23:23:23.332"
}
}
"""
@@ -470,21 +484,20 @@ class DirMonitor(_PluginBase):
if media_files:
file_exists = False
for file in media_files:
if str(event_path) == file.get("path"):
if str(file_path) == file.get("path"):
file_exists = True
break
if not file_exists:
media_files.append({
"path": event_path,
"path": str(file_path),
"mediainfo": mediainfo,
"file_meta": file_meta,
"transferinfo": transferinfo,
"transferinfo": transferinfo
})
else:
media_files = [
{
"path": event_path,
"path": str(file_path),
"mediainfo": mediainfo,
"file_meta": file_meta,
"transferinfo": transferinfo
@@ -492,24 +505,19 @@ class DirMonitor(_PluginBase):
]
media_list = {
"files": media_files,
"time": datetime.datetime.now(),
"all_files_cnt": media_list.get("all_files_cnt")
"time": datetime.datetime.now()
}
else:
# 获取当前媒体本次下载的文件数
recent_download_files_cnt = self.__get_recent_download_files_cnt(download_hash=download_hash)
media_list = {
"files": [
{
"path": event_path,
"path": str(file_path),
"mediainfo": mediainfo,
"file_meta": file_meta,
"transferinfo": transferinfo
}
],
"time": datetime.datetime.now(),
"all_files_cnt": recent_download_files_cnt
"time": datetime.datetime.now()
}
self._medias[mediainfo.title_year + " " + file_meta.season] = media_list
@@ -526,7 +534,7 @@ class DirMonitor(_PluginBase):
if len(str(file_dir)) <= len(str(Path(mon_path))):
# 重要,删除到监控目录为止
break
files = SystemUtils.list_files(file_dir, settings.RMT_MEDIAEXT)
files = SystemUtils.list_files(file_dir, settings.RMT_MEDIAEXT + settings.DOWNLOAD_TMPEXT)
if not files:
logger.warn(f"移动模式,删除空目录:{file_dir}")
shutil.rmtree(file_dir, ignore_errors=True)
@@ -552,8 +560,6 @@ class DirMonitor(_PluginBase):
# 获取最后更新时间
last_update_time = media_list.get("time")
media_files = media_list.get("files")
all_files_cnt = media_list.get("all_files_cnt") or 0
retry_cnt = media_list.get("retry_cnt") or 0
if not last_update_time or not media_files:
continue
@@ -563,15 +569,6 @@ class DirMonitor(_PluginBase):
# 判断剧集最后更新时间距现在是已超过10秒或者电影发送消息
if (datetime.datetime.now() - last_update_time).total_seconds() > int(self._interval) \
or mediainfo.type == MediaType.MOVIE:
# 如果当前入库的媒体数据 < 本次批量下载的文件数量暂不处理等待一会容错最大retry 5次
if mediainfo.type == MediaType.TV and all_files_cnt > 0 and len(
media_files) < all_files_cnt and retry_cnt < 5:
# 更新重试次数
media_list['retry_cnt'] = retry_cnt + 1
self._medias[medis_title_year_season] = media_list
continue
# 发送通知
if self._notify:
@@ -609,49 +606,6 @@ class DirMonitor(_PluginBase):
del self._medias[medis_title_year_season]
continue
def __get_recent_download_files_cnt(self, download_hash: str):
"""
1。根据download_hash查询下载历史
2。查询该下载历史记录创建时间前1分钟及以后的所有的该type和tmdbid下的下载历史订阅批量下载的话下载时间间隔应该不会超过一分钟吧。
3。根据查询到的下载历史列表遍历查询对应的下载文件记录
4。根据统计的下载文件记录数目等待入库消息统一发送。
5。统一入库消息如果当前入库的媒体数据 < 本次批量下载的文件数量暂不处理等待一会容错最大retry 5次
"""
# 根据download_hash查询下载记录
recent_download_files = 0
try:
download_history = self.downloadhis.get_by_hash(download_hash=download_hash)
if download_history:
# 根据下载历史查询 下载时间前2分钟及以后的下载记录
# 将时间字符串转换为datetime对象 - 减去2分钟
new_dt = datetime.datetime.strptime(download_history.date, "%Y-%m-%d %H:%M:%S") - datetime.timedelta(
minutes=2)
download_historys = self.downloadhis.list_by_date(date=new_dt.strftime("%Y-%m-%d %H:%M:%S"),
type=download_history.type,
tmdbid=download_history.tmdbid,
seasons=download_history.seasons)
if download_historys:
for download_his in download_historys:
# 根据download_hash获取下载文件列表
download_files = self.downloadhis.get_files_by_hash(
download_hash=download_his.download_hash,
state=1)
if download_files:
recent_download_files += len(download_files)
except Exception as e:
print(str(e))
return recent_download_files
def get_download_hash(self, src: str):
"""
从表中获取download_hash避免连接下载器
"""
download_file = self.downloadhis.get_file_by_fullpath(src)
if download_file:
return download_file.download_hash
return None
def get_state(self) -> bool:
return self._enabled
@@ -680,10 +634,33 @@ class DirMonitor(_PluginBase):
"description": "目录监控同步",
}]
def sync(self) -> schemas.Response:
def get_service(self) -> List[Dict[str, Any]]:
"""
注册插件公共服务
[{
"id": "服务ID",
"name": "服务名称",
"trigger": "触发器cron/interval/date/CronTrigger.from_crontab()",
"func": self.xxx,
"kwargs": {} # 定时器参数
}]
"""
if self._enabled and self._cron:
return [{
"id": "DirMonitor",
"name": "目录监控全量同步服务",
"trigger": CronTrigger.from_crontab(self._cron),
"func": self.sync_all,
"kwargs": {}
}]
return []
def sync(self, apikey: str) -> schemas.Response:
"""
API调用目录同步
"""
if apikey != settings.API_TOKEN:
return schemas.Response(success=False, message="API密钥错误")
self.sync_all()
return schemas.Response(success=True)
@@ -784,7 +761,7 @@ class DirMonitor(_PluginBase):
{'title': '移动', 'value': 'move'},
{'title': '复制', 'value': 'copy'},
{'title': '硬链接', 'value': 'link'},
{'title': '软链接', 'value': 'filesoftlink'},
{'title': '软链接', 'value': 'softlink'},
{'title': 'Rclone复制', 'value': 'rclone_copy'},
{'title': 'Rclone移动', 'value': 'rclone_move'}
]
@@ -817,7 +794,8 @@ class DirMonitor(_PluginBase):
{
'component': 'VCol',
'props': {
'cols': 12
'cols': 12,
'md': 4
},
'content': [
{
@@ -829,6 +807,23 @@ class DirMonitor(_PluginBase):
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'size',
'label': '监控文件大小GB',
'placeholder': '0'
}
}
]
}
]
},
@@ -847,7 +842,7 @@ class DirMonitor(_PluginBase):
'model': 'monitor_dirs',
'label': '监控目录',
'rows': 5,
'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、filesoftlink、rclone_copy、rclone_move\n'
'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、softlink、rclone_copy、rclone_move\n'
'监控目录\n'
'监控目录#转移方式\n'
'监控目录:转移目的目录\n'
@@ -880,6 +875,27 @@ class DirMonitor(_PluginBase):
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '监控目录不指定目的目录时,将转移到媒体库目录,并自动创建一级分类目录,同时按配置创建二级分类目录;监控目录指定了目的目录时,不会自动创建一级目录,但会根据配置创建二级分类目录。'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
@@ -900,6 +916,27 @@ class DirMonitor(_PluginBase):
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '监控文件大小单位GB0为不开启低于监控文件大小的文件不会被监控转移。'
}
}
]
}
]
}
]
}
@@ -912,7 +949,8 @@ class DirMonitor(_PluginBase):
"monitor_dirs": "",
"exclude_keywords": "",
"interval": 10,
"cron": ""
"cron": "",
"size": 0
}
def get_page(self) -> List[dict]: