diff --git a/package.json b/package.json index 844f08d..52224d5 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "CrossSeed": { "name": "青蛙辅种助手", "description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。", - "version": "2.0", + "version": "2.1", "icon": "qingwa.png", "author": "233@qingwa", "level": 2 @@ -362,7 +362,7 @@ "RemoveLink": { "name": "清理硬链接", "description": "监控目录内文件被删除时,同步删除监控目录内所有和它硬链接的文件", - "version": "1.6", + "version": "1.8", "icon": "Ombi_A.png", "author": "DzAvril", "level": 1 @@ -418,7 +418,7 @@ "QbCommand": { "name": "QB远程操作", "description": "通过定时任务或交互命令远程操作QB暂停/开始/限速等。", - "version": "1.2", + "version": "1.3", "icon": "Qbittorrent_A.png", "author": "DzAvril", "level": 1 diff --git a/plugins/crossseed/__init__.py b/plugins/crossseed/__init__.py index fc75269..c981619 100644 --- a/plugins/crossseed/__init__.py +++ b/plugins/crossseed/__init__.py @@ -56,7 +56,7 @@ class CSSiteConfig(object): def get_api_url(self): if self.name == "憨憨": - return f"{self.url}npapi/pieces-hash" + return f"{self.url}nexusapi/pieces-hash" return f"{self.url}api/pieces-hash" def get_torrent_url(self, torrent_id: str): @@ -177,7 +177,7 @@ class CrossSeed(_PluginBase): # 插件图标 plugin_icon = "qingwa.png" # 插件版本 - plugin_version = "2.0" + plugin_version = "2.1" # 插件作者 plugin_author = "233@qingwa" # 作者主页 diff --git a/plugins/qbcommand/__init__.py b/plugins/qbcommand/__init__.py index f1e7966..d6d0b09 100644 --- a/plugins/qbcommand/__init__.py +++ b/plugins/qbcommand/__init__.py @@ -1,6 +1,5 @@ -import math from typing import List, Tuple, Dict, Any - +from enum import Enum from app.log import logger from app.modules.qbittorrent import Qbittorrent from app.plugins import _PluginBase @@ -19,7 +18,7 @@ class QbCommand(_PluginBase): # 插件图标 plugin_icon = "Qbittorrent_A.png" # 插件版本 - plugin_version = "1.2" + plugin_version = "1.3" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -39,6 +38,9 @@ class QbCommand(_PluginBase): _resume_cron = None _only_pause_once = False _only_resume_once = False + _only_pause_upload = False + _only_pause_download = False + _only_pause_checking = False _upload_limit = 0 _enable_upload_limit = False _download_limit = 0 @@ -55,6 +57,9 @@ class QbCommand(_PluginBase): self._resume_cron = config.get("resume_cron") self._only_pause_once = config.get("onlypauseonce") self._only_resume_once = config.get("onlyresumeonce") + self._only_pause_upload = config.get("onlypauseupload") + self._only_pause_download = config.get("onlypausedownload") + self._only_pause_checking = config.get("onlypausechecking") self._download_limit = config.get("download_limit") self._upload_limit = config.get("upload_limit") self._enable_download_limit = config.get("enable_download_limit") @@ -82,11 +87,40 @@ class QbCommand(_PluginBase): } ) + if self._only_pause_upload or self._only_pause_download or self._only_pause_checking: + if self._only_pause_upload: + self.pause_torrent(self.TorrentType.UPLOADING) + if self._only_pause_download: + self.pause_torrent(self.TorrentType.DOWNLOADING) + if self._only_pause_checking: + self.pause_torrent(self.TorrentType.CHECKING) + + self._only_pause_upload = False + self._only_pause_download = False + self._only_pause_checking = False + self.update_config( + { + "onlypauseupload": False, + "onlypausedownload": False, + "onlypausechecking": False, + "enabled": self._enabled, + "notify": self._notify, + "pause_cron": self._pause_cron, + "resume_cron": self._resume_cron, + } + ) + self.set_limit(self._upload_limit, self._download_limit) def get_state(self) -> bool: return self._enabled + class TorrentType(Enum): + ALL = 1 + DOWNLOADING = 2 + UPLOADING = 3 + CHECKING = 4 + @staticmethod def get_command() -> List[Dict[str, Any]]: """ @@ -97,17 +131,45 @@ class QbCommand(_PluginBase): { "cmd": "/pause_torrents", "event": EventType.PluginAction, - "desc": "暂停QB种子", + "desc": "暂停QB所有任务", "category": "QB", "data": {"action": "pause_torrents"}, }, + { + "cmd": "/pause_upload_torrents", + "event": EventType.PluginAction, + "desc": "暂停QB上传任务", + "category": "QB", + "data": {"action": "pause_upload_torrents"}, + }, + { + "cmd": "/pause_download_torrents", + "event": EventType.PluginAction, + "desc": "暂停QB下载任务", + "category": "QB", + "data": {"action": "pause_download_torrents"}, + }, + { + "cmd": "/pause_checking_torrents", + "event": EventType.PluginAction, + "desc": "暂停QB检查任务", + "category": "QB", + "data": {"action": "pause_checking_torrents"}, + }, { "cmd": "/resume_torrents", "event": EventType.PluginAction, - "desc": "开始QB种子", + "desc": "开始QB所有任务", "category": "QB", "data": {"action": "resume_torrents"}, }, + { + "cmd": "/qb_status", + "event": EventType.PluginAction, + "desc": "QB当前任务状态", + "category": "QB", + "data": {"action": "qb_status"}, + }, { "cmd": "/toggle_upload_limit", "event": EventType.PluginAction, @@ -209,7 +271,11 @@ class QbCommand(_PluginBase): for torrent in torrents: if torrent.state_enum.is_uploading and not torrent.state_enum.is_paused: uploading_torrents.append(torrent.get("hash")) - elif torrent.state_enum.is_downloading and not torrent.state_enum.is_paused: + elif ( + torrent.state_enum.is_downloading + and not torrent.state_enum.is_paused + and not torrent.state_enum.is_checking + ): downloading_torrents.append(torrent.get("hash")) elif torrent.state_enum.is_checking: checking_torrents.append(torrent.get("hash")) @@ -236,7 +302,37 @@ class QbCommand(_PluginBase): return self.pause_torrent() - def pause_torrent(self): + @eventmanager.register(EventType.PluginAction) + def handle_pause_upload_torrent(self, event: Event): + if not self._enabled: + return + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "pause_upload_torrents": + return + self.pause_torrent(self.TorrentType.UPLOADING) + + @eventmanager.register(EventType.PluginAction) + def handle_pause_download_torrent(self, event: Event): + if not self._enabled: + return + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "pause_download_torrents": + return + self.pause_torrent(self.TorrentType.DOWNLOADING) + + @eventmanager.register(EventType.PluginAction) + def handle_pause_checking_torrent(self, event: Event): + if not self._enabled: + return + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "pause_checking_torrents": + return + self.pause_torrent(self.TorrentType.CHECKING) + + def pause_torrent(self, type: TorrentType = TorrentType.ALL): if not self._enabled: return @@ -244,7 +340,15 @@ class QbCommand(_PluginBase): hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = ( self.get_torrents_status(all_torrents) ) - to_be_paused = hash_downloading + hash_uploading + hash_checking + if type == self.TorrentType.DOWNLOADING: + to_be_paused = hash_downloading + elif type == self.TorrentType.UPLOADING: + to_be_paused = hash_uploading + elif type == self.TorrentType.CHECKING: + to_be_paused = hash_checking + else: + to_be_paused = hash_downloading + hash_uploading + hash_checking + logger.info( f"暂定任务启动 \n" f"种子总数: {len(all_torrents)} \n" @@ -260,12 +364,12 @@ class QbCommand(_PluginBase): mtype=NotificationType.SiteMessage, title=f"【QB暂停任务启动】", text=f"种子总数: {len(all_torrents)} \n" - f"做种数量: {len(hash_uploading)}\n" - f"下载数量: {len(hash_downloading)}\n" - f"检查数量: {len(hash_checking)}\n" - f"暂停数量: {len(hash_paused)}\n" - f"错误数量: {len(hash_error)}\n" - f"暂停操作中请稍等...\n", + f"做种数量: {len(hash_uploading)}\n" + f"下载数量: {len(hash_downloading)}\n" + f"检查数量: {len(hash_checking)}\n" + f"暂停数量: {len(hash_paused)}\n" + f"错误数量: {len(hash_error)}\n" + f"暂停操作中请稍等...\n", ) if len(to_be_paused) > 0: if self._qb.stop_torrents(ids=(to_be_paused)): @@ -300,11 +404,11 @@ class QbCommand(_PluginBase): mtype=NotificationType.SiteMessage, title=f"【QB暂停任务完成】", text=f"种子总数: {len(all_torrents)} \n" - f"做种数量: {len(hash_uploading)}\n" - f"下载数量: {len(hash_downloading)}\n" - f"检查数量: {len(hash_checking)}\n" - f"暂停数量: {len(hash_paused)}\n" - f"错误数量: {len(hash_error)}\n", + f"做种数量: {len(hash_uploading)}\n" + f"下载数量: {len(hash_downloading)}\n" + f"检查数量: {len(hash_checking)}\n" + f"暂停数量: {len(hash_paused)}\n" + f"错误数量: {len(hash_error)}\n", ) @eventmanager.register(EventType.PluginAction) @@ -340,12 +444,12 @@ class QbCommand(_PluginBase): mtype=NotificationType.SiteMessage, title=f"【QB开始任务启动】", text=f"种子总数: {len(all_torrents)} \n" - f"做种数量: {len(hash_uploading)}\n" - f"下载数量: {len(hash_downloading)}\n" - f"检查数量: {len(hash_checking)}\n" - f"暂停数量: {len(hash_paused)}\n" - f"错误数量: {len(hash_error)}\n" - f"开始操作中请稍等...\n", + f"做种数量: {len(hash_uploading)}\n" + f"下载数量: {len(hash_downloading)}\n" + f"检查数量: {len(hash_checking)}\n" + f"暂停数量: {len(hash_paused)}\n" + f"错误数量: {len(hash_error)}\n" + f"开始操作中请稍等...\n", ) if not self._qb.start_torrents(ids=hash_paused): logger.error(f"开始种子失败") @@ -377,11 +481,52 @@ class QbCommand(_PluginBase): mtype=NotificationType.SiteMessage, title=f"【QB开始任务完成】", text=f"种子总数: {len(all_torrents)} \n" - f"做种数量: {len(hash_uploading)}\n" - f"下载数量: {len(hash_downloading)}\n" - f"检查数量: {len(hash_checking)}\n" - f"暂停数量: {len(hash_paused)}\n" - f"错误数量: {len(hash_error)}\n", + f"做种数量: {len(hash_uploading)}\n" + f"下载数量: {len(hash_downloading)}\n" + f"检查数量: {len(hash_checking)}\n" + f"暂停数量: {len(hash_paused)}\n" + f"错误数量: {len(hash_error)}\n", + ) + + @eventmanager.register(EventType.PluginAction) + def handle_qb_status(self, event: Event): + if not self._enabled: + return + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "qb_status": + return + self.qb_status() + + def qb_status(self): + if not self._enabled: + return + + all_torrents = self.get_all_torrents() + hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = ( + self.get_torrents_status(all_torrents) + ) + logger.info( + f"QB开始任务启动 \n" + f"种子总数: {len(all_torrents)} \n" + f"做种数量: {len(hash_uploading)}\n" + f"下载数量: {len(hash_downloading)}\n" + f"检查数量: {len(hash_checking)}\n" + f"暂停数量: {len(hash_paused)}\n" + f"错误数量: {len(hash_error)}\n" + f"开始操作中请稍等...\n", + ) + if self._notify: + self.post_message( + mtype=NotificationType.SiteMessage, + title=f"【QB开始任务启动】", + text=f"种子总数: {len(all_torrents)} \n" + f"做种数量: {len(hash_uploading)}\n" + f"下载数量: {len(hash_downloading)}\n" + f"检查数量: {len(hash_checking)}\n" + f"暂停数量: {len(hash_paused)}\n" + f"错误数量: {len(hash_error)}\n" + f"开始操作中请稍等...\n", ) @eventmanager.register(EventType.PluginAction) @@ -408,7 +553,12 @@ class QbCommand(_PluginBase): if not self._enable_upload_limit or not self._enable_upload_limit: return True - if not upload_limit or not upload_limit.isdigit() or not download_limit or not download_limit.isdigit(): + if ( + not upload_limit + or not upload_limit.isdigit() + or not download_limit + or not download_limit.isdigit() + ): self.post_message( mtype=NotificationType.SiteMessage, title=f"【QB远程操作】", @@ -434,7 +584,8 @@ class QbCommand(_PluginBase): download_limit_current_val, _ = self._qb.get_speed_limit() return self._qb.set_speed_limit( - download_limit=int(download_limit_current_val), upload_limit=int(upload_limit) + download_limit=int(download_limit_current_val), + upload_limit=int(upload_limit), ) def set_download_limit(self, download_limit): @@ -451,7 +602,8 @@ class QbCommand(_PluginBase): _, upload_limit_current_val = self._qb.get_speed_limit() return self._qb.set_speed_limit( - download_limit=int(download_limit), upload_limit=int(upload_limit_current_val) + download_limit=int(download_limit), + upload_limit=int(upload_limit_current_val), ) def set_limit(self, upload_limit, download_limit): @@ -470,7 +622,6 @@ class QbCommand(_PluginBase): elif flag is None and self._enabled and self._enable_upload_limit: flag = self.set_upload_limit(upload_limit) - if flag: logger.info(f"设置QB限速成功") if self._notify: @@ -654,6 +805,50 @@ class QbCommand(_PluginBase): }, ], }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "onlypauseupload", + "label": "暂停上传任务", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "onlypausedownload", + "label": "暂停下载任务", + }, + } + ], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "onlypausechecking", + "label": "暂停检查任务", + }, + } + ], + }, + ], + }, { "component": "VRow", "content": [ @@ -668,7 +863,7 @@ class QbCommand(_PluginBase): "props": { "type": "info", "variant": "tonal", - "text": "开始周期和暂停周期使用Cron表达式,如:0 0 0 * *", + "text": "开始周期和暂停周期使用Cron表达式,如:0 0 0 * *,仅针对开始/暂定全部任务", }, } ], @@ -704,7 +899,7 @@ class QbCommand(_PluginBase): }, } ], - } + }, ], }, ], @@ -714,6 +909,9 @@ class QbCommand(_PluginBase): "notify": True, "onlypauseonce": False, "onlyresumeonce": False, + "onlypauseupload": False, + "onlypausedownload": False, + "onlypausechecking": False, "upload_limit": 0, "download_limit": 0, "enable_upload_limit": False, diff --git a/plugins/removelink/__init__.py b/plugins/removelink/__init__.py index a8aedf7..f741380 100644 --- a/plugins/removelink/__init__.py +++ b/plugins/removelink/__init__.py @@ -11,6 +11,7 @@ from watchdog.observers import Observer from app.log import logger from app.plugins import _PluginBase from app.schemas import Notification, NotificationType +from app.utils.timer import TimerUtils state_lock = threading.Lock() @@ -90,7 +91,7 @@ class RemoveLink(_PluginBase): # 插件图标 plugin_icon = "Ombi_A.png" # 插件版本 - plugin_version = "1.6" + plugin_version = "1.8" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -98,7 +99,7 @@ class RemoveLink(_PluginBase): # 插件配置项ID前缀 plugin_config_prefix = "linkdeleted_" # 加载顺序 - plugin_order = 27 + plugin_order = 0 # 可使用的用户级别 auth_level = 1 @@ -174,6 +175,40 @@ class RemoveLink(_PluginBase): 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: + # 随机时间 + triggers = TimerUtils.random_scheduler( + num_executions=1, + begin_hour=0, + end_hour=1, + min_interval=1, + max_interval=60, + ) + ret_jobs = [] + for trigger in triggers: + ret_jobs.append( + { + "id": f"RemoveLink|{trigger.hour}:{trigger.minute}", + "name": "清理空文件夹", + "trigger": "cron", + "func": self.delete_empty_folders, + "kwargs": {"hour": trigger.hour, "minute": trigger.minute}, + } + ) + return ret_jobs + return [] + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: return [ { @@ -273,26 +308,26 @@ class RemoveLink(_PluginBase): ], }, { - 'component': 'VRow', - 'content': [ + "component": "VRow", + "content": [ { - 'component': 'VCol', - 'props': { - 'cols': 12, + "component": "VCol", + "props": { + "cols": 12, }, - 'content': [ + "content": [ { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '监控目录如有多个需换行,源目录和硬链接目录都需要添加到监控目录中;如需实现删除硬链接时不删除源文件,可把源文件目录配置到不删除目录中。' - } + "component": "VAlert", + "props": { + "type": "info", + "variant": "tonal", + "text": "监控目录如有多个需换行,源目录和硬链接目录都需要添加到监控目录中;如需实现删除硬链接时不删除源文件,可把源文件目录配置到不删除目录中。", + }, } - ] + ], } - ] - } + ], + }, ], } ], { @@ -329,6 +364,25 @@ class RemoveLink(_PluginBase): return True return False + def delete_empty_folders(self): + """ + 删除空目录 + """ + for mon_path in self.monitor_dirs.split("\n"): + for subdir, dirs, files in os.walk(mon_path, topdown=False): + for dir in dirs: + dir_path = os.path.join(subdir, dir) + # 检查当前目录是否为空 + if not os.listdir(dir_path) and not self.__is_excluded(dir_path): + os.rmdir(dir_path) + logger.info(f"删除空目录:{dir_path}") + if self._notify: + self.post_message( + mtype=NotificationType.SiteMessage, + title=f"【清理硬链接】", + text=f"清理空文件夹:[{dir_path}]\n", + ) + def handle_deleted(self, file_path: Path): """ 处理删除事件 @@ -358,7 +412,10 @@ class RemoveLink(_PluginBase): mtype=NotificationType.SiteMessage, title=f"【清理硬链接】", text=f"监控到删除源文件:[{file_path}]\n" - f"同步删除硬链接文件:[{path}]", + f"同步删除硬链接文件:[{path}]", ) + except Exception as e: - logger.error("删除硬链接文件发生错误:%s - %s" % (str(e), traceback.format_exc())) + logger.error( + "删除硬链接文件发生错误:%s - %s" % (str(e), traceback.format_exc()) + )