diff --git a/package.json b/package.json index 5c34843..94c0fbb 100644 --- a/package.json +++ b/package.json @@ -238,11 +238,12 @@ "BrushFlow": { "name": "站点刷流", "description": "自动托管刷流,将会提高对应站点的访问频率。", - "version": "2.6", + "version": "2.7", "icon": "brush.jpg", "author": "jxxghp,InfinityPacer", "level": 2, "history": { + "v2.7": "动态删除种子规则调整(请注意查阅插件文档),站点独立配置样式优化、日志优化,修复部分配置项无法配置小数的问题,修复部分场景可能导致重复下载的问题", "v2.6": "修复排除订阅功能", "v2.5": "增加H&R做种时间、下载器监控配置项,刷流前置条件逻辑调整,代理下载种子默认为关闭" } diff --git a/plugins/brushflow/__init__.py b/plugins/brushflow/__init__.py index 03df5f7..2a865cc 100644 --- a/plugins/brushflow/__init__.py +++ b/plugins/brushflow/__init__.py @@ -85,7 +85,11 @@ class BrushConfig: def __initialize_site_config(self): if not self.site_config: + logger.error(f"没有设置站点配置,已关闭站点独立配置并恢复默认配置示例,请检查配置项") + self.site_config = self.__get_demo_site_config() self.group_site_configs = {} + self.enable_site_config = False + return # 定义允许覆盖的字段列表 allowed_fields = { @@ -130,6 +134,47 @@ class BrushConfig: self.enable_site_config = False self.enabled = False + @staticmethod + def __get_demo_site_config() -> str: + desc = ("//以下为配置示例,请参考 " + "https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md " + "进行配置,请注意,只需要保留实际配置内容(删除这段)\n") + config = """[{ + "sitename": "站点1", + "seed_time": 96, + "hr_seed_time": 144 +}, { + "sitename": "站点2", + "hr": "yes", + "size": "10-500", + "seeder": "5-10", + "pubtime": "5-120", + "seed_time": 96, + "save_path": "/downloads/site2", + "proxy_download": true, + "hr_seed_time": 144 +}, { + "sitename": "站点3", + "freeleech": "free", + "hr": "yes", + "include": "", + "exclude": "", + "size": "10-500", + "seeder": "1", + "pubtime": "5-120", + "seed_time": 120, + "hr_seed_time": 144, + "seed_ratio": "", + "seed_size": "", + "download_time": "", + "seed_avgspeed": "", + "seed_inactivetime": "", + "save_path": "/downloads/site1", + "proxy_download": false, + "proxy_delete": false +}]""" + return desc + config + def get_site_config(self, sitename): """ 根据站点名称获取特定的BrushConfig实例。如果没有找到站点特定的配置,则返回全局的BrushConfig实例。 @@ -192,7 +237,7 @@ class BrushFlow(_PluginBase): # 插件图标 plugin_icon = "brush.jpg" # 插件版本 - plugin_version = "2.6" + plugin_version = "2.7" # 插件作者 plugin_author = "jxxghp,InfinityPacer" # 作者主页 @@ -239,10 +284,6 @@ class BrushFlow(_PluginBase): logger.info("站点刷流任务出错,无法获取插件配置") return False - # 如果没有站点配置时,增加默认的配置项 - if not config.get("site_config"): - config["site_config"] = self.__get_demo_site_config() - # 如果配置校验没有通过,那么这里修改配置文件后退出 if not self.__validate_and_fix_config(config=config): self._brush_config = BrushConfig(config=config) @@ -941,12 +982,6 @@ class BrushFlow(_PluginBase): } ] }, - { - 'component': 'VRow', - 'content': [ - - ] - }, { 'component': 'VRow', 'content': [ @@ -1148,7 +1183,7 @@ class BrushFlow(_PluginBase): "component": "VDialog", "props": { "model": "dialog_closed", - "max-width": "80rem", + "max-width": "65rem", "overlay-class": "v-dialog--scrollable v-overlay--scroll-blocked", "content-class": "v-card v-card--density-default v-card--variant-elevated rounded-t" }, @@ -1174,12 +1209,12 @@ class BrushFlow(_PluginBase): }, 'content': [ { - "component": "VTextarea", + "component": "VAceEditor", "props": { - "model": "site_config", - "placeholder": "请输入站点配置", - "label": "站点配置", - "rows": 16 + 'modelvalue': 'site_config', + 'lang': 'json', + 'theme': 'monokai', + 'style': 'height: 30rem', } } ] @@ -1747,12 +1782,14 @@ class BrushFlow(_PluginBase): size_condition_passed, reason = self.__evaluate_size_condition_for_brush(torrents_size=torrents_size) self.__log_brush_conditions(passed=size_condition_passed, reason=reason) if not size_condition_passed: + logger.info(f"刷流任务执行完成") return # 判断能否通过刷流前置条件 pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush() self.__log_brush_conditions(passed=pre_condition_passed, reason=reason) if not pre_condition_passed: + logger.info(f"刷流任务执行完成") return statistic_info = self.__get_statistic_info() @@ -1823,7 +1860,7 @@ class BrushFlow(_PluginBase): for torrent in torrents: # 判断能否通过刷流前置条件 pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(include_network_conditions=False) - self.__log_brush_conditions(passed=pre_condition_passed, reason=reason, torrent=torrent) + self.__log_brush_conditions(passed=pre_condition_passed, reason=reason) if not pre_condition_passed: return False @@ -1975,10 +2012,19 @@ class BrushFlow(_PluginBase): """ brush_config = self.__get_brush_config(torrent.site_name) + # 排除重复种子 + # 默认根据标题和站点名称进行排除 task_key = f"{torrent.site_name}{torrent.title}" if any(task_key == f"{task.get('site_name')}{task.get('title')}" for task in torrent_tasks.values()): return False, "重复种子" + # 部分站点标题会上新时携带后缀,这里进一步根据种子详情地址进行排除 + if torrent.page_url: + task_page_url = f"{torrent.site_name}{torrent.page_url}" + if any(task_page_url == f"{task.get('site_name')}{task.get('page_url')}" for task in + torrent_tasks.values()): + return False, "重复种子" + # 促销条件 if brush_config.freeleech and torrent.downloadvolumefactor != 0: return False, "非免费种子" @@ -2011,7 +2057,7 @@ class BrushFlow(_PluginBase): # 做种人数 if brush_config.seeder: - seeders_range = [int(n) for n in brush_config.seeder.split("-")] + seeders_range = [float(n) for n in brush_config.seeder.split("-")] # 检查是否仅指定了一个数字,即做种人数需要小于等于该数字 if len(seeders_range) == 1: # 当做种人数大于该数字时,不符合条件 @@ -2027,7 +2073,7 @@ class BrushFlow(_PluginBase): pubdate_minutes = self.__get_pubminutes(torrent.pubdate) pubdate_minutes = self.__adjust_site_pubminutes(pubdate_minutes, torrent) if brush_config.pubtime: - pubtimes = [int(n) for n in brush_config.pubtime.split("-")] + pubtimes = [float(n) for n in brush_config.pubtime.split("-")] if len(pubtimes) == 1: # 单个值:选择发布时间小于等于该值的种子 if pubdate_minutes > pubtimes[0]: @@ -2045,7 +2091,7 @@ class BrushFlow(_PluginBase): """ if not passed: if not torrent: - logger.warn(f"种子没有通过前置刷流条件校验,原因:{reason}") + logger.warn(f"没有通过前置刷流条件校验,原因:{reason}") else: brush_config = self.__get_brush_config() if brush_config.log_more: @@ -2112,7 +2158,7 @@ class BrushFlow(_PluginBase): need_delete_hashes = [] - # 如果配置了删种阈值,则根据动态删种进行分组处理 + # 如果配置了动态删除以及删种阈值,则根据动态删种进行分组处理 if brush_config.proxy_delete and brush_config.delete_size_range: logger.info("已开启动态删种,按系统默认动态删种条件开始检查任务") proxy_delete_hashs = self.__delete_torrent_for_proxy(torrents=check_torrents, @@ -2300,6 +2346,22 @@ class BrushFlow(_PluginBase): return True, reason if not hit_and_run else "H&R种子(未设置H&R条件)," + reason + def __evaluate_proxy_pre_conditions_for_delete(self, site_name: str, torrent_info: dict) -> Tuple[bool, str]: + """ + 评估动态删除前置条件并返回是否应删除种子及其原因 + """ + brush_config = self.__get_brush_config(sitename=site_name) + + reason = "未能满足动态删除设置的前置删除条件" + + if brush_config.download_time and torrent_info.get("downloaded") < torrent_info.get( + "total_size") and torrent_info.get("dltime") >= float(brush_config.download_time) * 3600: + reason = f"下载耗时 {torrent_info.get('dltime') / 3600:.1f} 小时,大于 {brush_config.download_time} 小时" + else: + return False, reason + + return True, reason + def __delete_torrent_for_evaluate_conditions(self, torrents: List[Any], torrent_tasks: Dict[str, dict], proxy_delete: bool = False) -> List: """ @@ -2336,11 +2398,54 @@ class BrushFlow(_PluginBase): return delete_hashs - def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List: + def __delete_torrent_for_evaluate_proxy_pre_conditions(self, torrents: List[Any], + torrent_tasks: Dict[str, dict]) -> List: """ - 支持动态删除种子,当设置了动态删种(全局)和删除阈值时,当做种体积达到删除阈值时,优先按设置规则进行删除,若还没有达到阈值,则排除HR种子后按加入时间倒序进行删除 - 删除阈值:100,当做种体积 > 100G 时,则开始删除种子,直至降低至 100G - 删除阈值:50-100,当做种体积 > 100G 时,则开始删除种子,直至降至为 50G + 根据动态删除前置条件排除H&R种子后删除种子并获取已删除列表 + """ + brush_config = self.__get_brush_config() + delete_hashs = [] + + for torrent in torrents: + torrent_hash = self.__get_hash(torrent) + torrent_task = torrent_tasks.get(torrent_hash, None) + # 如果找不到种子任务,说明不在管理的种子范围内,直接跳过 + if not torrent_task: + continue + + # 如果是H&R种子,前置条件中不进行处理 + if torrent_task.get('hit_and_run', False): + continue + + site_name = torrent_task.get("site_name", "") + torrent_title = torrent_task.get("title", "") + torrent_desc = torrent_task.get("description", "") + + torrent_info = self.__get_torrent_info(torrent) + + # 删除种子的具体实现可能会根据实际情况略有不同 + should_delete, reason = self.__evaluate_proxy_pre_conditions_for_delete(site_name=site_name, + torrent_info=torrent_info) + if should_delete: + delete_hashs.append(torrent_hash) + self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, + reason=reason) + logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}") + else: + if brush_config.log_more: + logger.info(f"站点:{site_name},{reason},不删除种子:{torrent_title}|{torrent_desc}") + + return delete_hashs + + def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List: + """ + 动态删除种子,删除规则如下; + - 不管做种体积是否超过设定的动态删除阈值,默认优先执行排除H&R种子后满足「下载超时时间」的种子 + - 上述规则执行完成后,当做种体积依旧超过设定的动态删除阈值时,继续执行下述种子删除规则 + - 优先删除满足用户设置删除规则的全部种子,即便在删除过程中已经低于了阈值下限,也会继续删除 + - 若删除后还没有达到阈值,则在已完成种子中排除H&R种子后按做种时间倒序进行删除 + - 动态删除阈值:100,当做种体积 > 100G 时,则开始删除种子,直至降低至 100G + - 动态删除阈值:50-100,当做种体积 > 100G 时,则开始删除种子,直至降至为 50G """ brush_config = self.__get_brush_config() @@ -2348,29 +2453,49 @@ class BrushFlow(_PluginBase): if not (brush_config.proxy_delete and brush_config.delete_size_range): return [] + # 获取种子信息Map + torrent_info_map = {self.__get_hash(torrent): self.__get_torrent_info(torrent=torrent) for torrent in torrents} + + # 计算当前总做种体积 + total_torrent_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks) + + logger.info( + f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,正在准备计算满足动态前置删除条件的种子") + + # 执行排除H&R种子后满足前置删除条件的种子 + pre_delete_hashes = self.__delete_torrent_for_evaluate_proxy_pre_conditions(torrents=torrents, + torrent_tasks=torrent_tasks) or [] + + # 如果存在前置删除种子,这里进行额外判断,总做种体积排除前置删除种子的体积 + if pre_delete_hashes: + pre_delete_total_size = sum(torrent_info_map[self.__get_hash(torrent)].get("total_size", 0) + for torrent in torrents if self.__get_hash(torrent) in pre_delete_hashes) + total_torrent_size = total_torrent_size - pre_delete_total_size + torrents = [torrent for torrent in torrents if self.__get_hash(torrent) not in pre_delete_hashes] + logger.info( + f"满足动态删除前置条件的种子共 {len(pre_delete_hashes)} 个,体积 {self.__bytes_to_gb(pre_delete_total_size):.1f} GB," + f"删除种子后,当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB") + else: + logger.info(f"没有找到任何满足动态删除前置条件的种子") + # 解析删除阈值范围 sizes = [float(size) * 1024 ** 3 for size in brush_config.delete_size_range.split("-")] min_size = sizes[0] # 至少需要达到的做种体积 max_size = sizes[1] if len(sizes) > 1 else sizes[0] # 触发删除操作的做种体积上限 - torrent_info_map = {self.__get_hash(torrent): self.__get_torrent_info(torrent=torrent) for torrent in torrents} - - # 计算当前总做种体积 - # total_torrent_size = sum(info.get("total_size", 0) for info in torrent_info_map.values()) - total_torrent_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks) - # 当总体积未超过最大阈值时,不需要执行删除操作 if total_torrent_size < max_size: logger.info( f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB," - f"下限 {self.__bytes_to_gb(min_size):.1f} GB,未触发动态删除") - return [] + f"下限 {self.__bytes_to_gb(min_size):.1f} GB,未进一步触发动态删除") + return pre_delete_hashes or [] else: logger.info( f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB," - f"下限 {self.__bytes_to_gb(min_size):.1f} GB,触发动态删除") + f"下限 {self.__bytes_to_gb(min_size):.1f} GB,进一步触发动态删除") need_delete_hashes = [] + need_delete_hashes.extend(pre_delete_hashes) # 即使开了动态删除,但是也有可能部分站点单独设置了关闭,这里根据种子托管进行分组,先处理不需要托管的种子,按设置的规则进行删除 proxy_delete_torrents, not_proxy_delete_torrents = self.__group_torrents_by_proxy_delete(torrents=torrents, @@ -2395,7 +2520,6 @@ class BrushFlow(_PluginBase): self.__get_hash(torrent) in proxy_delete_hashes) # 在完成初始删除步骤后,如果总体积仍然超过最小阈值,则进一步找到已完成种子并排除HR种子后按做种时间正序进行删除 - sites_names = set() if total_torrent_size > min_size: # 重新计算当前的种子列表,排除已删除的种子 remaining_hashes = list( @@ -2428,14 +2552,15 @@ class BrushFlow(_PluginBase): torrent_desc = torrent_task.get("description", "") seeding_time = torrent_task.get("seeding_time", 0) if seeding_time: - sites_names.add(site_name) reason = f"触发动态删除,做种时间 {seeding_time / 3600:.1f} 小时,系统自动删除" self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, reason=reason) logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}") - msg = (f"站点:{','.join(sites_names)}\n内容:已完成 {len(need_delete_hashes)} 个种子删除," + delete_sites = {torrent_tasks[hash_key].get('site_name', '') for hash_key in need_delete_hashes if + hash_key in torrent_tasks} + msg = (f"站点:{','.join(delete_sites)}\n内容:已完成 {len(need_delete_hashes)} 个种子删除," f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB\n原因:触发动态删除") logger.info(msg) self.__send_message(title="【刷流任务状态更新】", text=msg) @@ -2817,7 +2942,7 @@ class BrushFlow(_PluginBase): if response and response.ok: torrent_content = response.content else: - logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载') + logger.error('尝试通过MP下载种子失败,继续尝试传递种子地址到下载器进行下载') if torrent_content: state = self.qb.add_torrent(content=torrent_content, download_dir=download_dir, @@ -2832,7 +2957,7 @@ class BrushFlow(_PluginBase): torrent_hash = self.qb.get_torrent_id_by_tag(tags=tag) if not torrent_hash: logger.error(f"{brush_config.downloader} 获取种子Hash失败" - f"{',请尝试开启代理下载种子' if not brush_config.proxy_download else ''}") + f"{',请尝试启用「代理下载种子」配置项' if not brush_config.proxy_download else ''}") return None return torrent_hash return None @@ -2848,7 +2973,7 @@ class BrushFlow(_PluginBase): if response and response.ok: torrent_content = response.content else: - logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载') + logger.error('尝试通过MP下载种子失败,继续尝试传递种子地址到下载器进行下载') if torrent_content: torrent = self.tr.add_torrent(content=torrent_content, download_dir=download_dir, @@ -3364,9 +3489,9 @@ class BrushFlow(_PluginBase): @staticmethod def __is_number_or_range(value): """ - 检查字符串是否表示单个数字或数字范围(如'5'或'5-10') + 检查字符串是否表示单个数字或数字范围(如'5', '5.5', '5-10' 或 '5.5-10.2') """ - return bool(re.match(r"^\d+(-\d+)?$", value)) + return bool(re.match(r"^\d+(\.\d+)?(-\d+(\.\d+)?)?$", value)) @staticmethod def __is_number(value): @@ -3442,47 +3567,6 @@ class BrushFlow(_PluginBase): } return statistic_info - @staticmethod - def __get_demo_site_config() -> str: - desc = ("以下为配置示例,请参考 " - "https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md " - "进行配置,请注意,只需要保留实际配置内容(删除这段)\n") - config = """[{ - "sitename": "站点1", - "seed_time": 96, - "hr_seed_time": 144 - }, { - "sitename": "站点2", - "hr": "yes", - "size": "10-500", - "seeder": "5-10", - "pubtime": "5-120", - "seed_time": 96, - "save_path": "/downloads/site2", - "proxy_download": true, - "hr_seed_time": 144 - }, { - "sitename": "站点3", - "freeleech": "free", - "hr": "yes", - "include": "", - "exclude": "", - "size": "10-500", - "seeder": "1", - "pubtime": "5-120", - "seed_time": 120, - "hr_seed_time": 144, - "seed_ratio": "", - "seed_size": "", - "download_time": "", - "seed_avgspeed": "", - "seed_inactivetime": "", - "save_path": "/downloads/site1", - "proxy_download": false, - "proxy_delete": false, - }]""" - return desc + config - @staticmethod def __is_valid_time_range(time_range: str) -> bool: """检查时间范围字符串是否有效:格式为"HH:MM-HH:MM",且时间有效"""