From 0770e6fed29d555bf25505803b4029e3f887f2a9 Mon Sep 17 00:00:00 2001 From: thsrite Date: Wed, 17 Jul 2024 17:08:17 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E6=96=87=E4=BB=B6=E5=A4=8D=E5=88=B6v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + icons/copy_files.png | Bin 0 -> 3840 bytes package.json | 1330 +++++++++++++++++----------------- plugins/filecopy/__init__.py | 356 +++++++++ 4 files changed, 1028 insertions(+), 659 deletions(-) create mode 100644 icons/copy_files.png create mode 100644 plugins/filecopy/__init__.py diff --git a/README.md b/README.md index 1dc5da2..e846a2f 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,4 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/ - 云盘同步删除 v1.4 - 媒体库重复媒体检测 v1.9 - 演员作品订阅 v1.0 +- 文件复制 v1.0 diff --git a/icons/copy_files.png b/icons/copy_files.png new file mode 100644 index 0000000000000000000000000000000000000000..9692ac85b8b4f2f1ce14b4babb942816dd2bf2ba GIT binary patch literal 3840 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 S)3`=}drZz*U`h 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(":") + + # 目的目录 + 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: + self._scheduler.add_job(func=self.copy_files, trigger='date', + run_date=datetime.datetime.now( + tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3), + name=f"文件复制 {mon_path}") + # 运行一次定时服务 + if self._onlyonce: + logger.info("文件复制服务启动,立即运行一次") + self._scheduler.add_job(name="文件复制", func=self.copy_files, 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 copy_files(self): + """ + 定时任务,复制文件 + """ + logger.info("开始全量复制监控目录 ...") + # 遍历所有监控目录 + for mon_path in self._dirconf.keys(): + target_path = self._dirconf[mon_path] + # 遍历目录下所有文件 + files = SystemUtils.list_files(Path(mon_path), [ext.strip() for ext in self._rmt_mediaext.split(",")]) + cnt = 0 + for file in files: + logger.info(f"开始处理本地文件:{file}") + cloud_file = str(file).replace(mon_path, str(target_path)) + if Path(cloud_file).exists(): + logger.info(f"{cloud_file} 文件已存在,跳过") + continue + state, _ = SystemUtils.copy(file, Path(cloud_file)) + print(f"{file} -> {cloud_file} {'成功' if state == 0 else '失败'}") + + # 随机延时 + if self._delay: + cnt += 1 + delays = self._delay.split(",") + if cnt >= int(delays[0]): + if str(delays[1]).count("-") == 1: + wait_time = random.randint(int(str(delays[1]).split("-")[0]), + int(str(delays[1]).split("-")[1])) + logger.info(f"随机延迟 {wait_time} 秒") + time.sleep(wait_time) + else: + delay = int(delays[1]) + logger.info(f"延迟 {delay} 秒") + time.sleep(delay) + cnt = 0 + + logger.info("全量复制监控目录完成!") + + def __update_config(self): + """ + 更新配置 + """ + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "monitor_dirs": self._monitor_dirs, + "cron": self._cron, + "delay": self._delay, + "rmt_mediaext": self._rmt_mediaext + }) + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + 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": "FileCopy", + "name": "文件复制", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self.copy_files, + "kwargs": {} + }] + return [] + + 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': 'VRow', + 'content': [ + { + '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': 'delay', + 'label': '随机延时', + 'placeholder': '20,1-10 处理10个文件后随机延迟1-10秒' + } + } + ] + } + ] + }, + { + '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': 'rmt_mediaext', + 'label': '文件格式', + 'rows': 2, + 'placeholder': ".nfo, .jpg" + } + } + ] + } + ] + }, + ] + } + ], { + "enabled": False, + "onlyonce": False, + "monitor_dirs": "", + "cron": "", + "delay": "20,1-10", + "rmt_mediaext": ".nfo, .jpg" + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._event.set() + self._scheduler.shutdown() + self._event.clear() + self._scheduler = None