fix brushflow 动态删除种子规则调整,站点独立配置样式优化、日志优化等

This commit is contained in:
InfinityPacer
2024-04-13 00:16:40 +08:00
parent 6834b4bb90
commit 0c24b1fcf8
2 changed files with 170 additions and 85 deletions

View File

@@ -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做种时间、下载器监控配置项刷流前置条件逻辑调整代理下载种子默认为关闭"
}

View File

@@ -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",且时间有效"""