diff --git a/package.json b/package.json index 6fc5075..7c8ad5c 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,12 @@ "name": "站点数据统计", "description": "自动统计和展示站点数据。", "labels": "站点,仪表板", - "version": "3.5", + "version": "3.6", "icon": "statistic.png", "author": "lightolly", "level": 2, "history": { + "v3.6": "支持站点数据统计刷新后触发插件事件", "v3.5": "站点数据统计支持YemaPT", "v3.4": "修复馒头站点数据统计", "v3.3": "支持选择仪表板组件规格", @@ -75,11 +76,12 @@ "name": "目录监控", "description": "监控目录文件发生变化时实时整理到媒体库。", "labels": "文件整理", - "version": "2.2", + "version": "2.3", "icon": "directory.png", "author": "jxxghp", "level": 1, "history": { + "v2.3": "特殊场景下补充转移成功历史记录", "v2.2": "更新目录设置说明", "v2.1": "增加了元数据刮削开关,升级后需要手动打开,否则默认不刮削", "v2.0": "增强API安全性", @@ -134,11 +136,12 @@ "name": "媒体文件同步删除", "description": "同步删除历史记录、源文件和下载任务。", "labels": "文件整理", - "version": "1.6", + "version": "1.7", "icon": "mediasyncdel.png", "author": "thsrite", "level": 1, "history": { + "v1.7": "修复重新整理被一并删除问题", "v1.6": "修复删除辅种", "v1.5": "支持手动删除订阅历史记录(本次更新之后的历史)" } @@ -446,10 +449,13 @@ "name": "配置中心", "description": "快速调整部分系统设定。", "labels": "系统设置", - "version": "2.4", + "version": "2.5", "icon": "setting.png", "author": "jxxghp", - "level": 1 + "level": 1, + "history": { + "v2.5": "增加Github加速服务器设置项" + } }, "WorkWechatMsg": { "name": "企微机器人消息推送", @@ -641,13 +647,14 @@ }, "DownloaderHelper": { "name": "下载器助手", - "description": "自动做种、站点标签、自动删种。", + "description": "自动标签、自动做种、自动删种。", "labels": "下载管理,仪表板", - "version": "2.7", + "version": "2.8", "icon": "DownloaderHelper.png", "author": "hotlcc", "level": 1, "history": { + "v2.8": "优化了仪表板组件高度;优化了活动种子状态;优化了自动标签,优化站点标签,新增BT/PT标签;监听原种删除事件补充删种逻辑。MP需要升级至v1.9.3,更新后若插件功能异常,请重启一次MP。", "v2.7": "实时速率仪表板样式微调。", "v2.6": "新增仪表板实时速率组件,支持单独展示qb和tr的实时速率(tr未测试,有问题提Issue并@hotlcc)。", "v2.5": "优化通知类型;降低认证级别要求,使MP非认证用户可用,但无法使用【站点名称优先】功能。主程序需升级至v1.9.2及以上版本,否则插件功能异常!", @@ -746,11 +753,13 @@ "name": "清理QB无效做种", "description": "清理已经被站点删除的种子及对应源文件,仅支持QB", "labels": "Qbittorrent", - "version": "1.9", + "version": "2.0", "icon": "clean_a.png", "author": "DzAvril", "level": 1, "history": { + "v2.1": "1. 修复删除无效做种没有tg通知的问题。2. 检测未工作做种排除已暂停做种", + "v2.0": "修复检测不到无效做种的bug", "v1.9": "增加自定义需删除做种的tracker的错误信息", "v1.8": "增加远程命令切换全量通知;修复bug", "v1.7": "修复因消息内容包含'_'导致telegram API调用失败的问题", diff --git a/plugins/brushflow/__init__.py b/plugins/brushflow/__init__.py index ffe9cf6..7da7cb0 100644 --- a/plugins/brushflow/__init__.py +++ b/plugins/brushflow/__init__.py @@ -320,11 +320,12 @@ class BrushFlow(_PluginBase): brush_config = self._brush_config # 这里先过滤掉已删除的站点并保存,特别注意的是,这里保留了界面选择站点时的顺序,以便后续站点随机刷流或顺序刷流 - site_id_to_public_status = {site.get("id"): site.get("public") for site in self.siteshelper.get_indexers()} - brush_config.brushsites = [ - site_id for site_id in brush_config.brushsites - if site_id in site_id_to_public_status and not site_id_to_public_status[site_id] - ] + if brush_config.brushsites: + site_id_to_public_status = {site.get("id"): site.get("public") for site in self.siteshelper.get_indexers()} + brush_config.brushsites = [ + site_id for site_id in brush_config.brushsites + if site_id in site_id_to_public_status and not site_id_to_public_status[site_id] + ] self.__update_config() @@ -859,8 +860,9 @@ class BrushFlow(_PluginBase): { 'component': 'VSelect', 'props': { - 'chips': True, 'multiple': True, + 'chips': True, + 'clearable': True, 'model': 'brushsites', 'label': '刷流站点', 'items': site_options diff --git a/plugins/cleaninvalidseed/__init__.py b/plugins/cleaninvalidseed/__init__.py index 427420f..ab71e01 100644 --- a/plugins/cleaninvalidseed/__init__.py +++ b/plugins/cleaninvalidseed/__init__.py @@ -28,7 +28,7 @@ class CleanInvalidSeed(_PluginBase): # 插件图标 plugin_icon = "clean_a.png" # 插件版本 - plugin_version = "1.9" + plugin_version = "2.1" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -174,6 +174,14 @@ class CleanInvalidSeed(_PluginBase): if event: event_data = event.event_data if event_data: + if not ( + event_data.get("action") == "detect_invalid_torrents" + or event_data.get("action") == "delete_invalid_torrents" + or event_data.get("action") == "detect_invalid_files" + or event_data.get("action") == "delete_invalid_files" + or event_data.get("action") == "toggle_notify_all" + ): + return self.post_message( channel=event.event_data.get("channel"), title="开始执行远程命令...", @@ -285,9 +293,16 @@ class CleanInvalidSeed(_PluginBase): # tracker未工作,但暂时不能判定为失效做种,需人工判断 tracker_not_working_torrents = [] working_tracker_set = set() - exclude_categories = self._exclude_categories.split("\n") if self._exclude_categories else [] - exclude_labels = self._exclude_labels.split("\n") if self._exclude_labels else [] - error_msgs = self._error_msg + self._custom_error_msg.split("\n") if self._exclude_labels else [] + exclude_categories = ( + self._exclude_categories.split("\n") if self._exclude_categories else [] + ) + exclude_labels = ( + self._exclude_labels.split("\n") if self._exclude_labels else [] + ) + custom_msgs = ( + self._custom_error_msg.split("\n") if self._custom_error_msg else [] + ) + error_msgs = self._error_msg + custom_msgs # 第一轮筛选出所有未工作的种子 for torrent in all_torrents: trackers = torrent.trackers @@ -302,8 +317,7 @@ class CleanInvalidSeed(_PluginBase): is_tracker_working = True if not ( - (tracker.get("status") == 4) - and (tracker.get("msg") in error_msgs) + (tracker.get("status") == 4) and (tracker.get("msg") in error_msgs) ): is_invalid = False working_tracker_set.add(tracker_domian) @@ -311,17 +325,19 @@ class CleanInvalidSeed(_PluginBase): if is_invalid: temp_invalid_torrents.append(torrent) elif not is_tracker_working: - tracker_not_working_torrents.append(torrent) + # 排除已暂停的种子 + if not torrent.state_enum.is_paused: + tracker_not_working_torrents.append(torrent) logger.info(f"初筛共有{len(temp_invalid_torrents)}个无效做种") # 第二轮筛选出tracker有正常工作种子而当前种子未工作的,避免因临时关站或tracker失效导致误删的问题 - invalid_torrents = [] # 失效做种但通过种子分类排除的种子 invalid_torrents_exclude_categories = [] # 失效做种但通过种子标签排除的种子 invalid_torrents_exclude_labels = [] - deleted_torrents = [] - + # 将invalid_torrents基本信息保存起来,在种子被删除后依然可以打印这些信息 + invalid_torrent_tuple_list = [] + deleted_torrent_tuple_list = [] for torrent in temp_invalid_torrents: trackers = torrent.trackers for tracker in trackers: @@ -330,14 +346,25 @@ class CleanInvalidSeed(_PluginBase): tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1] if tracker_domian in working_tracker_set: # tracker是正常的,说明该种子是无效的 - invalid_torrents.append(torrent) + invalid_torrent_tuple_list.append( + ( + torrent.name, + torrent.category, + torrent.tags, + torrent.size, + tracker_domian, + tracker.msg, + ) + ) if self._delete_invalid_torrents: # 检查种子分类和标签是否排除 is_excluded = False if torrent.category in exclude_categories: is_excluded = True invalid_torrents_exclude_categories.append(torrent) - torrent_labels = [tag.strip() for tag in torrent.tags.split(",")] + torrent_labels = [ + tag.strip() for tag in torrent.tags.split(",") + ] for label in torrent_labels: if label in exclude_labels: is_excluded = True @@ -345,33 +372,28 @@ class CleanInvalidSeed(_PluginBase): if not is_excluded: # 只删除种子不删除文件,以防其它站点辅种 self._qb.delete_torrents(False, torrent.get("hash")) - deleted_torrents.append(torrent) + deleted_torrent_tuple_list.append( + ( + torrent.name, + torrent.category, + torrent.tags, + torrent.size, + tracker_domian, + tracker.msg, + ) + ) break - invalid_msg = f"检测到{len(invalid_torrents)}个失效做种\n" - tracker_not_working_msg = ( - f"检测到{len(tracker_not_working_torrents)}个tracker未工作做种,请检查种子状态\n" - ) + invalid_msg = f"检测到{len(invalid_torrent_tuple_list)}个失效做种\n" + tracker_not_working_msg = f"检测到{len(tracker_not_working_torrents)}个tracker未工作做种,请检查种子状态\n" if self._delete_invalid_torrents: - deleted_msg = f"删除{len(deleted_torrents)}个失效种子\n" + deleted_msg = f"删除{len(deleted_torrent_tuple_list)}个失效种子\n" if len(exclude_categories) != 0: - exclude_categories_msg = ( - f"分类排除{len(invalid_torrents_exclude_categories)}个失效种子未删除,请手动处理\n" - ) + exclude_categories_msg = f"分类排除{len(invalid_torrents_exclude_categories)}个失效种子未删除,请手动处理\n" if len(exclude_labels) != 0: - exclude_labels_msg = ( - f"标签排除{len(invalid_torrents_exclude_labels)}个失效种子未删除,请手动处理\n" - ) - for index in range(len(invalid_torrents)): - torrent = invalid_torrents[index] - trackers = torrent.trackers - tracker_msg = "" - for tracker in trackers: - if tracker.get("tier") == -1: - continue - tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1] - tracker_msg += f" {tracker_domian}:{tracker.msg} " - - invalid_msg += f"{index + 1}. {torrent.name},分类:{torrent.category},标签:{torrent.tags}, 大小:{StringUtils.str_filesize(torrent.size)},Trackers: {tracker_msg}\n" + exclude_labels_msg = f"标签排除{len(invalid_torrents_exclude_labels)}个失效种子未删除,请手动处理\n" + for index in range(len(invalid_torrent_tuple_list)): + torrent = invalid_torrent_tuple_list[index] + invalid_msg += f"{index + 1}. {torrent[0]},分类:{torrent[1]},标签:{torrent[2]}, 大小:{StringUtils.str_filesize(torrent[3])},Trackers: {torrent[4]}:{torrent[5]}\n" for index in range(len(tracker_not_working_torrents)): torrent = tracker_not_working_torrents[index] @@ -406,16 +428,9 @@ class CleanInvalidSeed(_PluginBase): tracker_msg += f" {tracker_domian}:{tracker.msg} " exclude_labels_msg += f"{index + 1}. {torrent.name},分类:{torrent.category},标签:{torrent.tags}, 大小:{StringUtils.str_filesize(torrent.size)},Trackers: {tracker_msg}\n" - for index in range(len(deleted_torrents)): - torrent = deleted_torrents[index] - trackers = torrent.trackers - tracker_msg = "" - for tracker in trackers: - if tracker.get("tier") == -1: - continue - tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1] - tracker_msg += f" {tracker_domian}:{tracker.msg} " - deleted_msg += f"{index + 1}. {torrent.name},分类:{torrent.category},标签:{torrent.tags}, 大小:{StringUtils.str_filesize(torrent.size)},Trackers: {tracker_msg}\n" + for index in range(len(deleted_torrent_tuple_list)): + torrent = deleted_torrent_tuple_list[index] + deleted_msg += f"{index + 1}. {torrent[0]},分类:{torrent[1]},标签:{torrent[2]}, 大小:{StringUtils.str_filesize(torrent[3])},Trackers: {torrent[4]}:{torrent[5]}\n" # 日志 logger.info(invalid_msg) @@ -428,34 +443,34 @@ class CleanInvalidSeed(_PluginBase): logger.info(exclude_labels_msg) # 通知 if self._notify: - invalid_msg = invalid_msg.replace('_', '\_') + invalid_msg = invalid_msg.replace("_", "\_") self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=invalid_msg, ) if self._notify_all: - tracker_not_working_msg = tracker_not_working_msg.replace('_', '\_') + tracker_not_working_msg = tracker_not_working_msg.replace("_", "\_") self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=tracker_not_working_msg, ) if self._delete_invalid_torrents: - deleted_msg = deleted_msg.replace('_', '\_') + deleted_msg = deleted_msg.replace("_", "\_") self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=deleted_msg, ) if self._notify_all: - exclude_categories_msg = exclude_categories_msg.replace('_', '\_') + exclude_categories_msg = exclude_categories_msg.replace("_", "\_") self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=exclude_categories_msg, ) - exclude_labels_msg = exclude_labels_msg.replace('_', '\_') + exclude_labels_msg = exclude_labels_msg.replace("_", "\_") self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", @@ -472,7 +487,9 @@ class CleanInvalidSeed(_PluginBase): source_paths = [] total_size = 0 deleted_file_cnt = 0 - exclude_key_words = self._exclude_keywords.split("\n") if self._exclude_keywords else [] + exclude_key_words = ( + self._exclude_keywords.split("\n") if self._exclude_keywords else [] + ) if not self._download_dirs: logger.error("未配置下载目录,无法检测未做种无效源文件") self.post_message( @@ -493,6 +510,15 @@ class CleanInvalidSeed(_PluginBase): message = "检测未做种无效源文件:\n" for source_path_str in source_paths: source_path = Path(source_path_str) + # 判断source_path是否存在 + if not source_path.exists(): + logger.error(f"{source_path} 不存在,无法检测未做种无效源文件") + self.post_message( + mtype=NotificationType.SiteMessage, + title=f"【检测无效源文件】", + text=f"{source_path} 不存在,无法检测未做种无效源文件", + ) + continue source_files = [] # 获取source_path下的所有文件包括文件夹 for file in source_path.iterdir(): @@ -532,7 +558,7 @@ class CleanInvalidSeed(_PluginBase): message += f"***已删除无效源文件,释放{StringUtils.str_filesize(total_size)}空间!***\n" logger.info(message) if self._notify: - message = message.replace('_', '\_') + message = message.replace("_", "\_") self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", @@ -818,23 +844,3 @@ class CleanInvalidSeed(_PluginBase): self._scheduler = None except Exception as e: logger.error("退出插件失败:%s" % str(e)) - - -if __name__ == "__main__": - clean = CleanInvalidSeed() - config = { - "enabled": True, - "notify": True, - "download_dirs": "/sata16t/春天:/保种/春天\n/sata16t/观众:/保种/观众\n/sata16t/UB:/保种/UB\n/sata16t/听听歌:/保种/听听歌\n/ssd/Download/shualiu:/Downloads/shualiu", - "delete_invalid_torrents": False, - "delete_invalid_files": False, - "detect_invalid_files": True, - "notify_all": False, - "onlyonce": False, - "cron": "0 0 * * *", - "exclude_keywords": "ABF-075\nIPZZ-002-C_GG5\nIPZZ-061\n.!qB", - "exclude_categories": "电影", - "exclude_labels": "春天", - } - clean.init_plugin(config) - clean.clean_invalid_seed() \ No newline at end of file diff --git a/plugins/configcenter/__init__.py b/plugins/configcenter/__init__.py index 8576e73..be4b816 100644 --- a/plugins/configcenter/__init__.py +++ b/plugins/configcenter/__init__.py @@ -17,7 +17,7 @@ class ConfigCenter(_PluginBase): # 插件图标 plugin_icon = "setting.png" # 插件版本 - plugin_version = "2.4" + plugin_version = "2.5" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -37,7 +37,8 @@ class ConfigCenter(_PluginBase): "GITHUB_TOKEN", "API_TOKEN", "TMDB_API_DOMAIN", "TMDB_IMAGE_DOMAIN", "WALLPAPER", "RECOGNIZE_SOURCE", "SCRAP_FOLLOW_TMDB", "AUTO_DOWNLOAD_USER", "OCR_HOST", "DOWNLOAD_SUBTITLE", "PLUGIN_MARKET", "MOVIE_RENAME_FORMAT", - "TV_RENAME_FORMAT", "FANART_ENABLE", "DOH_ENABLE", "SEARCH_MULTIPLE_NAME", "META_CACHE_EXPIRE" + "TV_RENAME_FORMAT", "FANART_ENABLE", "DOH_ENABLE", "SEARCH_MULTIPLE_NAME", "META_CACHE_EXPIRE", + "GITHUB_PROXY" ] def init_plugin(self, config: dict = None): @@ -333,6 +334,27 @@ class ConfigCenter(_PluginBase): } ] }, + { + 'component': 'VRow', + 'content': [ + { + "component": "VCol", + "props": { + "cols": 12, + }, + "content": [ + { + "component": "VTextField", + "props": { + "model": "GITHUB_PROXY", + "label": "Github加速服务器", + "placeholder": "https://mirror.ghproxy.com/" + } + } + ] + } + ] + }, { 'component': 'VRow', 'content': [ diff --git a/plugins/dirmonitor/__init__.py b/plugins/dirmonitor/__init__.py index 82cb002..f6623e6 100644 --- a/plugins/dirmonitor/__init__.py +++ b/plugins/dirmonitor/__init__.py @@ -59,7 +59,7 @@ class DirMonitor(_PluginBase): # 插件图标 plugin_icon = "directory.png" # 插件版本 - plugin_version = "2.2" + plugin_version = "2.3" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -174,7 +174,8 @@ class DirMonitor(_PluginBase): 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} 的子目录,无法监控", title="目录监控") + self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控", + title="目录监控") continue except Exception as e: logger.debug(str(e)) @@ -429,6 +430,20 @@ class DirMonitor(_PluginBase): return if not transferinfo.success: + # 判断是否转移后文件已存在,补充转移成功历史记录 + if transferinfo.target_path and transferinfo.target_path.exists(): + logger.info(f"{file_path.name} 目标文件已存在,补充转移成功历史记录") + # 补充转移成功历史记录 + self.transferhis.add_success( + src_path=file_path, + mode=transfer_type, + download_hash=download_hash, + meta=file_meta, + mediainfo=mediainfo, + transferinfo=transferinfo + ) + return + # 转移失败 logger.warn(f"{file_path.name} 入库失败:{transferinfo.message}") # 新增转移失败历史记录 diff --git a/plugins/downloaderhelper/__init__.py b/plugins/downloaderhelper/__init__.py index 5c69377..9a5af12 100644 --- a/plugins/downloaderhelper/__init__.py +++ b/plugins/downloaderhelper/__init__.py @@ -32,11 +32,11 @@ class DownloaderHelper(_PluginBase): # 插件名称 plugin_name = "下载器助手" # 插件描述 - plugin_desc = "自动做种、站点标签、自动删种。" + plugin_desc = "自动标签、自动做种、自动删种。" # 插件图标 plugin_icon = "DownloaderHelper.png" # 插件版本 - plugin_version = "2.7" + plugin_version = "2.8" # 插件作者 plugin_author = "hotlcc" # 作者主页 @@ -103,6 +103,12 @@ class DownloaderHelper(_PluginBase): 'mdi-download-box': 'M5 3h14a2 2 0 0 1 2 2v14c0 1.11-.89 2-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2m3 14h8v-2H8zm8-7h-2.5V7h-3v3H8l4 4z', 'mdi-content-save': 'M15 9H5V5h10m-3 14a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3a3 3 0 0 1-3 3m5-16H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7z', } + # 公共 tracker url + __public_tracker_urls = [ + "** [DHT] **", + "** [PeX] **", + "** [LSD] **", + ] def init_plugin(self, config: dict = None): """ @@ -202,6 +208,7 @@ class DownloaderHelper(_PluginBase): 'cron': '0/30 * * * *', 'exclude_tags': 'BT,刷流', 'dashboard_widget_refresh': 5, + 'dashboard_speed_widget_refresh': 5, } # 合并默认配置 config_suggest.update(self.__config_default) @@ -221,6 +228,11 @@ class DownloaderHelper(_PluginBase): }, 'content': [{ 'component': 'VRow', + 'props': { + 'style': { + 'margin-top': '0' + } + }, 'content': [{ 'component': 'VCol', 'props': { @@ -244,9 +256,9 @@ class DownloaderHelper(_PluginBase): 'content': [{ 'component': 'VSwitch', 'props': { - 'model': f'{d.short_id}_enable_seeding', - 'label': '自动做种', - 'hint': '是否开启自动做种功能' + 'model': f'{d.short_id}_enable_tagging', + 'label': '自动标签', + 'hint': '是否开启自动标签功能;包含BT/PT标签和站点标签(仅PT有效);不受【排除种子标签】限制。' } }] }, { @@ -258,9 +270,9 @@ class DownloaderHelper(_PluginBase): 'content': [{ 'component': 'VSwitch', 'props': { - 'model': f'{d.short_id}_enable_tagging', - 'label': '站点标签', - 'hint': '是否开启站点标签功能' + 'model': f'{d.short_id}_enable_seeding', + 'label': '自动做种', + 'hint': '是否开启自动做种功能;受【排除种子标签】限制。' } }] }, { @@ -274,7 +286,7 @@ class DownloaderHelper(_PluginBase): 'props': { 'model': f'{d.short_id}_enable_delete', 'label': '自动删种', - 'hint': '是否开启自动删种功能' + 'hint': '是否开启自动删种功能;受【排除种子标签】限制。' } }] }] @@ -346,7 +358,7 @@ class DownloaderHelper(_PluginBase): 'props': { 'model': 'listen_download_event', 'label': '监听下载事件', - 'hint': '监听下载添加事件。当MoviePilot添加下载任务时,会触发执行本插件进行自动做种和添加站点标签。' + 'hint': '监听下载添加事件。当MoviePilot添加下载任务时,会触发本插件进行自动标签和自动做种。' } }] }, { @@ -360,7 +372,7 @@ class DownloaderHelper(_PluginBase): 'props': { 'model': 'listen_source_file_event', 'label': '监听源文件事件', - 'hint': '监听源文件删除事件。当在【历史记录】中删除源文件时,会自动触发运行本插件任务进行自动删种。' + 'hint': '监听源文件删除事件以及原种删除事件。当在【历史记录】中删除源文件时,会触发本插件进行自动删种。' } }] }, { @@ -505,7 +517,7 @@ class DownloaderHelper(_PluginBase): ':\n\n' '例如:\n' 'chdbits.xyz:ptchdbits.co', - 'hint': 'Tracker映射。用于在站点打标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。' + 'hint': 'Tracker映射。用于在站点标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。' } }] }] @@ -1380,46 +1392,78 @@ class DownloaderHelper(_PluginBase): return site_tag, delete_suggest @classmethod - def __check_need_delete_for_qbittorrent(cls, torrent: TorrentDictionary, deleted_event_data: dict = None) -> bool: + def __check_need_delete_for_qbittorrent(cls, torrent: TorrentDictionary, context: TaskContext) -> bool: """ 检查qb种子是否满足删除条件 - :param deleted_event_data: 任务执行伴随的源文件删除事件数据 + :param context: 任务上下文 """ - if not torrent: + if not torrent or not context: return False # 根据种子状态判断是否应该删种:状态为丢失文件时需要删除 if torrent.get('state') == 'missingFiles': return True - # 根据伴随的源文件删除事件判断是否应该删种:如果当前种子和事件匹配并且种子中已经不存在数据文件时就需要删除 - match, torrent_data_path = cls.__check_torrent_match_file_for_qbittorrent(torrent=torrent, - source_file_info=deleted_event_data) - if not match: - return False - # 如果匹配的种子数据路径不存在,说明数据文件已经(全部)被删除了,那么就允许删种 - return not os.path.exists(torrent_data_path) + # 源文件删除事件数据 + download_file_deleted_event_data = context.get_download_file_deleted_event_data() + # 下载任务删除事件数据 + download_deleted_event_data = context.get_download_deleted_event_data() + + # 源文件删除事件触发 + if download_file_deleted_event_data: + # 根据伴随的源文件删除事件判断是否应该删种:如果当前种子和事件匹配并且种子中已经不存在数据文件时就需要删除 + match, torrent_data_path = cls.__check_torrent_match_file_for_qbittorrent(torrent=torrent, + source_file_info=download_file_deleted_event_data) + if not match: + return False + # 如果匹配的种子数据路径不存在,说明数据文件已经(全部)被删除了,那么就允许删种 + return not os.path.exists(torrent_data_path) + # 下载任务删除事件触发 + elif download_deleted_event_data: + torrent_info = download_deleted_event_data + match = cls.__check_torrent_match_torrent_info(torrent_hash=torrent.get('hash'), + torrent_data_file_name=torrent.get('name'), + torrent_size=torrent.get('total_size'), + torrent_info=torrent_info) + return match + return False @classmethod - def __check_need_delete_for_transmission(cls, torrent: Torrent, deleted_event_data: dict = None) -> bool: + def __check_need_delete_for_transmission(cls, torrent: Torrent, context: TaskContext) -> bool: """ 检查tr种子是否满足删除条件 :param deleted_event_data: 任务执行伴随的源文件删除事件数据 """ - if not torrent: + if not torrent or not context: return False # 根据种子状态判断是否应该删种:状态为丢失文件时需要删除 if torrent.error == 3 and torrent.error_string and 'No data found' in torrent.error_string: return True - # 根据伴随的源文件删除事件判断是否应该删种:如果当前种子和事件匹配并且种子中已经不存在数据文件时就需要删除 - match, torrent_data_path = cls.__check_torrent_match_file_for_transmission(torrent=torrent, - source_file_info=deleted_event_data) - if not match: - return False - # 如果匹配的种子数据路径不存在,说明数据文件已经(全部)被删除了,那么就允许删种 - return not os.path.exists(torrent_data_path) + # 源文件删除事件数据 + download_file_deleted_event_data = context.get_download_file_deleted_event_data() + # 下载任务删除事件数据 + download_deleted_event_data = context.get_download_deleted_event_data() + + # 源文件删除事件触发 + if download_file_deleted_event_data: + # 根据伴随的源文件删除事件判断是否应该删种:如果当前种子和事件匹配并且种子中已经不存在数据文件时就需要删除 + match, torrent_data_path = cls.__check_torrent_match_file_for_transmission(torrent=torrent, + source_file_info=download_file_deleted_event_data) + if not match: + return False + # 如果匹配的种子数据路径不存在,说明数据文件已经(全部)被删除了,那么就允许删种 + return not os.path.exists(torrent_data_path) + # 下载任务删除事件触发 + elif download_deleted_event_data: + torrent_info = download_deleted_event_data + match = cls.__check_torrent_match_torrent_info(torrent_hash=torrent.hashString, + torrent_data_file_name=torrent.name, + torrent_size=torrent.total_size, + torrent_info=torrent_info) + return match + return False @classmethod def __check_torrent_match_file_for_qbittorrent(cls, torrent: TorrentDictionary, @@ -1489,6 +1533,25 @@ class DownloaderHelper(_PluginBase): return False, None + @classmethod + def __check_torrent_match_torrent_info(cls, + torrent_hash: str, + torrent_data_file_name: str, + torrent_size: int, + torrent_info: dict) -> bool: + """ + 判断种子是否和种子信息匹配 + :param torrent_hash: 种子hash + :param torrent_data_file_name: 种子数据文件名 + :param torrent_size: 种子大小 + :param torrent_info: 被判断的其它种子信息 + :return: 是否匹配 + """ + if not torrent_hash or not torrent_data_file_name or not torrent_size or not torrent_info: + return False + return torrent_data_file_name == torrent_info.get("title") \ + and torrent_size == torrent_info.get("size") + def __send_notify(self, context: TaskContext): """ 发送通知 @@ -1625,10 +1688,10 @@ class DownloaderHelper(_PluginBase): return context if not context.is_selected_qb_downloader(): return context - enable_seeding = True if self.__get_config_item( - config_key='qb_enable_seeding') and context.is_enabled_seeding() else False enable_tagging = True if self.__get_config_item( config_key='qb_enable_tagging') and context.is_enabled_tagging() else False + enable_seeding = True if self.__get_config_item( + config_key='qb_enable_seeding') and context.is_enabled_seeding() else False enable_delete = True if self.__get_config_item( config_key='qb_enable_delete') and context.is_enabled_delete() else False if not enable_seeding and not enable_tagging and not enable_delete: @@ -1666,24 +1729,24 @@ class DownloaderHelper(_PluginBase): return context logger.info( - f'子任务执行状态: 自动做种={enable_seeding}, 自动打标={enable_tagging}, 自动删种={enable_delete}') + f'子任务执行状态: 自动标签={enable_tagging}, 自动做种={enable_seeding}, 自动删种={enable_delete}') - # 做种 + # 自动标签 + if enable_tagging: + result.set_tagging(self.__tagging_batch_for_qbittorrent(qbittorrent=qbittorrent, torrents=torrents)) + if self.__exit_event.is_set(): + logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') + return context + # 自动做种 if enable_seeding: result.set_seeding(self.__seeding_batch_for_qbittorrent(torrents=torrents)) if self.__exit_event.is_set(): logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') return context - # 打标 - if enable_tagging: - result.set_tagging(self.__tagging_batch_for_qbittorrent(torrents=torrents)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - # 删种 + # 自动删种 if enable_delete: result.set_delete(self.__delete_batch_for_qbittorrent(qbittorrent=qbittorrent, torrents=torrents, - deleted_event_data=context.get_deleted_event_data())) + context=context)) if self.__exit_event.is_set(): logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') return context @@ -1696,10 +1759,10 @@ class DownloaderHelper(_PluginBase): def __seeding_batch_for_qbittorrent(self, torrents: List[TorrentDictionary]) -> int: """ - qb批量做种 + qb批量自动做种 :return: 做种数 """ - logger.info('[QB]批量做种开始...') + logger.info('[QB]批量自动做种开始...') count = 0 if not torrents: return count @@ -1709,12 +1772,12 @@ class DownloaderHelper(_PluginBase): return count if self.__seeding_single_for_qbittorrent(torrent=torrent): count += 1 - logger.info('[QB]批量做种结束') + logger.info('[QB]批量自动做种结束') return count def __seeding_single_for_qbittorrent(self, torrent: TorrentDictionary) -> bool: """ - qb单个做种 + qb单个自动做种 :return: 是否执行 """ if not torrent: @@ -1728,15 +1791,17 @@ class DownloaderHelper(_PluginBase): if not need_seeding: return False torrent.resume() - logger.info(f"[QB]单个做种完成: hash = {torrent.get('hash')}, name = {torrent.get('name')}") + logger.info(f"[QB]单个自动做种完成: hash = {torrent.get('hash')}, name = {torrent.get('name')}") return True - def __tagging_batch_for_qbittorrent(self, torrents: List[TorrentDictionary]) -> int: + def __tagging_batch_for_qbittorrent(self, + qbittorrent: Qbittorrent, + torrents: List[TorrentDictionary]) -> int: """ - qb批量打标 + qb批量自动标签 :return: 打标数 """ - logger.info('[QB]批量打标开始...') + logger.info('[QB]批量自动标签开始...') count = 0 if not torrents: return count @@ -1744,49 +1809,110 @@ class DownloaderHelper(_PluginBase): if self.__exit_event.is_set(): logger.warn('插件服务正在退出,子任务终止') return count - if self.__tagging_single_for_qbittorrent(torrent=torrent): + if self.__tagging_single_for_qbittorrent(qbittorrent=qbittorrent, torrent=torrent): count += 1 - logger.info('[QB]批量打标结束') + logger.info('[QB]批量自动标签结束') return count - def __tagging_single_for_qbittorrent(self, torrent: TorrentDictionary) -> bool: + def __tagging_single_for_qbittorrent(self, + qbittorrent: Qbittorrent, + torrent: TorrentDictionary) -> bool: """ - qb单个打标签 + qb单个自动标签 :return: 是否执行 """ if not torrent: return False + + hash_str = torrent.get('hash') # 种子当前已经存在的标签 torrent_tags = self.__split_tags(torrent.get('tags')) - # 判断种子中是否存在排除的标签 - if self.__exists_exclude_tag(torrent_tags): - return False - # 种子的tracker地址 - tracker_url = self.__parse_tracker_for_qbittorrent(torrent=torrent) - if not tracker_url: - return False - # 获取标签建议 - site_tag, delete_suggest = self.__consult_site_tag_by_tracker(tracker_url=tracker_url) - # 移除建议删除的标签 - if delete_suggest and len(delete_suggest) > 0: - to_deletes = [to_delete for to_delete in delete_suggest if to_delete in torrent_tags] - if to_deletes and len(to_deletes) > 0: - torrent.remove_tags(to_deletes) - # 如果本次不需要打标签 - if not site_tag or site_tag in torrent_tags: + # 需要移除的标签 + remove_tags = None + # 要添加的标签 + add_tags = [] + + # 处理BT/PT标签 + if "BT" not in torrent_tags and "PT" not in torrent_tags: + is_private = self.__check_private_torrent_for_qbittorrent(qbittorrent=qbittorrent, hash_str=hash_str) + btpt_tag = "PT" if is_private else "BT" + add_tags.append(btpt_tag) + + # 处理站点标签 + # BT种子与站点无关,故排除BT标签 + if "BT" not in torrent_tags and "BT" not in add_tags: + # 种子的tracker地址 + tracker_url = self.__parse_tracker_for_qbittorrent(torrent=torrent) + if tracker_url: + # 获取标签建议 + site_tag, delete_suggest = self.__consult_site_tag_by_tracker(tracker_url=tracker_url) + # 移除建议删除的标签 + if delete_suggest: + remove_tags = [to_delete for to_delete in delete_suggest if to_delete and to_delete in torrent_tags] + # 如果本次需要打标签 + if site_tag and site_tag not in torrent_tags and site_tag not in add_tags: + add_tags.append(site_tag) + + if not remove_tags and not add_tags: return False + if remove_tags: + torrent.remove_tags(tags=remove_tags) # 打标签 - torrent.add_tags(site_tag) - logger.info(f"[QB]单个打标成功: hash = {torrent.get('hash')}, name = {torrent.get('name')}") + if add_tags: + torrent.add_tags(tags=add_tags) + logger.info(f"[QB]单个自动标签成功: hash = {hash_str}, name = {torrent.get('name')}") + # Flush 标签 + self.__flush_torrent_tags_for_qbittorrent(torrent=torrent, remove_tags=remove_tags, add_tags=add_tags) return True - def __delete_batch_for_qbittorrent(self, qbittorrent: Qbittorrent, torrents: List[TorrentDictionary], - deleted_event_data: dict = None) -> int: + def __flush_torrent_tags_for_qbittorrent(self, torrent: TorrentDictionary, remove_tags: List[str], add_tags: List[str]): """ - qb批量删种 + qb Flush 标签到种子信息中(即更新内存数据) + """ + try: + if not torrent: + return + torrent_tags = self.__split_tags(torrent.get('tags')) + if remove_tags: + for remove_tag in remove_tags: + if remove_tag and remove_tag in torrent_tags: + torrent_tags.remove(remove_tag) + if add_tags: + for add_tag in add_tags: + if add_tag and add_tag not in torrent_tags: + torrent_tags.add(add_tag) + tag_str = ', '.join(torrent_tags) + torrent.update({'tags': tag_str}) + except Exception as e: + logger.error(f'Flush种子标签异常: {str(e)}', exc_info=True) + + def __check_private_torrent_for_qbittorrent(self, + qbittorrent: Qbittorrent, + hash_str: str) -> bool: + """ + qb检查种子是否是私有种子 + :return: 是否是私有种子 + """ + trackers = qbittorrent.qbc.torrents_trackers(torrent_hash=hash_str) + if not trackers: + return False + for tracker in trackers: + if not tracker: + continue + url = tracker.get("url") + status = tracker.get("status") + tier = tracker.get("tier") + if url in self.__public_tracker_urls and status == 0 and tier == -1: + return True + return False + + def __delete_batch_for_qbittorrent(self, qbittorrent: Qbittorrent, torrents: List[TorrentDictionary], + context: TaskContext) -> int: + """ + qb批量自动删种 :return: 删种数 """ - logger.info('[QB]批量删种开始...') + logger.info('[QB]批量自动删种开始...') count = 0 if not torrents: return count @@ -1795,15 +1921,15 @@ class DownloaderHelper(_PluginBase): logger.warn('插件服务正在退出,子任务终止') return count if (self.__delete_single_for_qbittorrent(qbittorrent=qbittorrent, torrent=torrent, - deleted_event_data=deleted_event_data)): + context=context)): count += 1 - logger.info('[QB]批量删种结束') + logger.info('[QB]批量自动删种结束') return count def __delete_single_for_qbittorrent(self, qbittorrent: Qbittorrent, torrent: TorrentDictionary, - deleted_event_data: dict = None) -> bool: + context: TaskContext) -> bool: """ - qb单个删种 + qb单个自动删种 :return: 是否执行 """ if not torrent: @@ -1813,10 +1939,10 @@ class DownloaderHelper(_PluginBase): # 判断种子中是否存在排除的标签 if self.__exists_exclude_tag(torrent_tags): return False - if not self.__check_need_delete_for_qbittorrent(torrent=torrent, deleted_event_data=deleted_event_data): + if not self.__check_need_delete_for_qbittorrent(torrent=torrent, context=context): return False qbittorrent.delete_torrents(True, torrent.get('hash')) - logger.info(f"[QB]单个删种完成: hash = {torrent.get('hash')}, name = {torrent.get('name')}") + logger.info(f"[QB]单个自动删种完成: hash = {torrent.get('hash')}, name = {torrent.get('name')}") return True def __run_for_transmission(self, context: TaskContext = None) -> TaskContext: @@ -1834,10 +1960,10 @@ class DownloaderHelper(_PluginBase): return context if not context.is_selected_tr_downloader(): return context - enable_seeding = True if self.__get_config_item( - config_key='tr_enable_seeding') and context.is_enabled_seeding() else False enable_tagging = True if self.__get_config_item( config_key='tr_enable_tagging') and context.is_enabled_tagging() else False + enable_seeding = True if self.__get_config_item( + config_key='tr_enable_seeding') and context.is_enabled_seeding() else False enable_delete = True if self.__get_config_item( config_key='tr_enable_delete') and context.is_enabled_delete() else False if not enable_seeding and not enable_tagging and not enable_delete: @@ -1859,8 +1985,15 @@ class DownloaderHelper(_PluginBase): context.save_result(result=result) - torrents, error = transmission.get_torrents() - if error: + # 获取全部种子 + # 需要 isPrivate 字段判断是否是私有种子 + arguments = transmission._trarg.copy() + is_private_field = "isPrivate" + if is_private_field not in arguments: + arguments.append(is_private_field) + try: + torrents = transmission.trc.get_torrents(arguments=arguments) + except Exception as e: logger.warn(f'从下载器[{downloader_name}]中获取种子失败,任务终止') return context if not torrents or len(torrents) <= 0: @@ -1877,24 +2010,24 @@ class DownloaderHelper(_PluginBase): return context logger.info( - f'子任务执行状态: 自动做种={enable_seeding}, 自动打标={enable_tagging}, 自动删种={enable_delete}') + f'子任务执行状态: 自动标签={enable_tagging}, 自动做种={enable_seeding}, 自动删种={enable_delete}') - # 做种 - if enable_seeding: - result.set_seeding(self.__seeding_batch_for_transmission(transmission=transmission, torrents=torrents)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - # 打标 + # 自动标签 if enable_tagging: result.set_tagging(self.__tagging_batch_for_transmission(transmission=transmission, torrents=torrents)) if self.__exit_event.is_set(): logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') return context - # 删种 + # 自动做种 + if enable_seeding: + result.set_seeding(self.__seeding_batch_for_transmission(transmission=transmission, torrents=torrents)) + if self.__exit_event.is_set(): + logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') + return context + # 自动删种 if enable_delete: result.set_delete(self.__delete_batch_for_transmission(transmission=transmission, torrents=torrents, - deleted_event_data=context.get_deleted_event_data())) + context=context)) if self.__exit_event.is_set(): logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') return context @@ -1907,10 +2040,10 @@ class DownloaderHelper(_PluginBase): def __seeding_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent]) -> int: """ - tr批量做种 + tr批量自动做种 :return: 做种数 """ - logger.info('[TR]批量做种开始...') + logger.info('[TR]批量自动做种开始...') count = 0 if not torrents: return count @@ -1920,12 +2053,12 @@ class DownloaderHelper(_PluginBase): return count if self.__seeding_single_for_transmission(transmission=transmission, torrent=torrent): count += 1 - logger.info('[TR]批量做种结束') + logger.info('[TR]批量自动做种结束') return count def __seeding_single_for_transmission(self, transmission: Transmission, torrent: Torrent) -> bool: """ - tr单个做种 + tr单个自动做种 :return: 是否执行 """ if not torrent: @@ -1939,15 +2072,15 @@ class DownloaderHelper(_PluginBase): if not need_seeding: return False transmission.start_torrents(torrent.hashString) - logger.info(f"[TR]单个做种完成: hash = {torrent.hashString}, name = {torrent.get('name')}") + logger.info(f"[TR]单个自动做种完成: hash = {torrent.hashString}, name = {torrent.get('name')}") return True def __tagging_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent]) -> int: """ - tr批量打标 + tr批量自动标签 :return: 打标数 """ - logger.info('[TR]批量打标开始...') + logger.info('[TR]批量自动标签开始...') count = 0 if not torrents: return count @@ -1957,52 +2090,89 @@ class DownloaderHelper(_PluginBase): return count if self.__tagging_single_for_transmission(transmission=transmission, torrent=torrent): count += 1 - logger.info('[TR]批量打标结束') + logger.info('[TR]批量自动标签结束') return count def __tagging_single_for_transmission(self, transmission: Transmission, torrent: Torrent) -> bool: """ - tr单个打标签 + tr单个自动标签 :return: 是否执行 """ if not torrent: return False + + hash_str = torrent.hashString # 种子当前已经存在的标签 - torrent_tags = torrent.get('labels') - # 判断种子中是否存在排除的标签 - if self.__exists_exclude_tag(torrent_tags): - return False - # 种子的tracker地址 - tracker_url = self.__parse_tracker_for_transmission(torrent=torrent) - if not tracker_url: - return False - # 获取标签建议 - site_tag, delete_suggest = self.__consult_site_tag_by_tracker(tracker_url=tracker_url) - # 种子标签副本 - torrent_tags_copy = torrent_tags.copy() - # 移除建议删除的标签 - if delete_suggest and len(delete_suggest) > 0: - for to_delete in delete_suggest: - if to_delete and to_delete in torrent_tags_copy: - torrent_tags_copy.remove(to_delete) - # 如果本次需要打标签 - if site_tag and site_tag not in torrent_tags_copy: - torrent_tags_copy.append(site_tag) + torrent_tags = torrent.get('labels') or [] + # 需要移除的标签 + remove_tags = None + # 要添加的标签 + add_tags = [] + + # 处理BT/PT标签 + if "BT" not in torrent_tags and "PT" not in torrent_tags: + is_private = self.__check_private_torrent_for_transmission(torrent=torrent) + btpt_tag = "PT" if is_private else "BT" + add_tags.append(btpt_tag) + + # 处理站点标签 + # BT种子与站点无关,故排除BT标签 + if "BT" not in torrent_tags and "BT" not in add_tags: + # 种子的tracker地址 + tracker_url = self.__parse_tracker_for_transmission(torrent=torrent) + if tracker_url: + # 获取标签建议 + site_tag, delete_suggest = self.__consult_site_tag_by_tracker(tracker_url=tracker_url) + # 移除建议删除的标签 + if delete_suggest: + remove_tags = [to_delete for to_delete in delete_suggest if to_delete and to_delete in torrent_tags] + # 如果本次需要打标签 + if site_tag and site_tag not in torrent_tags and site_tag not in add_tags: + add_tags.append(site_tag) + # 如果没有变化就不继续保存 - if torrent_tags_copy == torrent_tags: + if not remove_tags and not add_tags: return False + torrent_tags_copy = torrent_tags.copy() + if remove_tags: + for remove_tag in remove_tags: + torrent_tags_copy.remove(remove_tag) + if add_tags: + for add_tag in add_tags: + torrent_tags_copy.append(add_tag) # 保存标签 - transmission.set_torrent_tag(torrent.hashString, torrent_tags_copy) - logger.info(f"[TR]单个打标成功: hash = {torrent.hashString}, name = {torrent.get('name')}") + transmission.set_torrent_tag(hash_str, torrent_tags_copy) + logger.info(f"[TR]单个自动标签成功: hash = {hash_str}, name = {torrent.get('name')}") + # Flush 标签 + self.__flush_torrent_tags_for_transmission(torrent=torrent, tags=torrent_tags_copy) return True - def __delete_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent], - deleted_event_data: dict = None) -> int: + def __flush_torrent_tags_for_transmission(self, torrent: Torrent, tags: List[str]): """ - tr批量删种 + tr Flush 标签到种子信息中(即更新内存数据) + """ + try: + if not torrent: + return + torrent.fields.update({'labels': tags}) + except Exception as e: + logger.error(f'Flush种子标签异常: {str(e)}', exc_info=True) + + def __check_private_torrent_for_transmission(self, + torrent: Torrent) -> bool: + """ + tr检查种子是否是私有种子 + :return: 是否是私有种子 + """ + return torrent.get("isPrivate") + + def __delete_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent], + context: TaskContext) -> int: + """ + tr批量自动删种 :return: 删种数 """ - logger.info('[TR]批量删种开始...') + logger.info('[TR]批量自动删种开始...') count = 0 if not torrents: return count @@ -2011,15 +2181,15 @@ class DownloaderHelper(_PluginBase): logger.warn('插件服务正在退出,子任务终止') return count if (self.__delete_single_for_transmission(transmission=transmission, torrent=torrent, - deleted_event_data=deleted_event_data)): + context=context)): count += 1 - logger.info('[TR]批量删种结束') + logger.info('[TR]批量自动删种结束') return count def __delete_single_for_transmission(self, transmission: Transmission, torrent: Torrent, - deleted_event_data: dict = None) -> bool: + context: TaskContext) -> bool: """ - tr单个删种 + tr单个自动删种 :return: 是否执行 """ if not torrent: @@ -2029,10 +2199,10 @@ class DownloaderHelper(_PluginBase): # 判断种子中是否存在排除的标签 if self.__exists_exclude_tag(torrent_tags): return False - if not self.__check_need_delete_for_transmission(torrent=torrent, deleted_event_data=deleted_event_data): + if not self.__check_need_delete_for_transmission(torrent=torrent, context=context): return False transmission.delete_torrents(True, torrent.hashString) - logger.info(f"'[TR]单个删种完成: hash = {torrent.hashString}, name = {torrent.get('name')}") + logger.info(f"'[TR]单个自动删种完成: hash = {torrent.hashString}, name = {torrent.get('name')}") return True @staticmethod @@ -2504,7 +2674,7 @@ class DownloaderHelper(_PluginBase): 'fixed-header': True, 'density': 'compact', 'style': { - 'height': '230px' + 'height': '242px' } }, 'content': [ @@ -2627,21 +2797,24 @@ class DownloaderHelper(_PluginBase): }] } - def __build_dashboard_speed_widget_list_item_element(self, mdi_icon: str, label: str, value: str) -> dict: + def __build_dashboard_speed_widget_list_item_element(self, mdi_icon: str, label: str, value: str, is_last: bool = False) -> dict: """ 构造仪表板实时速率组件列表item元素 """ if not mdi_icon or not label or not value: return None + div_style = { + 'display': 'grid', + 'grid-template-areas': '"prepend content append"', + 'grid-template-columns': 'max-content 1fr auto', + 'padding-bottom': '16px' + } + if is_last: + del div_style['padding-bottom'] return { 'component': 'div', 'props': { - 'style': { - 'display': 'grid', - 'grid-template-areas': '"prepend content append"', - 'grid-template-columns': 'max-content 1fr auto', - 'padding-bottom': '16px' - } + 'style': div_style }, 'content': [{ 'component': 'div', @@ -2700,13 +2873,13 @@ class DownloaderHelper(_PluginBase): list_items = [ self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-cloud-upload', label='总上传量', value=data.upload_size), self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-download-box', label='总下载量', value=data.download_size), - self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-content-save', label='磁盘剩余空间', value=data.free_space), + self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-content-save', label='磁盘剩余空间', value=data.free_space, is_last=True), ] return [{ 'component': 'div', 'props': { 'style': { - 'padding': '16px 0 20px 0' + 'padding': '16px 0 0 0' } }, 'content': [{ @@ -2739,12 +2912,12 @@ class DownloaderHelper(_PluginBase): 监听下载添加事件 """ logger.info('监听到下载添加事件') - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return if not self.get_state() or not self.__get_config_item(config_key='listen_download_event'): logger.warn('插件状态无效或未开启监听,忽略事件') return + if not event or not event.event_data: + logger.warn('事件信息无效,忽略事件') + return if self.__exit_event.is_set(): logger.warn('插件服务正在退出,忽略事件') return @@ -2769,12 +2942,12 @@ class DownloaderHelper(_PluginBase): 监听源文件删除事件 """ logger.info('监听到源文件删除事件') - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return if not self.get_state() or not self.__get_config_item(config_key='listen_source_file_event'): logger.warn('插件状态无效或未开启监听,忽略事件') return + if not event or not event.event_data: + logger.warn('事件信息无效,忽略事件') + return if self.__exit_event.is_set(): logger.warn('插件服务正在退出,忽略事件') return @@ -2784,6 +2957,41 @@ class DownloaderHelper(_PluginBase): context = TaskContext().enable_seeding(False) \ .enable_tagging(False) \ .enable_delete(True) \ - .set_deleted_event_data(event.event_data) + .set_download_file_deleted_event_data(event.event_data) self.__block_run(context=context) logger.info('源文件删除事件监听任务执行结束') + + @eventmanager.register(EventType.DownloadDeleted) + def listen_download_deleted_event(self, event: Event = None): + """ + 监听下载任务删除事件 + """ + logger.info('监听到下载任务删除事件') + if not self.get_state() or not self.__get_config_item(config_key='listen_source_file_event'): + logger.warn('插件状态无效或未开启监听,忽略事件') + return + if not event or not event.event_data: + logger.warn('事件信息无效,忽略事件') + return + torrents = event.event_data.get("torrents") + if not torrents: + logger.warn('事件信息无效,忽略事件') + return + torrents = [torrent for torrent in torrents if torrent] + if not torrents: + logger.warn('事件信息无效,忽略事件') + return + if self.__exit_event.is_set(): + logger.warn('插件服务正在退出,忽略事件') + return + # 执行 + logger.info('下载任务删除事件监听任务执行开始...') + # 针对下载任务删除事件只需要处理删种 + # 删除的种子信息 + torrent_info = torrents[0] + context = TaskContext().enable_seeding(False) \ + .enable_tagging(False) \ + .enable_delete(True) \ + .set_download_deleted_event_data(torrent_info) + self.__block_run(context=context) + logger.info('下载任务删除事件监听任务执行结束') diff --git a/plugins/downloaderhelper/convertor.py b/plugins/downloaderhelper/convertor.py index d062571..b85d586 100644 --- a/plugins/downloaderhelper/convertor.py +++ b/plugins/downloaderhelper/convertor.py @@ -69,6 +69,8 @@ class StateConvertor(IConvertor, metaclass=Singleton): return '等待' if data == TorrentState.CHECKING_DOWNLOAD.value: return '校验' + if data == TorrentState.QUEUED_DOWNLOAD.value: + return '排队' # tr if data == 6: return '做种' diff --git a/plugins/downloaderhelper/module.py b/plugins/downloaderhelper/module.py index cbabf2d..686b7aa 100644 --- a/plugins/downloaderhelper/module.py +++ b/plugins/downloaderhelper/module.py @@ -95,7 +95,9 @@ class TaskContext: self.__selected_torrents = None # 源文件删除事件数据 - self.__deleted_event_data = None + self.__download_file_deleted_event_data = None + # 下载任务删除事件数据 + self.__download_deleted_event_data = None # 任务结果集 self.__results: Optional[List[TaskResult]] = None @@ -226,18 +228,31 @@ class TaskContext: """ return self.__selected_torrents - def set_deleted_event_data(self, deleted_event_data: dict): + def set_download_file_deleted_event_data(self, download_file_deleted_event_data: dict): """ 设置源文件删除事件数据 """ - self.__deleted_event_data = deleted_event_data + self.__download_file_deleted_event_data = download_file_deleted_event_data return self - def get_deleted_event_data(self) -> dict: + def get_download_file_deleted_event_data(self) -> dict: """ 获取源文件删除事件数据 """ - return self.__deleted_event_data + return self.__download_file_deleted_event_data + + def set_download_deleted_event_data(self, download_deleted_event_data: dict): + """ + 设置下载任务删除事件数据 + """ + self.__download_deleted_event_data = download_deleted_event_data + return self + + def get_download_deleted_event_data(self) -> dict: + """ + 获取下载任务删除事件数据 + """ + return self.__download_deleted_event_data def save_result(self, result: TaskResult): """ diff --git a/plugins/mediasyncdel/__init__.py b/plugins/mediasyncdel/__init__.py index 2ff19c1..41cb858 100644 --- a/plugins/mediasyncdel/__init__.py +++ b/plugins/mediasyncdel/__init__.py @@ -29,7 +29,7 @@ class MediaSyncDel(_PluginBase): # 插件图标 plugin_icon = "mediasyncdel.png" # 插件版本 - plugin_version = "1.6" + plugin_version = "1.7" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -754,6 +754,20 @@ class MediaSyncDel(_PluginBase): logger.error(f"{media_name} 同步删除失败,未获取到媒体类型,请检查媒体是否刮削") return + # 处理路径映射 (处理同一媒体多分辨率的情况) + if self._library_path: + paths = self._library_path.split("\n") + for path in paths: + sub_paths = path.split(":") + if len(sub_paths) < 2: + continue + media_path = media_path.replace(sub_paths[0], sub_paths[1]).replace('\\', '/') + + # 兼容重新整理的场景 + if Path(media_path).exists(): + logger.warn(f"转移路径 {media_path} 未被删除或重新生成,跳过处理") + return + # 查询转移记录 msg, transfer_history = self.__get_transfer_his(media_type=media_type, media_name=media_name, @@ -890,15 +904,6 @@ class MediaSyncDel(_PluginBase): # 类型 mtype = MediaType.MOVIE if media_type in ["Movie", "MOV"] else MediaType.TV - # 处理路径映射 (处理同一媒体多分辨率的情况) - if self._library_path: - paths = self._library_path.split("\n") - for path in paths: - sub_paths = path.split(":") - if len(sub_paths) < 2: - continue - media_path = media_path.replace(sub_paths[0], sub_paths[1]).replace('\\', '/') - # 删除电影 if mtype == MediaType.MOVIE: msg = f'电影 {media_name} {tmdb_id}' diff --git a/plugins/sitestatistic/__init__.py b/plugins/sitestatistic/__init__.py index d99a389..897e1a9 100644 --- a/plugins/sitestatistic/__init__.py +++ b/plugins/sitestatistic/__init__.py @@ -43,7 +43,7 @@ class SiteStatistic(_PluginBase): # 插件图标 plugin_icon = "statistic.png" # 插件版本 - plugin_version = "3.5" + plugin_version = "3.6" # 插件作者 plugin_author = "lightolly" # 作者主页 @@ -1440,6 +1440,11 @@ class SiteStatistic(_PluginBase): # 更新时间 self.save_data("last_update_time", today_date) + + self.eventmanager.send_event(etype=EventType.PluginAction, data={ + "action": "sitestatistic_refresh_complete" + }) + logger.info("站点数据刷新完成") def __custom_sites(self) -> List[Any]: