diff --git a/plugins/removelink/__init__.py b/plugins/removelink/__init__.py index 914af81..1146efd 100644 --- a/plugins/removelink/__init__.py +++ b/plugins/removelink/__init__.py @@ -1,55 +1,52 @@ -import datetime -import re -import shutil -import threading import traceback from pathlib import Path -from typing import List, Tuple, Dict, Any, Optional +from typing import List, Tuple, Dict, Any +import os +import re -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.chain.tmdb import TmdbChain -from app.chain.transfer import TransferChain -from app.core.config import settings -from app.core.context import MediaInfo -from app.core.event import eventmanager, Event -from app.core.metainfo import MetaInfoPath -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.types import EventType, MediaType, SystemConfigKey -from app.utils.string import StringUtils -from app.utils.system import SystemUtils - -lock = threading.Lock() - +from app.schemas import Notification, NotificationType +import time 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) + logger.info("新增文件:%s" % event.event_data) + self.sync._state_set.add((event.src_path, event.src_path.stat().st_ino)) - def on_moved(self, event): - self.sync.event_handler(event=event, text="移动", - mon_path=self._watch_path, event_path=event.dest_path) + def on_deleted(self, event): + logger.info("监测到删除:%s" % event.src_path) + self.sync.event_handler(event=event, event_path=event.src_path) +def updateState(monitor_dirs: List[str]): + """ + 更新监控目录的文件列表 + """ + start_time = time.time() # 记录开始时间 + state_set = set() + for mon_path in monitor_dirs: + for root, dirs, files in os.walk(mon_path): + for file in files: + file = Path(root) / file + if not file.exists(): + continue + state_set.add((file, file.stat().st_ino)) + end_time = time.time() # 记录结束时间 + elapsed_time = end_time - start_time # 计算耗时 + logger.info(f"更新文件列表完成,共计{len(state_set)}个文件,耗时:{elapsed_time}秒") + + return state_set class RemoveLink(_PluginBase): # 插件名称 @@ -71,166 +68,45 @@ class RemoveLink(_PluginBase): # 可使用的用户级别 auth_level = 1 - # 私有属性 - _scheduler = None - transferhis = None - downloadhis = None - transferchian = None - tmdbchain = None - _observer = [] - _enabled = False - _notify = False - _onlyonce = False - _cron = None - _size = 0 - # 模式 compatibility/fast - _mode = "fast" - # 转移方式 - _transfer_type = settings.TRANSFER_TYPE + # preivate property _monitor_dirs = "" _exclude_keywords = "" - _interval: int = 10 - # 存储源目录与目的目录关系 - _dirconf: Dict[str, Optional[Path]] = {} - # 存储源目录转移方式 - _transferconf: Dict[str, Optional[str]] = {} - _medias = {} - # 退出事件 - _event = threading.Event() + _enabled = False + _observer = [] + _state_set = set() def init_plugin(self, config: dict = None): - self.transferhis = TransferHistoryOper() - self.downloadhis = DownloadHistoryOper() - self.transferchian = TransferChain() - self.tmdbchain = TmdbChain() - # 清空配置 - self._dirconf = {} - self._transferconf = {} - - # 读取配置 + logger.info(f"Hello, RemoveLink! config {config}") if config: self._enabled = config.get("enabled") self._notify = config.get("notify") - self._onlyonce = config.get("onlyonce") - self._mode = config.get("mode") - self._transfer_type = config.get("transfer_type") - self._monitor_dirs = config.get("monitor_dirs") or "" + self._monitor_dirs = config.get("monitor_dirs") 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.__update_config() # 停止现有任务 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) - + if self._enabled: # 读取目录配置 monitor_dirs = self._monitor_dirs.split("\n") + logger.info(f"监控目录:{monitor_dirs}") if not monitor_dirs: return for mon_path in monitor_dirs: # 格式源目录:目的目录 if not mon_path: continue - - # 自定义转移方式 - _transfer_type = self._transfer_type - if mon_path.count("#") == 1: - _transfer_type = mon_path.split("#")[1] - mon_path = mon_path.split("#")[0] - - # 存储目的目录 - 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 - - # 转移方式 - self._transferconf[mon_path] = _transfer_type - - # 启用目录监控 - 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(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._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() - self._scheduler.start() + observer = Observer(timeout=10) + self._observer.append(observer) + observer.schedule(FileMonitorHandler(mon_path, self), mon_path, recursive=True) + observer.daemon = True + observer.start() + logger.info(f"{mon_path} 的目录监控服务启动") + except Exception as e: + err_msg = str(e) + logger.error(f"{mon_path} 启动目录监控失败:{err_msg}") + self.systemmessage.put(f"{mon_path} 启动目录监控失败:{err_msg}") + self._state_set = updateState(monitor_dirs) def __update_config(self): """ @@ -239,409 +115,19 @@ class RemoveLink(_PluginBase): self.update_config({ "enabled": self._enabled, "notify": self._notify, - "onlyonce": self._onlyonce, - "mode": self._mode, - "transfer_type": self._transfer_type, "monitor_dirs": self._monitor_dirs, "exclude_keywords": self._exclude_keywords, - "interval": self._interval, - "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") != "directory_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: - transfer_history = self.transferhis.get_by_src(event_path) - if transfer_history: - logger.debug("文件已处理过:%s" % event_path) - return - - # 回收站及隐藏的文件不处理 - 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 file_path.suffix not in settings.RMT_MEDIAEXT: - logger.debug(f"{event_path} 不是媒体文件") - return - - # 判断是不是蓝光目录 - if re.search(r"BDMV[/\\]STREAM", event_path, re.IGNORECASE): - # 截取BDMV前面的路径 - event_path = event_path[:event_path.find("BDMV")] - file_path = Path(event_path) - - # 查询历史记录,已转移的不处理 - if self.transferhis.get_by_src(event_path): - logger.info(f"{event_path} 已整理过") - return - - # 元数据 - 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"{event_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) - - # 识别媒体信息 - mediainfo: MediaInfo = self.chain.recognize_media(meta=file_meta, - tmdbid=download_history.tmdbid if download_history else None) - if not mediainfo: - logger.warn(f'未识别到媒体信息,标题:{file_meta.name}') - # 新增转移成功历史记录 - his = self.transferhis.add_fail( - src_path=file_path, - mode=transfer_type, - meta=file_meta - ) - if self._notify: - self.chain.post_message(Notification( - mtype=NotificationType.Manual, - title=f"{file_path.name} 未识别到媒体信息,无法入库!\n" - f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。" - )) - return - - # 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title - if not settings.SCRAP_FOLLOW_TMDB: - transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id, - mtype=mediainfo.type.value) - if transfer_history: - mediainfo.title = transfer_history.title - logger.info(f"{file_path.name} 识别为:{mediainfo.type.value} {mediainfo.title_year}") - - # 更新媒体图片 - self.chain.obtain_images(mediainfo=mediainfo) - - # 获取集数据 - if mediainfo.type == MediaType.TV: - episodes_info = self.tmdbchain.tmdb_episodes(tmdbid=mediainfo.tmdb_id, - season=file_meta.begin_season or 1) - else: - episodes_info = None - - # 获取downloadhash - download_hash = self.get_download_hash(src=str(file_path)) - - # 转移 - transferinfo: TransferInfo = self.chain.transfer(mediainfo=mediainfo, - path=file_path, - transfer_type=transfer_type, - target=target, - meta=file_meta, - episodes_info=episodes_info) - - if not transferinfo: - logger.error("文件转移模块运行失败") - return - if not transferinfo.success: - # 转移失败 - logger.warn(f"{file_path.name} 入库失败:{transferinfo.message}") - # 新增转移失败历史记录 - self.transferhis.add_fail( - src_path=file_path, - mode=transfer_type, - download_hash=download_hash, - meta=file_meta, - mediainfo=mediainfo, - transferinfo=transferinfo - ) - if self._notify: - self.chain.post_message(Notification( - mtype=NotificationType.Manual, - title=f"{mediainfo.title_year}{file_meta.season_episode} 入库失败!", - text=f"原因:{transferinfo.message or '未知'}", - image=mediainfo.get_message_image() - )) - return - - # 新增转移成功历史记录 - self.transferhis.add_success( - src_path=file_path, - mode=transfer_type, - download_hash=download_hash, - meta=file_meta, - mediainfo=mediainfo, - transferinfo=transferinfo - ) - - # 刮削单个文件 - if settings.SCRAP_METADATA: - self.chain.scrape_metadata(path=transferinfo.target_path, - mediainfo=mediainfo, - transfer_type=transfer_type) - - """ - { - "title_year season": { - "files": [ - { - "path":, - "mediainfo":, - "file_meta":, - "transferinfo": - } - ], - "time": "2023-08-24 23:23:23.332" - } - } - """ - # 发送消息汇总 - media_list = self._medias.get(mediainfo.title_year + " " + file_meta.season) or {} - if media_list: - media_files = media_list.get("files") or [] - if media_files: - file_exists = False - for file in media_files: - if str(event_path) == file.get("path"): - file_exists = True - break - if not file_exists: - media_files.append({ - "path": event_path, - "mediainfo": mediainfo, - "file_meta": file_meta, - "transferinfo": transferinfo - }) - else: - media_files = [ - { - "path": event_path, - "mediainfo": mediainfo, - "file_meta": file_meta, - "transferinfo": transferinfo - } - ] - media_list = { - "files": media_files, - "time": datetime.datetime.now() - } - else: - media_list = { - "files": [ - { - "path": event_path, - "mediainfo": mediainfo, - "file_meta": file_meta, - "transferinfo": transferinfo - } - ], - "time": datetime.datetime.now() - } - self._medias[mediainfo.title_year + " " + file_meta.season] = media_list - - # 广播事件 - self.eventmanager.send_event(EventType.TransferComplete, { - 'meta': file_meta, - 'mediainfo': mediainfo, - 'transferinfo': transferinfo - }) - - # 移动模式删除空目录 - if transfer_type == "move": - for file_dir in file_path.parents: - if len(str(file_dir)) <= len(str(Path(mon_path))): - # 重要,删除到监控目录为止 - break - files = SystemUtils.list_files(file_dir, settings.RMT_MEDIAEXT) - if not files: - logger.warn(f"移动模式,删除空目录:{file_dir}") - shutil.rmtree(file_dir, ignore_errors=True) - - except Exception as e: - logger.error("目录监控发生错误:%s - %s" % (str(e), traceback.format_exc())) - - def send_msg(self): - """ - 定时检查是否有媒体处理完,发送统一消息 - """ - if not self._medias or not self._medias.keys(): - return - - # 遍历检查是否已刮削完,发送消息 - for medis_title_year_season in list(self._medias.keys()): - media_list = self._medias.get(medis_title_year_season) - logger.info(f"开始处理媒体 {medis_title_year_season} 消息") - - if not media_list: - continue - - # 获取最后更新时间 - last_update_time = media_list.get("time") - media_files = media_list.get("files") - if not last_update_time or not media_files: - continue - - transferinfo = media_files[0].get("transferinfo") - file_meta = media_files[0].get("file_meta") - mediainfo = media_files[0].get("mediainfo") - # 判断剧集最后更新时间距现在是已超过10秒或者电影,发送消息 - if (datetime.datetime.now() - last_update_time).total_seconds() > int(self._interval) \ - or mediainfo.type == MediaType.MOVIE: - # 发送通知 - if self._notify: - - # 汇总处理文件总大小 - total_size = 0 - file_count = 0 - - # 剧集汇总 - episodes = [] - for file in media_files: - transferinfo = file.get("transferinfo") - total_size += transferinfo.total_size - file_count += 1 - - file_meta = file.get("file_meta") - if file_meta and file_meta.begin_episode: - episodes.append(file_meta.begin_episode) - - transferinfo.total_size = total_size - # 汇总处理文件数量 - transferinfo.file_count = file_count - - # 剧集季集信息 S01 E01-E04 || S01 E01、E02、E04 - season_episode = None - # 处理文件多,说明是剧集,显示季入库消息 - if mediainfo.type == MediaType.TV: - # 季集文本 - season_episode = f"{file_meta.season} {StringUtils.format_ep(episodes)}" - # 发送消息 - self.transferchian.send_transfer_message(meta=file_meta, - mediainfo=mediainfo, - transferinfo=transferinfo, - season_episode=season_episode) - # 发送完消息,移出key - del self._medias[medis_title_year_season] - continue - - 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 @staticmethod def get_command() -> List[Dict[str, Any]]: - """ - 定义远程控制命令 - :return: 命令关键字、事件、描述、附带数据 - """ - return [{ - "cmd": "/directory_sync", - "event": EventType.PluginAction, - "desc": "目录监控同步", - "category": "管理", - "data": { - "action": "directory_sync" - } - }] + pass def get_api(self) -> List[Dict[str, Any]]: - return [{ - "path": "/directory_sync", - "endpoint": self.sync, - "methods": ["GET"], - "summary": "目录监控同步", - "description": "目录监控同步", - }] - - def sync(self) -> schemas.Response: - """ - API调用目录同步 - """ - self.sync_all() - return schemas.Response(success=True) + pass def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: return [ @@ -682,127 +168,6 @@ class RemoveLink(_PluginBase): } } ] - }, - { - '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': 'VSelect', - 'props': { - 'model': 'mode', - 'label': '监控模式', - 'items': [ - {'title': '兼容模式', 'value': 'compatibility'}, - {'title': '性能模式', 'value': 'fast'} - ] - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VSelect', - 'props': { - 'model': 'transfer_type', - 'label': '转移方式', - 'items': [ - {'title': '移动', 'value': 'move'}, - {'title': '复制', 'value': 'copy'}, - {'title': '硬链接', 'value': 'link'}, - {'title': '软链接', 'value': 'softlink'}, - {'title': 'Rclone复制', 'value': 'rclone_copy'}, - {'title': 'Rclone移动', 'value': 'rclone_move'} - ] - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 4 - }, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'interval', - 'label': '入库消息延迟', - 'placeholder': '10' - } - } - ] - } - ] - }, - { - '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': 'size', - 'label': '监控文件大小(GB)', - 'placeholder': '0' - } - } - ] } ] }, @@ -821,11 +186,7 @@ class RemoveLink(_PluginBase): 'model': 'monitor_dirs', 'label': '监控目录', 'rows': 5, - 'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、softlink、rclone_copy、rclone_move:\n' - '监控目录\n' - '监控目录#转移方式\n' - '监控目录:转移目的目录\n' - '监控目录:转移目的目录#转移方式' + 'placeholder': '每一行一个目录' } } ] @@ -853,69 +214,6 @@ class RemoveLink(_PluginBase): ] } ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '监控目录不指定目的目录时,将转移到媒体库目录,并自动创建一级分类目录,同时按配置创建二级分类目录;监控目录指定了目的目录时,不会自动创建一级目录,但会根据配置创建二级分类目录。' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '入库消息延迟默认10s,如网络较慢可酌情调大,有助于发送统一入库消息。' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '监控文件大小:单位GB,0为不开启,低于监控文件大小的文件不会被监控转移。' - } - } - ] - } - ] } ] } @@ -923,13 +221,8 @@ class RemoveLink(_PluginBase): "enabled": False, "notify": False, "onlyonce": False, - "mode": "fast", - "transfer_type": settings.TRANSFER_TYPE, "monitor_dirs": "", "exclude_keywords": "", - "interval": 10, - "cron": "", - "size": 0 } def get_page(self) -> List[dict]: @@ -946,11 +239,42 @@ class RemoveLink(_PluginBase): observer.join() except Exception as e: print(str(e)) + logger.error(f"停止目录监控失败:{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 + + def event_handler(self, event, event_path: str): + """ + 处理删除事件 + """ + deleted_files = [] + if not event.is_directory: + # 命中过滤关键字不处理 + 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 + + file_path = Path(event_path) + current_set = updateState(self._monitor_dirs.split("\n")) + deleted_set = self._state_set - current_set + deleted_inode = [x[1] for x in deleted_set] + try: + # 在current_set中查找与deleted_inode有相同inode的文件并删除 + for path, inode in current_set: + if inode in deleted_inode: + file = Path(path) + file.unlink() + deleted_files.append(path) + logger.info(f"删除硬链接文件:{path}") + except Exception as e: + logger.error("目录监控发生错误:%s - %s" % (str(e), traceback.format_exc())) + + if self._notify and (len(deleted_files) != 0): + self.chain.post_message(Notification( + mtype=NotificationType.SiteMessage, + title=f"[删除硬链接]", + text=f"根据源文件:{file_path}\n" + f"删除硬链接文件:{[str(x) for x in deleted_files]}", + )) + self._state_set = updateState(self._monitor_dirs.split("\n")) \ No newline at end of file