From 2af25e8506d04699a93c4bf8b0fa15176755fabd Mon Sep 17 00:00:00 2001 From: thsrite Date: Thu, 18 Apr 2024 10:23:10 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E6=96=87=E4=BB=B6=E8=BD=AF=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + icons/softlink.png | Bin 0 -> 4821 bytes package.json | 11 + plugins/cloudlinkmonitor/__init__.py | 34 +- plugins/dirmonitor/__init__.py | 4 +- plugins/filesoftlink/__init__.py | 607 +++++++++++++++++++++++++++ plugins/shortplaymonitor/__init__.py | 4 +- 7 files changed, 638 insertions(+), 23 deletions(-) create mode 100644 icons/softlink.png create mode 100644 plugins/filesoftlink/__init__.py diff --git a/README.md b/README.md index 1292dee..68e8aaf 100644 --- a/README.md +++ b/README.md @@ -32,4 +32,5 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ - [自定义命令 1.5](docs%2FCustomCommand.md) - docker自定义任务 1.2 - 插件彻底卸载 1.0 +- 文件软连接 1.0 diff --git a/icons/softlink.png b/icons/softlink.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf57b31f4555f220f223718a7b8362025952b83 GIT binary patch literal 4821 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 t>B92UC^G4>FS_8M6{{e8W?oj{$ literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 6dcaf15..ad19f02 100644 --- a/package.json +++ b/package.json @@ -302,5 +302,16 @@ "history": { "v1.0": "init" } + }, + "FileSoftLink": { + "name": "文件软连接", + "description": "监控目录文件变化,媒体文件软连接,其他文件可选复制。", + "version": "1.0", + "icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/softlink.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.0": "init" + } } } diff --git a/plugins/cloudlinkmonitor/__init__.py b/plugins/cloudlinkmonitor/__init__.py index 08e32fb..6964e20 100644 --- a/plugins/cloudlinkmonitor/__init__.py +++ b/plugins/cloudlinkmonitor/__init__.py @@ -219,8 +219,9 @@ class CloudLinkMonitor(_PluginBase): # 运行一次定时服务 if self._onlyonce: - logger.info("目录监控服务启动,立即运行一次") - self._scheduler.add_job(func=self.sync_all, trigger='date', + logger.info("云盘实时监控服务启动,立即运行一次") + self._scheduler.add_job(name="云盘实时监控", + func=self.sync_all, trigger='date', run_date=datetime.datetime.now( tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3) ) @@ -258,7 +259,7 @@ class CloudLinkMonitor(_PluginBase): """ if event: event_data = event.event_data - if not event_data or event_data.get("action") != "directory_sync": + if not event_data or event_data.get("action") != "cloud_link_sync": return self.post_message(channel=event.event_data.get("channel"), title="开始同步监控目录 ...", @@ -347,11 +348,6 @@ class CloudLinkMonitor(_PluginBase): file_path = Path(blurray_dir) logger.info(f"{event_path} 是蓝光目录,更正文件路径为:{str(file_path)}") - # 查询历史记录,已转移的不处理 - if self.transferhis.get_by_src(str(file_path)): - logger.info(f"{file_path} 已整理过") - return - # 元数据 file_meta = MetaInfoPath(file_path) if not file_meta.name: @@ -649,22 +645,22 @@ class CloudLinkMonitor(_PluginBase): :return: 命令关键字、事件、描述、附带数据 """ return [{ - "cmd": "/directory_sync", + "cmd": "/cloud_link_sync", "event": EventType.PluginAction, - "desc": "目录监控同步", - "category": "管理", + "desc": "云盘实时监控同步", + "category": "", "data": { - "action": "directory_sync" + "action": "cloud_link_sync" } }] def get_api(self) -> List[Dict[str, Any]]: return [{ - "path": "/directory_sync", + "path": "/cloud_link_sync", "endpoint": self.sync, "methods": ["GET"], - "summary": "目录监控同步", - "description": "目录监控同步", + "summary": "云盘实时监控同步", + "description": "云盘实时监控同步", }] def get_service(self) -> List[Dict[str, Any]]: @@ -680,8 +676,8 @@ class CloudLinkMonitor(_PluginBase): """ if self._enabled and self._cron: return [{ - "id": "DirMonitor", - "name": "目录监控全量同步服务", + "id": "CloudLinkMonitor", + "name": "云盘实时监控全量同步服务", "trigger": CronTrigger.from_crontab(self._cron), "func": self.sync_all, "kwargs": {} @@ -792,7 +788,7 @@ class CloudLinkMonitor(_PluginBase): {'title': '移动', 'value': 'move'}, {'title': '复制', 'value': 'copy'}, {'title': '硬链接', 'value': 'link'}, - {'title': '软链接', 'value': 'softlink'}, + {'title': '软链接', 'value': 'filesoftlink'}, {'title': 'Rclone复制', 'value': 'rclone_copy'}, {'title': 'Rclone移动', 'value': 'rclone_move'} ] @@ -873,7 +869,7 @@ class CloudLinkMonitor(_PluginBase): 'model': 'monitor_dirs', 'label': '监控目录', 'rows': 5, - 'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、softlink、rclone_copy、rclone_move:\n' + 'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、filesoftlink、rclone_copy、rclone_move:\n' '监控目录:转移目的目录\n' '监控目录:转移目的目录$是否刮削(True/False)\n' '监控目录:转移目的目录#转移方式\n' diff --git a/plugins/dirmonitor/__init__.py b/plugins/dirmonitor/__init__.py index 803bae2..a03b409 100644 --- a/plugins/dirmonitor/__init__.py +++ b/plugins/dirmonitor/__init__.py @@ -784,7 +784,7 @@ class DirMonitor(_PluginBase): {'title': '移动', 'value': 'move'}, {'title': '复制', 'value': 'copy'}, {'title': '硬链接', 'value': 'link'}, - {'title': '软链接', 'value': 'softlink'}, + {'title': '软链接', 'value': 'filesoftlink'}, {'title': 'Rclone复制', 'value': 'rclone_copy'}, {'title': 'Rclone移动', 'value': 'rclone_move'} ] @@ -847,7 +847,7 @@ class DirMonitor(_PluginBase): 'model': 'monitor_dirs', 'label': '监控目录', 'rows': 5, - 'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、softlink、rclone_copy、rclone_move:\n' + 'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、filesoftlink、rclone_copy、rclone_move:\n' '监控目录\n' '监控目录#转移方式\n' '监控目录:转移目的目录\n' diff --git a/plugins/filesoftlink/__init__.py b/plugins/filesoftlink/__init__.py new file mode 100644 index 0000000..25556a0 --- /dev/null +++ b/plugins/filesoftlink/__init__.py @@ -0,0 +1,607 @@ +import datetime +import os +import re +import shutil +import threading +import traceback +from pathlib import Path +from typing import List, Tuple, Dict, Any, Optional + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer +from watchdog.observers.polling import PollingObserver + +from app import schemas +from app.core.config import settings +from app.core.event import eventmanager, Event +from app.core.metainfo import MetaInfoPath +from app.log import logger +from app.plugins import _PluginBase +from app.schemas.types import EventType, MediaType, SystemConfigKey +from app.utils.system import SystemUtils + +lock = threading.Lock() + + +class FileMonitorHandler(FileSystemEventHandler): + """ + 目录监控响应类 + """ + + def __init__(self, monpath: str, sync: Any, **kwargs): + super(FileMonitorHandler, self).__init__(**kwargs) + self._watch_path = monpath + self.sync = sync + + def on_created(self, event): + self.sync.event_handler(event=event, text="创建", + mon_path=self._watch_path, event_path=event.src_path) + + def on_moved(self, event): + self.sync.event_handler(event=event, text="移动", + mon_path=self._watch_path, event_path=event.dest_path) + + +class FileSoftLink(_PluginBase): + # 插件名称 + plugin_name = "文件软连接" + # 插件描述 + plugin_desc = "监控目录文件变化,媒体文件软连接,其他文件可选复制。" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/softlink.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "filesoftlink_" + # 加载顺序 + plugin_order = 10 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _scheduler = None + _observer = [] + _enabled = False + _onlyonce = False + _copy_files = False + _cron = None + _size = 0 + # 模式 compatibility/fast + _mode = "compatibility" + _monitor_dirs = "" + _exclude_keywords = "" + # 存储源目录与目的目录关系 + _dirconf: Dict[str, Optional[Path]] = {} + _medias = {} + # 退出事件 + _event = threading.Event() + + def init_plugin(self, config: dict = None): + # 清空配置 + self._dirconf = {} + + # 读取配置 + if config: + self._enabled = config.get("enabled") + self._onlyonce = config.get("onlyonce") + self._copy_files = config.get("copy_files") + self._mode = config.get("mode") + self._monitor_dirs = config.get("monitor_dirs") or "" + self._exclude_keywords = config.get("exclude_keywords") or "" + self._cron = config.get("cron") + self._size = config.get("size") or 0 + + # 停止现有任务 + self.stop_service() + + if self._enabled or self._onlyonce: + # 定时服务管理器 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + # 追加入库消息统一发送服务 + self._scheduler.add_job(self.send_msg, trigger='interval', seconds=15) + + # 读取目录配置 + monitor_dirs = self._monitor_dirs.split("\n") + if not monitor_dirs: + return + for mon_path in monitor_dirs: + # 格式源目录:目的目录 + if not mon_path: + continue + + # 存储目的目录 + if SystemUtils.is_windows(): + if mon_path.count(":") > 1: + paths = [mon_path.split(":")[0] + ":" + mon_path.split(":")[1], + mon_path.split(":")[2] + ":" + mon_path.split(":")[3]] + else: + paths = [mon_path] + else: + paths = mon_path.split(":") + + # 目的目录 + target_path = None + if len(paths) > 1: + mon_path = paths[0] + target_path = Path(paths[1]) + self._dirconf[mon_path] = target_path + else: + self._dirconf[mon_path] = None + + # 启用目录监控 + if self._enabled: + # 检查媒体库目录是不是下载目录的子目录 + try: + if target_path and target_path.is_relative_to(Path(mon_path)): + logger.warn(f"{target_path} 是监控目录 {mon_path} 的子目录,无法监控") + self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控") + continue + except Exception as e: + logger.debug(str(e)) + pass + + try: + if self._mode == "compatibility": + # 兼容模式,目录同步性能降低且NAS不能休眠,但可以兼容挂载的远程共享目录如SMB + observer = PollingObserver(timeout=10) + else: + # 内部处理系统操作类型选择最优解 + observer = Observer(timeout=10) + self._observer.append(observer) + observer.schedule(FileMonitorHandler(mon_path, self), path=mon_path, recursive=True) + observer.daemon = True + observer.start() + logger.info(f"{mon_path} 的目录监控服务启动") + except Exception as e: + err_msg = str(e) + if "inotify" in err_msg and "reached" in err_msg: + logger.warn( + f"目录监控服务启动出现异常:{err_msg},请在宿主机上(不是docker容器内)执行以下命令并重启:" + + """ + echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf + echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf + sudo sysctl -p + """) + else: + logger.error(f"{mon_path} 启动目录监控失败:{err_msg}") + self.systemmessage.put(f"{mon_path} 启动目录监控失败:{err_msg}") + + # 运行一次定时服务 + if self._onlyonce: + logger.info("软连接服务启动,立即运行一次") + self._scheduler.add_job(name="软连接", func=self.sync_all, trigger='date', + run_date=datetime.datetime.now( + tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3) + ) + # 关闭一次性开关 + self._onlyonce = False + # 保存配置 + self.__update_config() + + # 启动定时服务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def __update_config(self): + """ + 更新配置 + """ + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "mode": self._mode, + "monitor_dirs": self._monitor_dirs, + "exclude_keywords": self._exclude_keywords, + "cron": self._cron, + "size": self._size + }) + + @eventmanager.register(EventType.PluginAction) + def remote_sync(self, event: Event): + """ + 远程全量同步 + """ + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "softlink_sync": + return + self.post_message(channel=event.event_data.get("channel"), + title="开始同步监控目录 ...", + userid=event.event_data.get("user")) + self.sync_all() + if event: + self.post_message(channel=event.event_data.get("channel"), + title="监控目录同步完成!", userid=event.event_data.get("user")) + + def sync_all(self): + """ + 立即运行一次,全量同步目录中所有文件 + """ + logger.info("开始全量同步监控目录 ...") + # 遍历所有监控目录 + for mon_path in self._dirconf.keys(): + # 遍历目录下所有文件 + for file_path in SystemUtils.list_files(Path(mon_path), settings.RMT_MEDIAEXT): + self.__handle_file(event_path=str(file_path), mon_path=mon_path) + logger.info("全量同步监控目录完成!") + + def event_handler(self, event, mon_path: str, text: str, event_path: str): + """ + 处理文件变化 + :param event: 事件 + :param mon_path: 监控目录 + :param text: 事件描述 + :param event_path: 事件文件路径 + """ + if not event.is_directory: + # 文件发生变化 + logger.debug("文件%s:%s" % (text, event_path)) + self.__handle_file(event_path=event_path, mon_path=mon_path) + + def __handle_file(self, event_path: str, mon_path: str): + """ + 同步一个文件 + :param event_path: 事件文件路径 + :param mon_path: 监控目录 + """ + file_path = Path(event_path) + try: + if not file_path.exists(): + return + # 全程加锁 + with lock: + # 回收站及隐藏的文件不处理 + if event_path.find('/@Recycle/') != -1 \ + or event_path.find('/#recycle/') != -1 \ + or event_path.find('/.') != -1 \ + or event_path.find('/@eaDir') != -1: + logger.debug(f"{event_path} 是回收站或隐藏的文件") + return + + # 命中过滤关键字不处理 + if self._exclude_keywords: + for keyword in self._exclude_keywords.split("\n"): + if keyword and re.findall(keyword, event_path): + logger.info(f"{event_path} 命中过滤关键字 {keyword},不处理") + return + + # 整理屏蔽词不处理 + transfer_exclude_words = self.systemconfig.get(SystemConfigKey.TransferExcludeWords) + if transfer_exclude_words: + for keyword in transfer_exclude_words: + if not keyword: + continue + if keyword and re.search(r"%s" % keyword, event_path, re.IGNORECASE): + logger.info(f"{event_path} 命中整理屏蔽词 {keyword},不处理") + return + + # 判断是不是蓝光目录 + if re.search(r"BDMV[/\\]STREAM", event_path, re.IGNORECASE): + # 截取BDMV前面的路径 + blurray_dir = event_path[:event_path.find("BDMV")] + file_path = Path(blurray_dir) + logger.info(f"{event_path} 是蓝光目录,更正文件路径为:{str(file_path)}") + + # 元数据 + file_meta = MetaInfoPath(file_path) + if not file_meta.name: + 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) + target_file = str(file_path).replace(str(mon_path), str(target)) + + # 如果是文件夹 + if Path(target_file).is_dir(): + if not Path(target_file).exists(): + logger.info(f"创建目标文件夹 {target_file}") + os.makedirs(target_file) + return + else: + # 文件 + if Path(target_file).exists(): + logger.info(f"目标文件 {target_file} 已存在") + return + + if not Path(target_file).parent.exists(): + logger.info(f"创建目标文件夹 {Path(target_file).parent}") + os.makedirs(Path(target_file).parent) + + # 媒体文件软连接 + if target_file.lower().endswith(self._video_formats): + SystemUtils.softlink(str(file_path), target_file) + else: + if self._copy_files: + # 其他nfo、jpg等复制文件 + shutil.copy2(str(file_path), target_file) + logger.info(f"复制其他文件 {str(file_path)} 到 {target_file}") + except Exception as e: + logger.error("软连接发生错误:%s - %s" % (str(e), traceback.format_exc())) + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + return [{ + "cmd": "/softlink_sync", + "event": EventType.PluginAction, + "desc": "文件软连接同步", + "category": "", + "data": { + "action": "softlink_sync" + } + }] + + def get_api(self) -> List[Dict[str, Any]]: + return [{ + "path": "/softlink_sync", + "endpoint": self.sync, + "methods": ["GET"], + "summary": "文件软连接同步", + "description": "文件软连接同步", + }] + + 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": "FileSoftLink", + "name": "文件软连接全量同步服务", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self.sync_all, + "kwargs": {} + }] + return [] + + def sync(self) -> schemas.Response: + """ + API调用目录同步 + """ + self.sync_all() + return schemas.Response(success=True) + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'copy_files', + 'label': '复制非媒体文件', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'mode', + 'label': '监控模式', + 'items': [ + {'title': '兼容模式', 'value': 'compatibility'}, + {'title': '性能模式', 'value': 'fast'} + ] + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '定时全量同步周期', + 'placeholder': '5位cron表达式,留空关闭' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'size', + 'label': '监控文件大小(GB)', + 'placeholder': '0' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'monitor_dirs', + 'label': '监控目录', + 'rows': 5, + 'placeholder': '监控目录:转移目的目录' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'exclude_keywords', + 'label': '排除关键词', + 'rows': 2, + 'placeholder': '每一行一个关键词' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '监控文件大小:单位GB,0为不开启,低于监控文件大小的文件不会被监控转移。' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "onlyonce": False, + "copy_files": True, + "mode": "compatibility", + "monitor_dirs": "", + "exclude_keywords": "", + "cron": "", + "size": 0 + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + if self._observer: + for observer in self._observer: + try: + observer.stop() + observer.join() + except Exception as e: + print(str(e)) + self._observer = [] + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._event.set() + self._scheduler.shutdown() + self._event.clear() + self._scheduler = None diff --git a/plugins/shortplaymonitor/__init__.py b/plugins/shortplaymonitor/__init__.py index c222aa0..078aba1 100644 --- a/plugins/shortplaymonitor/__init__.py +++ b/plugins/shortplaymonitor/__init__.py @@ -503,7 +503,7 @@ class ShortPlayMonitor(_PluginBase): if transfer_type == 'link': # 硬链接 retcode, retmsg = SystemUtils.link(file_item, target_file) - elif transfer_type == 'softlink': + elif transfer_type == 'filesoftlink': # 软链接 retcode, retmsg = SystemUtils.softlink(file_item, target_file) elif transfer_type == 'move': @@ -884,7 +884,7 @@ class ShortPlayMonitor(_PluginBase): {'title': '移动', 'value': 'move'}, {'title': '复制', 'value': 'copy'}, {'title': '硬链接', 'value': 'link'}, - {'title': '软链接', 'value': 'softlink'}, + {'title': '软链接', 'value': 'filesoftlink'}, {'title': 'Rclone复制', 'value': 'rclone_copy'}, {'title': 'Rclone移动', 'value': 'rclone_move'} ]