import datetime import random import threading import time from pathlib import Path from typing import List, Tuple, Dict, Any import pytz from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from app.core.config import settings from app.log import logger from app.plugins import _PluginBase from app.utils.system import SystemUtils lock = threading.Lock() class FileCopy(_PluginBase): # 插件名称 plugin_name = "文件复制" # 插件描述 plugin_desc = "自定义文件类型从源目录复制到目的目录。" # 插件图标 plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/copy_files.png" # 插件版本 plugin_version = "1.1" # 插件作者 plugin_author = "thsrite" # 作者主页 author_url = "https://github.com/thsrite" # 插件配置项ID前缀 plugin_config_prefix = "filecopy_" # 加载顺序 plugin_order = 30 # 可使用的用户级别 auth_level = 1 # 私有属性 _scheduler = None _enabled = False _onlyonce = False _cron = None _delay = None _monitor_dirs = "" # 存储源目录与目的目录关系 _dirconf: Dict[str, Path] = {} _rmt_mediaext = None # 退出事件 _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._monitor_dirs = config.get("monitor_dirs") or "" self._cron = config.get("cron") self._delay = config.get("delay") self._rmt_mediaext = config.get("rmt_mediaext") or ".nfo, .jpg" # 停止现有任务 self.stop_service() if self._enabled or self._onlyonce: # 定时服务管理器 self._scheduler = BackgroundScheduler(timezone=settings.TZ) # 读取目录配置 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(":") # 目的目录 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, error = SystemUtils.copy(file, Path(cloud_file)) logger.info(f"{file} -> {cloud_file} {'成功' if state == 0 else '失败'} {error}") # 随机延时 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