diff --git a/plugins/brushflow/__init__.py b/plugins/brushflow/__init__.py
index d0227fb..d3dc620 100644
--- a/plugins/brushflow/__init__.py
+++ b/plugins/brushflow/__init__.py
@@ -1,11 +1,12 @@
-import base64
-import json
import re
import threading
import time
+import random
+import json
+import base64
from datetime import datetime, timedelta
from threading import Event
-from typing import Any, List, Dict, Tuple, Optional, Union
+from typing import Any, List, Dict, Tuple, Optional, Union, Set
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
@@ -14,6 +15,7 @@ from app import schemas
from app.chain.torrents import TorrentsChain
from app.core.config import settings
from app.db.site_oper import SiteOper
+from app.db.subscribe_oper import SubscribeOper
from app.helper.sites import SitesHelper
from app.log import logger
from app.modules.qbittorrent import Qbittorrent
@@ -25,8 +27,151 @@ from app.utils.string import StringUtils
lock = threading.Lock()
+class BrushConfig:
+ """
+ 刷流配置
+ """
+ def __init__(self, config: dict, process_site_config=True):
+ self.enabled = config.get("enabled", False)
+ self.notify = config.get("notify", True)
+ self.onlyonce = config.get("onlyonce", False)
+ self.brushsites = config.get("brushsites", [])
+ self.downloader = config.get("downloader", "qbittorrent")
+ self.disksize = self.__parse_number(config.get("disksize"))
+ self.freeleech = config.get("freeleech", "free")
+ self.hr = config.get("hr", "no")
+ self.maxupspeed = self.__parse_number(config.get("maxupspeed"))
+ self.maxdlspeed = self.__parse_number(config.get("maxdlspeed"))
+ self.maxdlcount = self.__parse_number(config.get("maxdlcount"))
+ self.include = config.get("include")
+ self.exclude = config.get("exclude")
+ self.size = config.get("size")
+ self.seeder = config.get("seeder")
+ self.pubtime = config.get("pubtime")
+ self.seed_time = self.__parse_number(config.get("seed_time"))
+ self.seed_ratio = self.__parse_number(config.get("seed_ratio"))
+ self.seed_size = self.__parse_number(config.get("seed_size"))
+ self.download_time = self.__parse_number(config.get("download_time"))
+ self.seed_avgspeed = self.__parse_number(config.get("seed_avgspeed"))
+ self.seed_inactivetime = self.__parse_number(config.get("seed_inactivetime"))
+ self.delete_size_range = config.get("delete_size_range")
+ self.up_speed = self.__parse_number(config.get("up_speed"))
+ self.dl_speed = self.__parse_number(config.get("dl_speed"))
+ self.save_path = config.get("save_path")
+ self.clear_task = config.get("clear_task", False)
+ self.archive_task = config.get("archive_task", False)
+ self.except_tags = config.get("except_tags", True)
+ self.except_subscribe = config.get("except_subscribe", True)
+ self.brush_sequential = config.get("brush_sequential", False)
+ self.proxy_download = config.get("proxy_download", True)
+ self.proxy_delete = config.get("proxy_delete", False)
+ self.active_time_range = config.get("active_time_range")
+ self.enable_site_config = config.get("enable_site_config", False)
+ self.brush_tag = "刷流"
+ self.site_config = config.get("site_config", "[]")
+ self.group_site_configs = {}
+
+ if self.enable_site_config and process_site_config:
+ self.__initialize_site_config()
+
+ def __initialize_site_config(self):
+ if not self.site_config:
+ self.group_site_configs = {}
+
+ # 定义允许覆盖的字段列表
+ allowed_fields = {
+ "freeleech",
+ "hr",
+ "include",
+ "exclude",
+ "size",
+ "seeder",
+ "pubtime",
+ "seed_time",
+ "seed_ratio",
+ "seed_size",
+ "download_time",
+ "seed_avgspeed",
+ "seed_inactivetime",
+ "save_path",
+ "proxy_download",
+ "proxy_delete"
+ # 当新增支持字段时,仅在此处添加字段名
+ }
+ try:
+ site_configs = json.loads(self.site_config)
+ self.group_site_configs = {}
+ for config in site_configs:
+ sitename = config.get("sitename")
+ if not sitename:
+ continue
+
+ # 只从站点特定配置中获取允许的字段
+ site_specific_config = {key: config[key] for key in allowed_fields & set(config.keys())}
+
+ full_config = {key: getattr(self, key) for key in vars(self) if key not in ['group_site_configs', 'site_config']}
+ full_config.update(site_specific_config)
+
+ self.group_site_configs[sitename] = BrushConfig(config=full_config, process_site_config=False)
+ except Exception as e:
+ logger.error(f"解析站点配置失败,请检查配置项。错误详情: {e}")
+ self.group_site_configs = {}
+
+ def get_site_config(self, sitename):
+ """
+ 根据站点名称获取特定的BrushConfig实例。如果没有找到站点特定的配置,则返回全局的BrushConfig实例。
+ """
+ if not self.enable_site_config:
+ return self
+ return self if not sitename else self.group_site_configs.get(sitename, self)
+
+ @staticmethod
+ def __parse_number(value):
+ if value is None or value == '': # 更精确地检查None或空字符串
+ return value
+ elif isinstance(value, int): # 直接判断是否为int
+ return value
+ elif isinstance(value, float): # 直接判断是否为float
+ return value
+ else:
+ try:
+ number = float(value)
+ # 检查number是否等于其整数形式
+ if number == int(number):
+ return int(number)
+ else:
+ return number
+ except (ValueError, TypeError):
+ return 0
+
+ def __format_value(self, v):
+ """
+ Format the value to mimic JSON serialization. This is now an instance method.
+ """
+ if isinstance(v, str):
+ return f'"{v}"'
+ elif isinstance(v, (int, float, bool)):
+ return str(v).lower() if isinstance(v, bool) else str(v)
+ elif isinstance(v, list):
+ return '[' + ', '.join(self.__format_value(i) for i in v) + ']'
+ elif isinstance(v, dict):
+ return '{' + ', '.join(f'"{k}": {self.__format_value(val)}' for k, val in v.items()) + '}'
+ else:
+ return str(v)
+
+ def __str__(self):
+ attrs = vars(self)
+ # Note the use of self.format_value(v) here to call the instance method
+ attrs_str = ', '.join(f'"{k}": {self.__format_value(v)}' for k, v in attrs.items())
+ return f'{{ {attrs_str} }}'
+
+ def __repr__(self):
+ return self.__str__()
class BrushFlow(_PluginBase):
+
+ # region 全局定义
+
# 插件名称
plugin_name = "站点刷流"
# 插件描述
@@ -36,7 +181,7 @@ class BrushFlow(_PluginBase):
# 插件版本
plugin_version = "1.6"
# 插件作者
- plugin_author = "jxxghp"
+ plugin_author = "jxxghp,InfinityPacer"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
@@ -50,219 +195,122 @@ class BrushFlow(_PluginBase):
siteshelper = None
siteoper = None
torrents = None
- sites = None
qb = None
tr = None
- # 添加种子定时 分钟
- _cron = 10
- _task_enable = False
- # 检查种子定时
+ # 刷流配置
+ _brush_config = None
+ # Brush任务是否启动
+ _task_brush_enable = False
+ # Brush定时
+ _brush_interval = 10
+ # Check定时
_check_interval = 5
# 退出事件
_event = Event()
_scheduler = None
- _enabled = False
- _notify = True
- _onlyonce = False
- _brushsites = []
- _downloader = "qbittorrent"
- _disksize = 0
- _freeleech = "free"
- _hr = "no"
- _maxupspeed = 0
- _maxdlspeed = 0
- _maxdlcount = 0
- _include = ""
- _exclude = ""
- _size = 0
- _seeder = 0
- _pubtime = 0
- _seed_time = 0
- _seed_ratio = 0
- _seed_size = 0
- _download_time = 0
- _seed_avgspeed = 0
- _seed_inactivetime = 0
- _up_speed = 0
- _dl_speed = 0
- _save_path = ""
- _clear_task = False
- _offline_mode = True
+
+ # endregion
- def init_plugin(self, config: dict = None):
+ def init_plugin(self, config: dict = None):
+ logger.info(f"站点刷流服务初始化")
self.siteshelper = SitesHelper()
self.siteoper = SiteOper()
self.torrents = TorrentsChain()
- self.sites = SitesHelper()
- if config:
- self._enabled = config.get("enabled")
- self._notify = config.get("notify")
- self._onlyonce = config.get("onlyonce")
- self._brushsites = config.get("brushsites")
- self._downloader = config.get("downloader")
- self._disksize = config.get("disksize")
- self._freeleech = config.get("freeleech")
- self._hr = config.get("hr")
- self._maxupspeed = config.get("maxupspeed")
- self._maxdlspeed = config.get("maxdlspeed")
- self._maxdlcount = config.get("maxdlcount")
- self._include = config.get("include")
- self._exclude = config.get("exclude")
- self._size = config.get("size")
- self._seeder = config.get("seeder")
- self._pubtime = config.get("pubtime")
- self._seed_time = config.get("seed_time")
- self._seed_ratio = config.get("seed_ratio")
- self._seed_size = config.get("seed_size")
- self._download_time = config.get("download_time")
- self._seed_avgspeed = config.get("seed_avgspeed")
- self._seed_inactivetime = config.get("seed_inactivetime")
- self._up_speed = config.get("up_speed")
- self._dl_speed = config.get("dl_speed")
- self._save_path = config.get("save_path")
- self._clear_task = config.get("clear_task")
- self._offline_mode = config.get("offline_mode")
+ self.subscribeoper = SubscribeOper()
+ self._task_brush_enable = False
+
+ if not config:
+ logger.info("站点刷流任务出错,无法获取插件配置")
+ return False
- # 过滤掉已删除的站点
- self._brushsites = [site.get("id") for site in self.sites.get_indexers() if
- not site.get("public") and site.get("id") in self._brushsites]
+ # 如果没有站点配置时,增加默认的配置项
+ 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)
+ self._brush_config.enabled = False
+ self.__update_config()
+ return
+
+ self._brush_config = BrushConfig(config=config)
- # 保存配置
+ 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]
+ ]
+
+ self.__update_config()
+
+ if brush_config.clear_task:
+ self.__clear_tasks()
+ brush_config.clear_task = False
+ brush_config.archive_task = False
+ self.__update_config()
+
+ elif brush_config.archive_task:
+ self.__archive_tasks()
+ brush_config.archive_task = False
self.__update_config()
- if self._clear_task:
- # 清除统计数据
- self.save_data("statistic", {})
- # 清除种子记录
- self.save_data("torrents", {})
- # 关闭一次性开关
- self._clear_task = False
- self.__update_config()
+ # 停止现有任务
+ self.stop_service()
+
+ if not self.__setup_downloader():
+ return
- # 停止现有任务
- self.stop_service()
+ # 如果下载器都没有配置,那么这里也不需要继续
+ if not brush_config.downloader:
+ brush_config.enabled = False
+ self.__update_config()
+ logger.info(f"站点刷流服务停止,没有配置下载器")
+ return
- # 启动定时任务 & 立即运行一次
- self._task_enable = False
- if self.get_state() or self._onlyonce:
- self.qb = Qbittorrent()
- self.tr = Transmission()
- # 检查配置
- if self._downloader == "qbittorrent":
- if self.qb.is_inactive():
- logger.error("站点刷流任务出错:Qbittorrent未连接")
- self.systemmessage.put("站点刷流任务出错:Qbittorrent未连接")
- return
- elif self._downloader == "transmission":
- if self.tr.is_inactive():
- logger.error("站点刷流任务出错:Transmission未连接")
- self.systemmessage.put("站点刷流任务出错:Transmission未连接")
- return
- if self._disksize and not StringUtils.is_number(self._disksize):
- logger.error(f"站点刷流任务出错,保种体积设置错误:{self._disksize}")
- self.systemmessage.put(f"站点刷流任务出错,保种体积设置错误:{self._disksize}")
- self._disksize = 0
- return
- if self._maxupspeed and not StringUtils.is_number(self._maxupspeed):
- logger.error(f"站点刷流任务出错,总上传带宽设置错误:{self._maxupspeed}")
- self.systemmessage.put(f"站点刷流任务出错,总上传带宽设置错误:{self._maxupspeed}")
- self._maxupspeed = 0
- return
- if self._maxdlspeed and not StringUtils.is_number(self._maxdlspeed):
- logger.error(f"站点刷流任务出错,总下载带宽设置错误:{self._maxdlspeed}")
- self.systemmessage.put(f"站点刷流任务出错,总下载带宽设置错误:{self._maxdlspeed}")
- self._maxdlspeed = 0
- return
- if self._maxdlcount and not StringUtils.is_number(self._maxdlcount):
- logger.error(f"站点刷流任务出错,同时下载任务数设置错误:{self._maxdlcount}")
- self.systemmessage.put(f"站点刷流任务出错,同时下载任务数设置错误:{self._maxdlcount}")
- self._maxdlcount = 0
- return
- if self._size:
- size = str(self._size).split("-")[0]
- if not StringUtils.is_number(size):
- logger.error(f"站点刷流任务出错,种子大小设置错误:{self._size}")
- self.systemmessage.put(f"站点刷流任务出错,种子大小设置错误:{self._size}")
- self._size = 0
- return
- if self._seeder:
- seeder = str(self._seeder).split("-")[0]
- if not StringUtils.is_number(seeder):
- logger.error(f"站点刷流任务出错,做种人数设置错误:{self._seeder}")
- self.systemmessage.put(f"站点刷流任务出错,做种人数设置错误:{self._seeder}")
- self._seeder = 0
- return
- if self._seed_time and not StringUtils.is_number(self._seed_time):
- logger.error(f"站点刷流任务出错,做种时间设置错误:{self._seed_time}")
- self.systemmessage.put(f"站点刷流任务出错,做种时间设置错误:{self._seed_time}")
- self._seed_time = 0
- return
- if self._seed_ratio and not StringUtils.is_number(self._seed_ratio):
- logger.error(f"站点刷流任务出错,分享率设置错误:{self._seed_ratio}")
- self.systemmessage.put(f"站点刷流任务出错,分享率设置错误:{self._seed_ratio}")
- self._seed_ratio = 0
- return
- if self._seed_size and not StringUtils.is_number(self._seed_size):
- logger.error(f"站点刷流任务出错,上传量设置错误:{self._seed_size}")
- self.systemmessage.put(f"站点刷流任务出错,上传量设置错误:{self._seed_size}")
- self._seed_size = 0
- return
- if self._download_time and not StringUtils.is_number(self._download_time):
- logger.error(f"站点刷流任务出错,下载超时时间设置错误:{self._download_time}")
- self.systemmessage.put(f"站点刷流任务出错,下载超时时间设置错误:{self._download_time}")
- self._download_time = 0
- return
- if self._seed_avgspeed and not StringUtils.is_number(self._seed_avgspeed):
- logger.error(f"站点刷流任务出错,平均上传速度设置错误:{self._seed_avgspeed}")
- self.systemmessage.put(f"站点刷流任务出错,平均上传速度设置错误:{self._seed_avgspeed}")
- self._seed_avgspeed = 0
- return
- if self._seed_inactivetime and not StringUtils.is_number(self._seed_inactivetime):
- logger.error(f"站点刷流任务出错,未活动时间设置错误:{self._seed_inactivetime}")
- self.systemmessage.put(f"站点刷流任务出错,未活动时间设置错误:{self._seed_inactivetime}")
- self._seed_inactivetime = 0
- return
- if self._up_speed and not StringUtils.is_number(self._up_speed):
- logger.error(f"站点刷流任务出错,单任务上传限速设置错误:{self._up_speed}")
- self.systemmessage.put(f"站点刷流任务出错,单任务上传限速设置错误:{self._up_speed}")
- self._up_speed = 0
- return
- if self._dl_speed and not StringUtils.is_number(self._dl_speed):
- logger.error(f"站点刷流任务出错,单任务下载限速设置错误:{self._dl_speed}")
- self.systemmessage.put(f"站点刷流任务出错,单任务下载限速设置错误:{self._dl_speed}")
- self._dl_speed = 0
- return
+ # 如果站点都没有配置,则不开启定时刷流服务
+ if not brush_config.brushsites:
+ logger.info(f"站点刷流Brush定时服务停止,没有配置站点")
- # 检查必要条件
- if not self._brushsites or not self._downloader:
- return
+ # 如果开启&存在站点时,才需要启用后台任务
+ self._task_brush_enable = brush_config.enabled and brush_config.brushsites
+
+ # brush_config.onlyonce = True
- # 增加任务
- self._task_enable = True
- self._scheduler = BackgroundScheduler(timezone=settings.TZ)
- self._scheduler.add_job(self.check, 'interval',
- minutes=self._check_interval,
- name="站点刷流检查服务")
-
- # 仅一次
- if self._onlyonce:
- logger.info(f"站点刷流服务启动,立即运行一次")
- self._scheduler.add_job(self.brush, 'date',
- run_date=datetime.now(
- tz=pytz.timezone(settings.TZ)
- ) + timedelta(seconds=3),
- name="站点刷流服务")
- # 关闭一次性开关
- self._onlyonce = False
- self.__update_config()
+ # 检查是否启用了一次性任务
+ if brush_config.onlyonce:
+ self._scheduler = BackgroundScheduler(timezone=settings.TZ)
+
+ logger.info(f"站点刷流Brush服务启动,立即运行一次")
+ self._scheduler.add_job(self.brush, 'date',
+ run_date=datetime.now(
+ tz=pytz.timezone(settings.TZ)
+ ) + timedelta(seconds=3),
+ name="站点刷流Brush服务")
+
+ logger.info(f"站点刷流Check服务启动,立即运行一次")
+ self._scheduler.add_job(self.check, 'date',
+ run_date=datetime.now(
+ tz=pytz.timezone(settings.TZ)
+ ) + timedelta(seconds=3),
+ name="站点刷流Check服务")
+
+ # 关闭一次性开关
+ brush_config.onlyonce = False
+ self.__update_config()
+ # 存在任务则启动任务
+ if self._scheduler.get_jobs():
# 启动服务
- if self._scheduler.get_jobs():
- self._scheduler.print_jobs()
- self._scheduler.start()
-
+ self._scheduler.print_jobs()
+ self._scheduler.start()
+
def get_state(self) -> bool:
- return True if self._enabled and self._brushsites and self._downloader else False
+ brush_config = self.__get_brush_config()
+ return True if brush_config and brush_config.enabled else False
@staticmethod
def get_command() -> List[Dict[str, Any]]:
@@ -282,20 +330,42 @@ class BrushFlow(_PluginBase):
"kwargs": {} # 定时器参数
}]
"""
- if self._task_enable:
- return [{
+ services = []
+
+ brush_config = self.__get_brush_config()
+ if not brush_config:
+ return services
+
+ if self._task_brush_enable:
+ logger.info(f"站点刷流Brush定时服务启动,时间间隔 {self._brush_interval} 分钟")
+ services.append({
"id": "BrushFlow",
- "name": "站点刷流服务",
+ "name": "站点刷流Brush服务",
"trigger": "interval",
"func": self.brush,
- "kwargs": {"minutes": self._cron}
- }]
- return []
+ "kwargs": {"minutes": self._brush_interval}
+ })
+
+ if brush_config.enabled:
+ logger.info(f"站点刷流Check定时服务启动,时间间隔 {self._check_interval} 分钟")
+ services.append({
+ "id": "BrushFlowCheck",
+ "name": "站点刷流Check服务",
+ "trigger": "interval",
+ "func": self.check,
+ "kwargs": {"minutes": self._check_interval}
+ })
+
+ if not services:
+ logger.info("站点刷流服务未开启")
+
+ return services
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
"""
+
# 站点的可选项
site_options = [{"title": site.get("name"), "value": site.get("id")}
for site in self.siteshelper.get_indexers()]
@@ -353,22 +423,6 @@ class BrushFlow(_PluginBase):
}
}
]
- },
- {
- 'component': 'VCol',
- 'props': {
- 'cols': 12,
- 'md': 4
- },
- 'content': [
- {
- 'component': 'VSwitch',
- 'props': {
- 'model': 'offline_mode',
- 'label': '离线下载种子',
- }
- }
- ]
}
]
},
@@ -377,6 +431,9 @@ class BrushFlow(_PluginBase):
'content': [
{
'component': 'VCol',
+ 'props': {
+ "cols": 12
+ },
'content': [
{
'component': 'VSelect',
@@ -427,7 +484,7 @@ class BrushFlow(_PluginBase):
'props': {
'model': 'disksize',
'label': '保种体积(GB)',
- 'placeholder': '达到后停止新增任务'
+ 'placeholder': '如:500,达到后停止新增任务'
}
}
]
@@ -631,6 +688,23 @@ class BrushFlow(_PluginBase):
}
]
},
+ {
+ 'component': 'VCol',
+ 'props': {
+ "cols": 12,
+ "md": 4
+ },
+ 'content': [
+ {
+ 'component': 'VTextField',
+ 'props': {
+ 'model': 'delete_size_range',
+ 'label': '动态删种阈值(GB)',
+ 'placeholder': '如:500 或 500-1000,达到后删除任务'
+ }
+ }
+ ]
+ },
{
'component': 'VCol',
'props': {
@@ -715,12 +789,7 @@ class BrushFlow(_PluginBase):
}
}
]
- }
- ]
- },
- {
- 'component': 'VRow',
- 'content': [
+ },
{
'component': 'VCol',
'props': {
@@ -771,6 +840,82 @@ class BrushFlow(_PluginBase):
}
}
]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ "cols": 12,
+ "md": 4
+ },
+ 'content': [
+ {
+ 'component': 'VTextField',
+ 'props': {
+ 'model': 'active_time_range',
+ 'label': '开启时间段',
+ 'placeholder': '如:00:00-08:00'
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'component': 'VRow',
+ 'content': [
+
+ ]
+ },
+ {
+ 'component': 'VRow',
+ 'content': [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'brush_sequential',
+ 'label': '站点顺序刷流',
+ }
+ }
+ ]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'except_tags',
+ 'label': '删种排除MoviePilot任务',
+ }
+ }
+ ]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'except_subscribe',
+ 'label': '排除订阅(实验性功能)',
+ }
+ }
+ ]
}
]
},
@@ -792,6 +937,113 @@ class BrushFlow(_PluginBase):
}
}
]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'archive_task',
+ 'label': '归档已删除种子',
+ }
+ }
+ ]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'proxy_delete',
+ 'label': '动态删除种子(实验性功能)',
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'component': 'VRow',
+ "content": [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'enable_site_config',
+ 'label': '站点独立配置',
+ }
+ }
+ ]
+ },
+ {
+ "component": "VCol",
+ "props": {
+ "cols": 12,
+ "md": 4,
+ # "class": "d-flex justify-end"
+ },
+ "content": [
+ {
+ "component": "VSwitch",
+ "props": {
+ "model": "dialog_closed",
+ "label": "设置站点"
+ }
+ }
+ ]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'proxy_download',
+ 'label': '代理下载种子',
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'component': 'VRow',
+ 'content': [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ },
+ 'content': [
+ {
+ 'component': 'VAlert',
+ 'props': {
+ 'type': 'success',
+ 'variant': 'tonal'
+ },
+ 'html': "
"
+ }
+ ]
}
]
},
@@ -815,6 +1067,75 @@ class BrushFlow(_PluginBase):
]
}
]
+ },
+ {
+ "component": "VDialog",
+ "props": {
+ "model": "dialog_closed",
+ "max-width": "80rem",
+ "overlay-class": "v-dialog--scrollable v-overlay--scroll-blocked",
+ "content-class": "v-card v-card--density-default v-card--variant-elevated rounded-t"
+ },
+ "content": [
+ {
+ "component": "VCard",
+ "content": [
+ {
+ "component": "VCardItem",
+ "text": "设置站点配置"
+ },
+ {
+ "component": "VCardText",
+ "props": {},
+ "content": [
+ {
+ 'component': 'VRow',
+ 'content': [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ },
+ 'content': [
+ {
+ "component": "VTextarea",
+ "props": {
+ "model": "site_config",
+ "placeholder": "请输入站点配置",
+ "label": "站点配置",
+ "rows": 16
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'component': 'VRow',
+ 'content': [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ },
+ 'content': [
+ {
+ 'component': 'VAlert',
+ 'props': {
+ 'type': 'info',
+ 'variant': 'tonal'
+ },
+ 'html': " "
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
}
@@ -822,22 +1143,24 @@ class BrushFlow(_PluginBase):
"enabled": False,
"notify": True,
"onlyonce": False,
- "offline_mode": True,
"clear_task": False,
+ "archive_task": False,
+ "except_tags": True,
+ "except_subscribe": True,
+ "brush_sequential": False,
+ "proxy_download": True,
+ "proxy_delete": False,
"freeleech": "free",
"hr": "yes",
+ "enable_site_config": False
}
def get_page(self) -> List[dict]:
# 种子明细
torrents = self.get_data("torrents") or {}
# 统计数据
- stattistic_data: Dict[str, dict] = self.get_data("statistic") or {
- "count": 0,
- "deleted": 0,
- "uploaded": 0,
- "downloaded": 0,
- }
+ statistic_info = self.__get_statistic_info()
+
if not torrents:
return [
{
@@ -852,40 +1175,76 @@ class BrushFlow(_PluginBase):
data_list = torrents.values()
# 按time倒序排序
data_list = sorted(data_list, key=lambda x: x.get("time") or 0, reverse=True)
- # 总上传量格式化
- total_upload = StringUtils.str_filesize(stattistic_data.get("uploaded") or 0)
- # 总下载量格式化
- total_download = StringUtils.str_filesize(stattistic_data.get("downloaded") or 0)
+ # 总上传量
+ total_uploaded = StringUtils.str_filesize(statistic_info.get("uploaded") or 0)
+ # 总下载量
+ total_downloaded = StringUtils.str_filesize(statistic_info.get("downloaded") or 0)
# 下载种子数
- total_count = stattistic_data.get("count") or 0
+ total_count = statistic_info.get("count") or 0
# 删除种子数
- total_deleted = stattistic_data.get("deleted") or 0
- # 表格标题
- headers = [
- {'title': '站点', 'key': 'site', 'sortable': True},
- {'title': '标题', 'key': 'title', 'sortable': True},
- {'title': '大小', 'key': 'size', 'sortable': True},
- {'title': '上传量', 'key': 'uploaded', 'sortable': True},
- {'title': '下载量', 'key': 'downloaded', 'sortable': True},
- {'title': '分享率', 'key': 'ratio', 'sortable': True},
- {'title': '状态', 'key': 'status', 'sortable': True},
- ]
+ total_deleted = statistic_info.get("deleted") or 0
+ # 待归档种子数
+ total_unarchived = statistic_info.get("unarchived") or 0
+ # 活跃种子数
+ total_active = statistic_info.get("active") or 0
+ # 活跃上传量
+ total_active_uploaded = StringUtils.str_filesize(statistic_info.get("active_uploaded") or 0)
+ # 活跃下载量
+ total_active_downloaded = StringUtils.str_filesize(statistic_info.get("active_downloaded") or 0)
# 种子数据明细
- items = [
+ torrent_trs = [
{
- 'site': data.get("site_name"),
- 'title': data.get("title"),
- 'size': StringUtils.str_filesize(data.get("size")),
- 'uploaded': StringUtils.str_filesize(data.get("uploaded") or 0),
- 'downloaded': StringUtils.str_filesize(data.get("downloaded") or 0),
- 'ratio': round(data.get('ratio') or 0, 2),
- 'status': "已删除" if data.get("deleted") else "正常"
+ 'component': 'tr',
+ 'props': {
+ 'class': 'text-sm'
+ },
+ 'content': [
+ {
+ 'component': 'td',
+ 'props': {
+ 'class': 'whitespace-nowrap break-keep text-high-emphasis'
+ },
+ 'text': data.get("site_name")
+ },
+ {
+ 'component': 'td',
+ 'html': f'{data.get("title")}' +
+ (f'
{data.get("description")}' if data.get("description") else "")
+
+ },
+ {
+ 'component': 'td',
+ 'text': StringUtils.str_filesize(data.get("size"))
+ },
+ {
+ 'component': 'td',
+ 'text': StringUtils.str_filesize(data.get("uploaded") or 0)
+ },
+ {
+ 'component': 'td',
+ 'text': StringUtils.str_filesize(data.get("downloaded") or 0)
+ },
+ {
+ 'component': 'td',
+ 'text': round(data.get('ratio') or 0, 2)
+ },
+ {
+ 'component': 'td',
+ 'text': "是" if data.get("hit_and_run") else "否"
+ },
+ {
+ 'component': 'td',
+ 'props': {
+ 'class': 'text-no-wrap'
+ },
+ 'text': "已删除" if data.get("deleted") else "正常"
+ }
+ ]
} for data in data_list
]
-
+
# 拼装页面
return [
- # 统计数据
{
'component': 'VRow',
'content': [
@@ -934,7 +1293,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-caption'
},
- 'text': '总上传量'
+ 'text': '总上传量(活跃)'
},
{
'component': 'div',
@@ -947,7 +1306,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-h6'
},
- 'text': total_upload
+ 'text': f"{total_uploaded}({total_active_uploaded})"
}
]
}
@@ -1004,7 +1363,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-caption'
},
- 'text': '总下载量'
+ 'text': '总下载量(活跃)'
},
{
'component': 'div',
@@ -1017,7 +1376,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-h6'
},
- 'text': total_download
+ 'text': f"{total_downloaded}({total_active_downloaded})"
}
]
}
@@ -1074,7 +1433,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-caption'
},
- 'text': '下载种子数'
+ 'text': '下载种子数(活跃)'
},
{
'component': 'div',
@@ -1087,7 +1446,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-h6'
},
- 'text': total_count
+ 'text': f"{total_count}({total_active})"
}
]
}
@@ -1144,7 +1503,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-caption'
},
- 'text': '删除种子数'
+ 'text': '删除种子数(待归档)'
},
{
'component': 'div',
@@ -1157,7 +1516,7 @@ class BrushFlow(_PluginBase):
'props': {
'class': 'text-h6'
},
- 'text': total_deleted
+ 'text': f"{total_deleted}({total_unarchived})"
}
]
}
@@ -1168,16 +1527,8 @@ class BrushFlow(_PluginBase):
]
}
]
- }
- ]
- },
- # 种子明细
- {
- 'component': 'VRow',
- 'props': {
- 'class': 'd-none d-sm-block',
- },
- 'content': [
+ },
+ # 种子明细
{
'component': 'VCol',
'props': {
@@ -1185,19 +1536,80 @@ class BrushFlow(_PluginBase):
},
'content': [
{
- 'component': 'VDataTableVirtual',
+ 'component': 'VTable',
'props': {
- 'class': 'text-sm',
- 'hover': True,
- 'headers': headers,
- 'items': items,
- 'height': '500',
- 'item-value': 'title',
- 'density': 'compact',
- 'fixed-header': True,
- 'hide-no-data': True,
- 'sticky': True,
- }
+ 'hover': True
+ },
+ 'content': [
+ {
+ 'component': 'thead',
+ 'props': {
+ 'class': 'text-no-wrap'
+ },
+ 'content': [
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '站点'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '标题'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '大小'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '上传量'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '下载量'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '分享率'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': 'HR'
+ },
+ {
+ 'component': 'th',
+ 'props': {
+ 'class': 'text-start ps-4'
+ },
+ 'text': '状态'
+ }
+ ]
+ },
+ {
+ 'component': 'tbody',
+ 'content': torrent_trs
+ }
+ ]
}
]
}
@@ -1220,354 +1632,810 @@ class BrushFlow(_PluginBase):
except Exception as e:
print(str(e))
- def __update_config(self):
- """
- 更新配置
- """
- self.update_config({
- "onlyonce": self._onlyonce,
- "enabled": self._enabled,
- "notify": self._notify,
- "brushsites": self._brushsites,
- "downloader": self._downloader,
- "disksize": self._disksize,
- "freeleech": self._freeleech,
- "hr": self._hr,
- "maxupspeed": self._maxupspeed,
- "maxdlspeed": self._maxdlspeed,
- "maxdlcount": self._maxdlcount,
- "include": self._include,
- "exclude": self._exclude,
- "size": self._size,
- "seeder": self._seeder,
- "pubtime": self._pubtime,
- "seed_time": self._seed_time,
- "seed_ratio": self._seed_ratio,
- "seed_size": self._seed_size,
- "download_time": self._download_time,
- "seed_avgspeed": self._seed_avgspeed,
- "seed_inactivetime": self._seed_inactivetime,
- "up_speed": self._up_speed,
- "dl_speed": self._dl_speed,
- "save_path": self._save_path,
- "clear_task": self._clear_task,
- "offline_mode": self._offline_mode
- })
+ #region Brush
def brush(self):
"""
- 执行刷流动作,添加下载任务
+ 定时刷流,添加下载任务
"""
- if not self._brushsites or not self._downloader:
+ brush_config = self.__get_brush_config()
+
+ if not brush_config.brushsites or not brush_config.downloader:
return
-
+
+ if not self.__is_current_time_in_range():
+ logger.info(f"当前不在指定的刷流时间区间内,刷流操作将暂时暂停")
+ return
+
with lock:
logger.info(f"开始执行刷流任务 ...")
- # 读取种子记录
- task_info: Dict[str, dict] = self.get_data("torrents") or {}
- if task_info:
- # 当前保种大小
- torrents_size = sum([
- task.get("size") or 0
- for task in task_info.values() if not task.get("deleted")
- ])
- else:
- torrents_size = 0
- # 读取统计数据
- statistic_info = self.get_data("statistic") or {
- "count": 0,
- "deleted": 0,
- }
- # 处理所有站点
- for siteid in self._brushsites:
- siteinfo = self.siteoper.get(siteid)
- if not siteinfo:
- logger.warn(f"站点不存在:{siteid}")
- continue
- logger.info(f"开始获取站点 {siteinfo.name} 的新种子 ...")
- torrents = self.torrents.browse(domain=siteinfo.domain)
- if not torrents:
- logger.info(f"站点 {siteinfo.name} 没有获取到种子")
- continue
- # 按pubdate降序排列
- torrents.sort(key=lambda x: x.pubdate or '', reverse=True)
- # 过滤种子
- for torrent in torrents:
- # 控重
- if f"{torrent.site_name}{torrent.title}" in [
- f"{task.get('site_name')}{task.get('title')}" for task in task_info.values()
- ]:
- continue
- # 促销
- if self._freeleech and torrent.downloadvolumefactor != 0:
- continue
- if self._freeleech == "2xfree" and torrent.uploadvolumefactor != 2:
- continue
- # H&R
- if self._hr == "yes" and torrent.hit_and_run:
- continue
- # 包含规则
- if self._include and not re.search(r"%s" % self._include, torrent.title, re.I):
- continue
- # 排除规则
- if self._exclude and re.search(r"%s" % self._exclude, torrent.title, re.I):
- continue
- # 种子大小(GB)
- if self._size:
- sizes = str(self._size).split("-")
- begin_size = sizes[0]
- if len(sizes) > 1:
- end_size = sizes[-1]
- else:
- end_size = 0
- if begin_size and not end_size \
- and torrent.size > float(begin_size) * 1024 ** 3:
- continue
- elif begin_size and end_size \
- and not float(begin_size) * 1024 ** 3 <= torrent.size <= float(end_size) * 1024 ** 3:
- continue
- # 做种人数
- if self._seeder:
- seeders = str(self._seeder).split("-")
- begin_seeder = seeders[0]
- if len(seeders) > 1:
- end_seeder = seeders[-1]
- else:
- end_seeder = 0
- if begin_seeder and not end_seeder \
- and torrent.seeders > int(begin_seeder):
- continue
- elif begin_seeder and end_seeder \
- and not int(begin_seeder) <= torrent.seeders <= int(end_seeder):
- continue
- # 计算发布时间,将字符串转换为时间
- pubdate_minutes = self.__get_pubminutes(torrent.pubdate)
- # 发布时间(分钟)
- if self._pubtime:
- pubtimes = str(self._pubtime).split("-")
- begin_pubtime = pubtimes[0]
- if len(pubtimes) > 1:
- end_pubtime = pubtimes[-1]
- else:
- end_pubtime = 0
- # 将种子发布日志转换为与当前时间的差
- if begin_pubtime and not end_pubtime \
- and pubdate_minutes > int(begin_pubtime):
- continue
- elif begin_pubtime and end_pubtime \
- and not int(begin_pubtime) <= pubdate_minutes <= int(end_pubtime):
- continue
- # 同时下载任务数
- downloads = self.__get_downloading_count()
- if self._maxdlcount and downloads >= int(self._maxdlcount):
- logger.warn(f"当前同时下载任务数 {downloads} 已达到最大值 {self._maxdlcount},停止新增任务")
- break
- # 获取下载器的下载信息
- downloader_info = self.__get_downloader_info()
- if downloader_info:
- current_upload_speed = downloader_info.upload_speed or 0
- current_download_speed = downloader_info.download_speed or 0
- # 总上传带宽(KB/s)
- if self._maxupspeed \
- and current_upload_speed >= float(self._maxupspeed) * 1024:
- logger.warn(f"当前总上传带宽 {StringUtils.str_filesize(current_upload_speed)} "
- f"已达到最大值 {self._maxupspeed} KB/s,暂时停止新增任务")
- break
- # 总下载带宽(KB/s)
- if self._maxdlspeed \
- and current_download_speed >= float(self._maxdlspeed) * 1024:
- logger.warn(f"当前总下载带宽 {StringUtils.str_filesize(current_download_speed)} "
- f"已达到最大值 {self._maxdlspeed} KB/s,暂时停止新增任务")
- break
- # 保种体积(GB)
- if self._disksize \
- and (torrents_size + torrent.size) > float(self._disksize) * 1024 ** 3:
- logger.warn(f"当前做种体积 {StringUtils.str_filesize(torrents_size)} "
- f"已超过保种体积 {self._disksize},停止新增任务")
- break
- # 添加下载任务
- hash_string = self.__download(torrent=torrent)
- if not hash_string:
- logger.warn(f"{torrent.title} 添加刷流任务失败!")
- continue
- # 保存任务信息
- task_info[hash_string] = {
- "site": siteinfo.id,
- "site_name": siteinfo.name,
- "title": torrent.title,
- "size": torrent.size,
- "pubdate": torrent.pubdate,
- "ratio": 0,
- "downloaded": 0,
- "uploaded": 0,
- "deleted": False,
- "time": time.time()
- }
- # 统计数据
- torrents_size += torrent.size
- statistic_info["count"] += 1
- # 发送消息
- self.__send_add_message(torrent)
+
+ torrent_tasks: Dict[str, dict] = self.get_data("torrents") or {}
+ torrents_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks)
+
+ # 判断能否通过刷流前置条件
+ pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(torrents_size=torrents_size)
+ if not pre_condition_passed:
+ return
+
+ statistic_info = self.__get_statistic_info()
+ # 获取所有站点的信息,并过滤掉不存在的站点
+ site_infos = []
+ for siteid in brush_config.brushsites:
+ siteinfo = self.siteoper.get(siteid)
+ if siteinfo:
+ site_infos.append(siteinfo)
+
+ # 根据是否开启顺序刷流来决定是否需要打乱顺序
+ if not brush_config.brush_sequential:
+ random.shuffle(site_infos)
+
+ logger.info(f"即将针对站点 {', '.join(site.name for site in site_infos)} 开始刷流")
+
+ # 处理所有站点
+ for site in site_infos:
+ # 如果站点刷流没有正确响应,说明没有通过前置条件,其他站点也不需要继续刷流了
+ if not self.__brush_site_torrents(siteid=site.id, torrent_tasks=torrent_tasks, statistic_info=statistic_info):
+ logger.info(f"站点 {site.name} 刷流中途结束,停止后续刷流")
+ break
+ else:
+ logger.info(f"站点 {site.name} 刷流完成")
+
# 保存数据
- self.save_data("torrents", task_info)
+ self.save_data("torrents", torrent_tasks)
# 保存统计数据
self.save_data("statistic", statistic_info)
logger.info(f"刷流任务执行完成")
+
+ def __brush_site_torrents(self, siteid, torrent_tasks, statistic_info) -> bool:
+
+ siteinfo = self.siteoper.get(siteid)
+ if not siteinfo:
+ logger.warn(f"站点不存在:{siteid}")
+ return True
+
+ logger.info(f"开始获取站点 {siteinfo.name} 的新种子 ...")
+ torrents = self.torrents.browse(domain=siteinfo.domain)
+ if not torrents:
+ logger.info(f"站点 {siteinfo.name} 没有获取到种子")
+ return True
+
+ brush_config = self.__get_brush_config(sitename=siteinfo.name)
+
+ # 排除包含订阅的种子
+ if brush_config.except_subscribe:
+ torrents = self.__filter_torrents_contains_subscribe(torrents=torrents)
+
+ # 按发布日期降序排列
+ torrents.sort(key=lambda x: x.pubdate or '', reverse=True)
+
+ torrents_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks)
+
+ logger.info(f"正在准备种子刷流,数量:{len(torrents)}")
+
+ # 过滤种子
+ for torrent in torrents:
+ # 判断能否通过刷流前置条件
+ seeding_size = torrents_size + torrent.size
+ pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(torrents_size=seeding_size, include_network_conditions=False)
+ if not pre_condition_passed:
+ # logger.info(f"种子没有通过刷流前置条件校验,原因:{reason} 种子:{torrent.title}|{torrent.description}")
+ return False
+ # else:
+ # logger.info(f"种子已通过刷流前置校验,种子:{torrent.title}|{torrent.description}")
+
+ # 判断能否通过刷流条件
+ condition_passed, reason = self.__evaluate_conditions_for_brush(torrent=torrent, torrent_tasks=torrent_tasks)
+ if not condition_passed:
+ # logger.info(f"种子没有通过刷流条件校验,原因:{reason} 种子:{torrent.title}|{torrent.description}")
+ continue
+ # else:
+ # logger.info(f"种子已通过刷流条件校验,种子:{torrent.title}|{torrent.description}")
+ # 添加下载任务
+ hash_string = self.__download(torrent=torrent)
+ if not hash_string:
+ logger.warn(f"{torrent.title} 添加刷流任务失败!")
+ continue
+
+ # 保存任务信息
+ torrent_tasks[hash_string] = {
+ "site": siteinfo.id,
+ "site_name": siteinfo.name,
+ "title": torrent.title,
+ "size": torrent.size,
+ "pubdate": torrent.pubdate,
+ # "site_cookie": torrent.site_cookie,
+ # "site_ua": torrent.site_ua,
+ # "site_proxy": torrent.site_proxy,
+ # "site_order": torrent.site_order,
+ "description": torrent.description,
+ "imdbid": torrent.imdbid,
+ # "enclosure": torrent.enclosure,
+ "page_url": torrent.page_url,
+ # "seeders": torrent.seeders,
+ # "peers": torrent.peers,
+ # "grabs": torrent.grabs,
+ "date_elapsed": torrent.date_elapsed,
+ "freedate": torrent.freedate,
+ "uploadvolumefactor": torrent.uploadvolumefactor,
+ "downloadvolumefactor": torrent.downloadvolumefactor,
+ "hit_and_run": torrent.hit_and_run,
+ "volume_factor": torrent.volume_factor,
+ "freedate_diff": torrent.freedate_diff,
+ # "labels": torrent.labels,
+ # "pri_order": torrent.pri_order,
+ # "category": torrent.category,
+ "ratio": 0,
+ "downloaded": 0,
+ "uploaded": 0,
+ "deleted": False,
+ "time": time.time()
+ }
+
+ # 统计数据
+ torrents_size += torrent.size
+ statistic_info["count"] += 1
+ logger.info(f"站点 {siteinfo.name},新增刷流种子下载:{torrent.title}|{torrent.description}")
+ self.__send_add_message(torrent)
+
+ return True
+
+ def __evaluate_pre_conditions_for_brush(self, torrents_size: int, include_network_conditions: bool = True) -> Tuple[bool, str]:
+ reasons = [
+ ("maxdlcount", lambda config: self.__get_downloading_count() >= int(config),
+ lambda config: f"当前同时下载任务数已达到最大值 {config},暂时停止新增任务"),
+ ("disksize", lambda config: torrents_size > float(config) * 1024 ** 3,
+ lambda config: f"当前做种体积 {self.__bytes_to_gb(torrents_size):.1f} GB,已超过保种体积 {config} GB,暂时停止新增任务"),
+ ]
+
+ if include_network_conditions:
+ downloader_info = self.__get_downloader_info()
+ if downloader_info:
+ current_upload_speed = downloader_info.upload_speed or 0
+ current_download_speed = downloader_info.download_speed or 0
+ reasons.extend([
+ ("maxupspeed", lambda config: current_upload_speed >= float(config) * 1024,
+ lambda config: f"当前总上传带宽 {StringUtils.str_filesize(current_upload_speed)},已达到最大值 {config} KB/s,暂时停止新增任务"),
+ ("maxdlspeed", lambda config: current_download_speed >= float(config) * 1024,
+ lambda config: f"当前总下载带宽 {StringUtils.str_filesize(current_download_speed)},已达到最大值 {config} KB/s,暂时停止新增任务"),
+ ])
+
+ brush_config = self.__get_brush_config()
+ for condition, check, message in reasons:
+ config_value = getattr(brush_config, condition, None)
+ if config_value and check(config_value):
+ reason = message(config_value)
+ logger.warn(reason)
+ return False, reason
+
+ return True, None
+
+ def __evaluate_conditions_for_brush(self, torrent, torrent_tasks) -> Tuple[bool, str]:
+ """
+ 过滤不符合条件的种子
+ """
+ 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 brush_config.freeleech and torrent.downloadvolumefactor != 0:
+ return False, "非免费种子"
+ if brush_config.freeleech == "2xfree" and torrent.uploadvolumefactor != 2:
+ return False, "非双倍上传种子"
+
+ # H&R
+ if brush_config.hr == "yes" and torrent.hit_and_run:
+ return False, "存在H&R"
+
+ # 包含规则
+ if brush_config.include and not (re.search(brush_config.include, torrent.title, re.I) or re.search(brush_config.include, torrent.description, re.I)):
+ return False, "不符合包含规则"
+
+ # 排除规则
+ if brush_config.exclude and (re.search(brush_config.exclude, torrent.title, re.I) or re.search(brush_config.exclude, torrent.description, re.I)):
+ return False, "符合排除规则"
+
+ # 种子大小(GB)
+ if brush_config.size:
+ sizes = [float(size) * 1024**3 for size in brush_config.size.split("-")]
+ if len(sizes) == 1 and torrent.size < sizes[0]:
+ return False, "种子大小不符合条件"
+ elif len(sizes) > 1 and not sizes[0] <= torrent.size <= sizes[1]:
+ return False, "种子大小不在指定范围内"
+
+ # 做种人数
+ if brush_config.seeder:
+ seeders_range = [int(n) for n in brush_config.seeder.split("-")]
+ # 检查是否仅指定了一个数字,即做种人数需要小于等于该数字
+ if len(seeders_range) == 1:
+ # 当做种人数大于该数字时,不符合条件
+ if torrent.seeders > seeders_range[0]:
+ return False, "做种人数超过单个指定值"
+ # 如果指定了一个范围
+ elif len(seeders_range) > 1:
+ # 检查做种人数是否在指定的范围内(包括边界)
+ if not (seeders_range[0] <= torrent.seeders <= seeders_range[1]):
+ return False, "做种人数不在指定范围内"
+
+ # 发布时间
+ 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("-")]
+ if len(pubtimes) == 1:
+ # 单个值:选择发布时间小于等于该值的种子
+ if pubdate_minutes > pubtimes[0]:
+ return False, "发布时间不符合条件"
+ else:
+ # 范围值:选择发布时间在范围内的种子
+ if not (pubtimes[0] <= pubdate_minutes <= pubtimes[1]):
+ return False, "发布时间不在指定范围内"
+
+ return True, None
+
+ #endregion
+
+ #region Check
+
def check(self):
"""
定时检查,删除下载任务
- {
- hash: {
- site_name:
- size:
- }
- }
"""
- if not self._downloader:
+ brush_config = self.__get_brush_config()
+
+ if not brush_config.downloader:
return
+
+ if not self.__is_current_time_in_range():
+ logger.info(f"当前不在指定的刷流时间区间内,检查操作将暂时暂停")
+ return
with lock:
- logger.info(f"开始检查刷流下载任务 ...")
- # 读取种子记录
- task_info: Dict[str, dict] = self.get_data("torrents") or {}
- # 种子Hash
- check_hashs = list(task_info.keys())
- if not task_info or not check_hashs:
- logger.info(f"没有需要检查的刷流下载任务")
- return
- logger.info(f"共有 {len(check_hashs)} 个任务正在刷流,开始检查任务状态")
- # 获取下载器实例
- downloader = self.__get_downloader(self._downloader)
+ logger.info("开始检查刷流下载任务 ...")
+ torrent_tasks: Dict[str, dict] = self.get_data("torrents") or {}
+ unmanaged_tasks: Dict[str, dict] = self.get_data("unmanaged") or {}
+
+ downloader = self.__get_downloader(brush_config.downloader)
if not downloader:
+ logger.warn("无法获取下载器实例,将在下个时间周期重试")
return
- # 读取统计数据
- statistic_info = self.get_data("statistic") or {
- "count": 0,
- "deleted": 0,
- "uploaded": 0,
- "downloaded": 0
- }
- # 获取下载器中的种子
- torrents, error = downloader.get_torrents(ids=check_hashs)
+
+ seeding_torrents, error = downloader.get_torrents()
if error:
logger.warn("连接下载器出错,将在下个时间周期重试")
return
- if not torrents:
- logger.warn(f"刷流任务在下载器中不存在,清除记录")
- self.save_data("torrents", {})
+
+ seeding_torrents_dict = {self.__get_hash(torrent): torrent for torrent in seeding_torrents}
+
+ self.__update_seeding_tasks_based_on_tags(torrent_tasks=torrent_tasks, unmanaged_tasks=unmanaged_tasks, seeding_torrents_dict=seeding_torrents_dict)
+
+ torrent_check_hashes = list(torrent_tasks.keys())
+ if not torrent_tasks or not torrent_check_hashes:
+ logger.info("没有需要检查的刷流下载任务")
return
- # 检查种子状态,判断是否要删种
- remove_torrents = []
- for torrent in torrents:
- torrent_hash = self.__get_hash(torrent)
- site_name = task_info.get(torrent_hash).get("site_name")
- torrent_info = self.__get_torrent_info(torrent)
- # 更新上传量、下载量
- if not task_info.get(torrent_info.get("hash")):
- task_info[torrent_hash] = {
- "downloaded": torrent_info.get("downloaded"),
- "uploaded": torrent_info.get("uploaded"),
- "ratio": torrent_info.get("ratio"),
- }
+
+ logger.info(f"共有 {len(torrent_check_hashes)} 个任务正在刷流,开始检查任务状态")
+
+ # 获取到当前所有做种数据中需要被检查的种子数据
+ check_torrents = [seeding_torrents_dict[th] for th in torrent_check_hashes if th in seeding_torrents_dict]
+
+ # 先更新刷流任务的最新状态,上下传,分享率
+ self.__update_torrent_tasks_state(torrents=check_torrents, torrent_tasks=torrent_tasks)
+
+ # 先通过获取的全量种子,判断已经被删除,但是任务记录中还没有被标记删除的种子
+ undeleted_hashes = self.__get_undeleted_torrents_missing_in_downloader(torrent_tasks, torrent_check_hashes, check_torrents) or []
+
+ # 排除MoviePilot种子
+ if check_torrents and brush_config.except_tags:
+ check_torrents = self.__filter_torrents_by_tag(torrents=check_torrents, exclude_tag=settings.TORRENT_TAG)
+
+ need_delete_hashes = []
+ need_delete_hashes.extend(undeleted_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, torrent_tasks=torrent_tasks) or []
+ need_delete_hashes.extend(proxy_delete_hashs)
+ # 否则均认为是没有开启动态删种
+ else:
+ logger.info("没有开启动态删种,按用户设置删种条件开始检查任务")
+ not_proxy_delete_hashs = self.__delete_torrent_for_evaluate_conditions(torrents=check_torrents, torrent_tasks=torrent_tasks) or []
+ need_delete_hashes.extend(not_proxy_delete_hashs)
+
+ if need_delete_hashes:
+ if downloader.delete_torrents(ids=need_delete_hashes, delete_file=True):
+ for torrent_hash in need_delete_hashes:
+ torrent_tasks[torrent_hash]["deleted"] = True
+
+ self.__update_and_save_statistic_info(torrent_tasks)
+
+ self.save_data("torrents", torrent_tasks)
+
+ logger.info("刷流下载任务检查完成")
+
+ def __update_torrent_tasks_state(self, torrents: List[Any], torrent_tasks: Dict[str, dict]):
+ """
+ 更新刷流任务的最新状态,上下传,分享率
+ """
+ for torrent in torrents:
+ torrent_hash = self.__get_hash(torrent)
+ torrent_task = torrent_tasks.get(torrent_hash, None)
+ # 如果找不到种子任务,说明不在管理的种子范围内,直接跳过
+ if not torrent_task:
+ continue
+
+ torrent_info = self.__get_torrent_info(torrent)
+
+ # 更新上传量、下载量
+ torrent_task.update({
+ "downloaded": torrent_info.get("downloaded"),
+ "uploaded": torrent_info.get("uploaded"),
+ "ratio": torrent_info.get("ratio"),
+ })
+
+ def __update_seeding_tasks_based_on_tags(self, torrent_tasks: Dict[str, dict], unmanaged_tasks: Dict[str, dict], seeding_torrents_dict: Dict[str, Any]):
+ brush_config = self.__get_brush_config()
+
+ if not brush_config.downloader == "qbittorrent":
+ return
+
+ # 基于 seeding_torrents_dict 的信息更新或添加到 torrent_tasks
+ for torrent_hash, torrent in seeding_torrents_dict.items():
+ tags = self.__get_label(torrent=torrent)
+ # 判断是否包含刷流标签
+ if brush_config.brush_tag in tags:
+ # 如果包含刷流标签又不在刷流任务中,则需要加入管理
+ if torrent_hash not in torrent_tasks:
+ # 检查该种子是否在 unmanaged_tasks 中
+ if torrent_hash in unmanaged_tasks:
+ # 如果在 unmanaged_tasks 中,移除并转移到 torrent_tasks
+ torrent_task = unmanaged_tasks.pop(torrent_hash)
+ torrent_tasks[torrent_hash] = torrent_task
+ logger.info(f"站点 {torrent_task.get('site_name')},种子再次加入刷流任务:{torrent_task.get('title')}|{torrent_task.get('description')}")
+ self.__send_add_message(torrent=torrent_task, title="【刷流任务种子再次加入】")
+ else:
+ # 否则,创建一个新的任务
+ torrent_task = self.__convert_torrent_info_to_task(torrent)
+ torrent_tasks[torrent_hash] = torrent_task
+ logger.info(f"站点 {torrent_task.get('site_name')},种子加入刷流任务:{torrent_task.get('title')}|{torrent_task.get('description')}")
+ self.__send_add_message(torrent=torrent_task, title="【刷流任务种子加入】")
+ # 包含刷流标签又在刷流任务中,这里额外处理一个特殊逻辑,就是种子在刷流任务中可能被标记删除但实际上又还在下载器中,这里进行重置
else:
- task_info[torrent_hash]["downloaded"] = torrent_info.get("downloaded")
- task_info[torrent_hash]["uploaded"] = torrent_info.get("uploaded")
- task_info[torrent_hash]["ratio"] = torrent_info.get("ratio")
- # 做种时间(小时)
- if self._seed_time:
- if torrent_info.get("seeding_time") >= float(self._seed_time) * 3600:
- logger.info(f"做种时间达到 {self._seed_time} 小时,删除种子:{torrent_info.get('title')}")
- downloader.delete_torrents(ids=torrent_hash, delete_file=True)
- remove_torrents.append(torrent_info)
- self.__send_delete_message(site_name=site_name,
- torrent_title=torrent_info.get("title"),
- reason=f"做种时间达到 {self._seed_time} 小时")
- continue
- # 分享率
- if self._seed_ratio:
- if torrent_info.get("ratio") >= float(self._seed_ratio):
- logger.info(f"分享率达到 {self._seed_ratio},删除种子:{torrent_info.get('title')}")
- downloader.delete_torrents(ids=torrent_hash, delete_file=True)
- remove_torrents.append(torrent_info)
- self.__send_delete_message(site_name=site_name,
- torrent_title=torrent_info.get("title"),
- reason=f"分享率达到 {self._seed_ratio}")
- continue
- # 上传量(GB)
- if self._seed_size:
- if torrent_info.get("uploaded") >= float(self._seed_size) * 1024 * 1024 * 1024:
- logger.info(f"上传量达到 {self._seed_size} GB,删除种子:{torrent_info.get('title')}")
- downloader.delete_torrents(ids=torrent_hash, delete_file=True)
- remove_torrents.append(torrent_info)
- self.__send_delete_message(site_name=site_name,
- torrent_title=torrent_info.get("title"),
- reason=f"上传量达到 {self._seed_size} GB")
- continue
- # 下载耗时(小时)
- if self._download_time \
- and torrent_info.get("downloaded") < torrent_info.get("total_size"):
- if torrent_info.get("dltime") >= float(self._download_time) * 3600:
- logger.info(f"下载耗时达到 {self._download_time} 小时,删除种子:{torrent_info.get('title')}")
- downloader.delete_torrents(ids=torrent_hash, delete_file=True)
- remove_torrents.append(torrent_info)
- self.__send_delete_message(site_name=site_name,
- torrent_title=torrent_info.get("title"),
- reason=f"下载耗时达到 {self._download_time} 小时")
- continue
- # 平均上传速度(KB / s),大于30分钟才有效
- if self._seed_avgspeed:
- if torrent_info.get("avg_upspeed") <= float(self._seed_avgspeed) * 1024 and \
- torrent_info.get("seeding_time") >= 30 * 60:
- logger.info(f"平均上传速度低于 {self._seed_avgspeed} KB/s,删除种子:{torrent_info.get('title')}")
- downloader.delete_torrents(ids=torrent_hash, delete_file=True)
- remove_torrents.append(torrent_info)
- self.__send_delete_message(site_name=site_name,
- torrent_title=torrent_info.get("title"),
- reason=f"平均上传速度低于 {self._seed_avgspeed} KB/s")
- continue
- # 未活动时间(分钟)
- if self._seed_inactivetime:
- if torrent_info.get("iatime") >= float(self._seed_inactivetime) * 60:
- logger.info(
- f"未活动时间达到 {self._seed_inactivetime} 分钟,删除种子:{torrent_info.get('title')}")
- downloader.delete_torrents(ids=torrent_hash, delete_file=True)
- remove_torrents.append(torrent_info)
- self.__send_delete_message(site_name=site_name,
- torrent_title=torrent_info.get("title"),
- reason=f"未活动时间达到 {self._seed_inactivetime} 分钟")
- continue
- # 统计删除状态
- if remove_torrents:
- if not statistic_info.get("deleted"):
- statistic_info["deleted"] = 0
- statistic_info["deleted"] += len(remove_torrents)
- # 删除任务记录
- for torrent in remove_torrents:
- task_info[torrent.get("hash")].update({
- "deleted": True,
- })
- # 统计总上传量、下载量
- total_uploaded = 0
- total_downloaded = 0
- for hash_str, task in task_info.items():
- total_downloaded += task.get("downloaded") or 0
- total_uploaded += task.get("uploaded") or 0
- # 更新统计数据
- statistic_info["uploaded"] = total_uploaded
- statistic_info["downloaded"] = total_downloaded
- # 打印统计数据
- logger.info(f"刷流任务统计数据:"
- f"总任务数:{len(task_info)},"
- f"已删除:{statistic_info.get('deleted')},"
- f"总上传量:{StringUtils.str_filesize(statistic_info.get('uploaded'))},"
- f"总下载量:{StringUtils.str_filesize(statistic_info.get('downloaded'))}")
- # 保存统计数据
- self.save_data("statistic", statistic_info)
- # 保存任务记录
- self.save_data("torrents", task_info)
- logger.info(f"刷流下载任务检查完成")
+ torrent_task = torrent_tasks[torrent_hash]
+ if torrent_task.get("deleted"):
+ torrent_task["deleted"] = False
+ logger.info(f"站点 {torrent_task.get('site_name')},种子再次加入刷流任务:{torrent_task.get('title')}|{torrent_task.get('description')}")
+ self.__send_add_message(torrent=torrent_task, title="【刷流任务种子再次加入】")
+ else:
+ # 不包含刷流标签但又在刷流任务中,则移除管理
+ if torrent_hash in torrent_tasks:
+ # 如果种子不符合刷流条件但在 torrent_tasks 中,移除并加入 unmanaged_tasks
+ torrent_task = torrent_tasks.pop(torrent_hash)
+ unmanaged_tasks[torrent_hash] = torrent_task
+ logger.info(f"站点 {torrent_task.get('site_name')},种子移除刷流任务:{torrent_task.get('title')}|{torrent_task.get('description')}")
+ self.__send_delete_message(site_name=torrent_task.get("site_name"),
+ torrent_title=torrent_task.get("title"),
+ torrent_desc=torrent_task.get("description"),
+ reason="刷流标签移除",
+ title="【刷流任务种子移除】")
+
+ self.save_data("torrents", torrent_tasks)
+ self.save_data("unmanaged", unmanaged_tasks)
+
+ def __group_torrents_by_proxy_delete(self, torrents: List[Any], torrent_tasks: Dict[str, dict]):
+ """
+ 根据是否启用动态删种进行分组
+ """
+ proxy_delete_torrents = []
+ not_proxy_delete_torrents = []
+
+ for torrent in torrents:
+ torrent_hash = self.__get_hash(torrent)
+ torrent_task = torrent_tasks.get(torrent_hash, None)
+
+ # 如果找不到种子任务,说明不在管理的种子范围内,直接跳过
+ if not torrent_task:
+ continue
+
+ site_name = torrent_task.get("site_name", "")
+ torrent_title = torrent_task.get("title", "")
+ torrent_desc = torrent_task.get("description", "")
+
+ brush_config = self.__get_brush_config(site_name)
+ if brush_config.proxy_delete:
+ proxy_delete_torrents.append(torrent)
+ else:
+ not_proxy_delete_torrents.append(torrent)
+
+ return proxy_delete_torrents, not_proxy_delete_torrents
+
+ def __evaluate_conditions_for_delete(self, site_name: str, torrent_info: str) -> Tuple[bool, str]:
+ """
+ 评估删除条件并返回是否应删除种子及其原因
+ """
+ brush_config = self.__get_brush_config(sitename=site_name)
+
+ if brush_config.seed_time and torrent_info.get("seeding_time") >= float(brush_config.seed_time) * 3600:
+ reason = f"做种时间 {torrent_info.get('seeding_time') / 3600:.1f} 小时,大于 {brush_config.seed_time} 小时"
+ elif brush_config.seed_ratio and torrent_info.get("ratio") >= float(brush_config.seed_ratio):
+ reason = f"分享率 {torrent_info.get('ratio'):.2f},大于 {brush_config.seed_ratio}"
+ elif brush_config.seed_size and torrent_info.get("uploaded") >= float(brush_config.seed_size) * 1024**3:
+ reason = f"上传量 {torrent_info.get('uploaded') / 1024**3:.1f} GB,大于 {brush_config.seed_size} GB"
+ elif 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} 小时"
+ elif brush_config.seed_avgspeed and torrent_info.get("avg_upspeed") <= float(brush_config.seed_avgspeed) * 1024 and torrent_info.get("seeding_time") >= 30 * 60:
+ reason = f"平均上传速度 {torrent_info.get('avg_upspeed') / 1024:.1f} KB/s,低于 {brush_config.seed_avgspeed} KB/s"
+ elif brush_config.seed_inactivetime and torrent_info.get("iatime") >= float(brush_config.seed_inactivetime) * 60:
+ reason = f"未活动时间 {torrent_info.get('iatime') / 60:.0f} 分钟,大于 {brush_config.seed_inactivetime} 分钟"
+ else:
+ return False, ""
+
+ return True, reason
+
+ def __delete_torrent_for_evaluate_conditions(self, torrents: List[Any], torrent_tasks: Dict[str, dict], proxy_delete: bool = False) -> List:
+ """
+ 根据条件删除种子并获取已删除列表
+ """
+ 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
+ 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_conditions_for_delete(site_name=site_name, torrent_info=torrent_info)
+ if should_delete:
+ delete_hashs.append(torrent_hash)
+ reason = "触发动态删除," + reason if proxy_delete else reason
+ 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}")
+
+ return delete_hashs
+
+ def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List:
+ """
+ 支持动态删除种子,当设置了动态删种(全局)和删除阈值时,当保种体积达到删除阈值时,优先按设置规则进行删除,若还没有达到阈值,则排除HR种子后按加入时间倒序进行删除
+ 删除阈值:100,当保种体积 > 100G 时,则开始删除种子,直至降低至 100G
+ 删除阈值:50-100,当保种体积 > 100G 时,则开始删除种子,直至降至为 50G
+ """
+ brush_config = self.__get_brush_config()
+
+ # 如果没有启用动态删除或没有设置删除阈值,则不执行删除操作
+ if not (brush_config.proxy_delete and brush_config.delete_size_range):
+ return []
+
+ # 解析删除阈值范围
+ 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())
+
+ # 当总体积未超过最大阈值时,不需要执行删除操作
+ 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,下限 {self.__bytes_to_gb(min_size):.1f} GB,未触发动态删除")
+ return []
+ else:
+ logger.info(f"当前保种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB,下限 {self.__bytes_to_gb(min_size):.1f} GB,触发动态删除")
+
+ need_delete_hashes = []
+
+ # 即使开了动态删除,但是也有可能部分站点单独设置了关闭,这里根据种子动态进行分组,先处理不需要动态的种子,按设置的规则进行删除
+ proxy_delete_torrents, not_proxy_delete_torrents = self.__group_torrents_by_proxy_delete(torrents=torrents, torrent_tasks=torrent_tasks)
+ logger.info(f"托管种子数 {len(proxy_delete_torrents)},未托管种子数 {len(not_proxy_delete_torrents)}")
+ if not_proxy_delete_torrents:
+ not_proxy_delete_hashes = self.__delete_torrent_for_evaluate_conditions(torrents=not_proxy_delete_torrents, torrent_tasks=torrent_tasks) or []
+ need_delete_hashes.extend(not_proxy_delete_hashes)
+ total_torrent_size -= sum(torrent_info_map[self.__get_hash(torrent)].get("total_size", 0) for torrent in not_proxy_delete_torrents if self.__get_hash(torrent) in not_proxy_delete_hashes)
+
+ # 如果删除非动态种子后仍未达到最小体积要求,则处理动态种子
+ if total_torrent_size > min_size and proxy_delete_torrents:
+ proxy_delete_hashes = self.__delete_torrent_for_evaluate_conditions(torrents=proxy_delete_torrents, torrent_tasks=torrent_tasks, proxy_delete=True) or []
+ need_delete_hashes.extend(proxy_delete_hashes)
+ total_torrent_size -= sum(torrent_info_map[self.__get_hash(torrent)].get("total_size", 0) for torrent in proxy_delete_torrents if self.__get_hash(torrent) in proxy_delete_hashes)
+
+ # 在完成初始删除步骤后,如果总体积仍然超过最小阈值,则进一步找到已完成种子并排除HR种子后按加入时间倒序进行删除
+ if total_torrent_size > min_size:
+ # 重新计算当前的种子列表,排除已删除的种子
+ remaining_hashes = {self.__get_hash(torrent) for torrent in proxy_delete_torrents} - set(need_delete_hashes)
+ # 这里根据排除后的种子列表,再次从下载器中找到已完成的任务
+ downloader = self.__get_downloader(brush_config.downloader)
+ completed_torrents = downloader.get_completed_torrents(ids=remaining_hashes)
+ remaining_hashes = {self.__get_hash(torrent) for torrent in completed_torrents}
+ remaining_torrents = [(hash, torrent_info_map[hash]) for hash in remaining_hashes]
+
+ # 准备一个列表,用于存放满足条件的种子,即非HR种子且有明确加入时间
+ filtered_torrents = [(hash, info['add_on']) for hash, info in remaining_torrents if not torrent_tasks[hash].get("hit_and_run", False)]
+ sorted_torrents = sorted(filtered_torrents, key=lambda x: x[1])
+
+ # 进行额外的删除操作,直到满足最小阈值或没有更多种子可删除
+ for torrent_hash, _ in sorted_torrents:
+ if total_torrent_size <= min_size:
+ break
+ torrent_task = torrent_tasks.get(torrent_hash, None)
+ torrent_info = torrent_info_map.get(torrent_hash, None)
+ if not torrent_task or not torrent_info:
+ continue
+
+ need_delete_hashes.append(torrent_hash)
+ total_torrent_size -= torrent_info.get("total_size", 0)
+
+ site_name = torrent_task.get("site_name", "")
+ torrent_title = torrent_task.get("title", "")
+ torrent_desc = torrent_task.get("description", "")
+ reason = "触发动态删除,系统自动删除"
+ 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"已完成 {len(need_delete_hashes)} 个种子删除,当前保种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB"
+ self.post_message(mtype=NotificationType.SiteMessage, title="【刷流任务种子删除】", text=msg)
+ logger.info(msg)
+
+ # 返回所有需要删除的种子的哈希列表
+ return need_delete_hashes
+
+ def __get_undeleted_torrents_missing_in_downloader(self, torrent_tasks, torrent_check_hashes, torrents) -> List:
+ """
+ 处理已经被删除,但是任务记录中还没有被标记删除的种子
+ """
+ torrent_all_hashes = self.__get_all_hashes(torrents)
+ missing_hashes = [hash_value for hash_value in torrent_check_hashes if hash_value not in torrent_all_hashes]
+ undeleted_hashes = [hash_value for hash_value in missing_hashes if not torrent_tasks[hash_value].get("deleted")]
+
+ if not undeleted_hashes:
+ return []
+
+ # 处理每个符合条件的任务
+ for hash_value in undeleted_hashes:
+ # 获取对应的任务信息
+ torrent_info = torrent_tasks[hash_value]
+ # 获取site_name和torrent_title
+ site_name = torrent_info.get("site_name", "")
+ torrent_title = torrent_info.get("title", "")
+ torrent_desc = torrent_info.get("description", "")
+ self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, reason="下载器中找不到种子")
+ logger.info(f"站点:{site_name},下载器中找不到种子,删除种子:{torrent_title}|{torrent_desc}")
+ return undeleted_hashes
+
+ def __convert_torrent_info_to_task(self, torrent: Any) -> dict:
+ """
+ 根据torrent_info转换成torrent_task
+ """
+ torrent_info = self.__get_torrent_info(torrent=torrent)
+
+ trackers = [tracker.get("url") for tracker in (torrent.trackers or []) if
+ tracker.get("tier", -1) >= 0 and tracker.get("url")]
+ trackers.append(torrent_info.get("tracker"))
+
+ site_id, site_name = self.__get_site_by_tracker(trackers=trackers)
+
+ torrent_task = {
+ "site": site_id,
+ "site_name": site_name,
+ "title": torrent_info.get("title", ""),
+ "size": torrent_info.get("total_size", 0), # 假设total_size对应于size
+ "pubdate": None,
+ "description": None,
+ "imdbid": None,
+ "page_url": None,
+ "date_elapsed": None,
+ "freedate": None,
+ "uploadvolumefactor": None,
+ "downloadvolumefactor": None,
+ "hit_and_run": None,
+ "volume_factor": None,
+ "freedate_diff": None, # 假设无法从torrent_info直接获取
+ "ratio": torrent_info.get("ratio", 0),
+ "downloaded": torrent_info.get("downloaded", 0),
+ "uploaded": torrent_info.get("uploaded", 0),
+ "deleted": False,
+ "time": torrent_info.get("add_on", time.time())
+ }
+ return torrent_task
+
+ #endregion
+
+ def __update_and_save_statistic_info(self, torrent_tasks):
+ """
+ 更新并保存统计信息
+ """
+ total_count, total_uploaded, total_downloaded, total_deleted = 0, 0, 0, 0
+ active_uploaded, active_downloaded, active_count, total_unarchived = 0, 0, 0, 0
+
+ statistic_info = self.__get_statistic_info()
+ archived_tasks = self.get_data("archived") or {}
+ combined_tasks = {**torrent_tasks, **archived_tasks}
+
+ for task in combined_tasks.values():
+ if task.get("deleted", False):
+ total_deleted += 1
+ total_downloaded += task.get("downloaded", 0)
+ total_uploaded += task.get("uploaded", 0)
+
+ # 计算torrent_tasks中未标记为删除的活跃任务的统计信息,及待归档的任务数
+ for task in torrent_tasks.values():
+ if not task.get("deleted", False):
+ active_uploaded += task.get("uploaded", 0)
+ active_downloaded += task.get("downloaded", 0)
+ active_count += 1
+ else:
+ total_unarchived += 1
+
+ # 更新统计信息
+ total_count = len(combined_tasks)
+ statistic_info.update({
+ "uploaded": total_uploaded,
+ "downloaded": total_downloaded,
+ "deleted": total_deleted,
+ "unarchived": total_unarchived,
+ "count": total_count,
+ "active": active_count,
+ "active_uploaded": active_uploaded,
+ "active_downloaded": active_downloaded
+ })
+
+ logger.info(f"刷流任务统计数据:总任务数:{total_count},活跃任务数:{active_count},已删除:{total_deleted},"
+ f"待归档:{total_unarchived},"
+ f"活跃上传量:{StringUtils.str_filesize(active_uploaded)},"
+ f"活跃下载量:{StringUtils.str_filesize(active_downloaded)},"
+ f"总上传量:{StringUtils.str_filesize(total_uploaded)},"
+ f"总下载量:{StringUtils.str_filesize(total_downloaded)}")
+
+ self.save_data("statistic", statistic_info)
+ self.save_data("torrents", torrent_tasks)
+
+ def __get_brush_config(self, sitename: str = None) -> BrushConfig:
+ """
+ 获取BrushConfig
+ """
+ return self._brush_config if not sitename else self._brush_config.get_site_config(sitename=sitename)
+
+ def __validate_and_fix_config(self, config: dict = None) -> bool:
+ """
+ 检查并修正配置值
+ """
+ if config is None:
+ logger.error("配置为None,无法验证和修正")
+ return False
+
+ # 设置一个标志,用于跟踪是否发现校验错误
+ found_error = False
+
+ config_number_attr_to_desc = {
+ "disksize": "保种体积",
+ "maxupspeed": "总上传带宽",
+ "maxdlspeed": "总下载带宽",
+ "maxdlcount": "同时下载任务数",
+ "seed_time": "做种时间",
+ "seed_ratio": "分享率",
+ "seed_size": "上传量",
+ "download_time": "下载超时时间",
+ "seed_avgspeed": "平均上传速度",
+ "seed_inactivetime": "未活动时间",
+ "up_speed": "单任务上传限速",
+ "dl_speed": "单任务下载限速"
+ }
+
+ config_range_number_attr_to_desc = {
+ "pubtime": "发布时间",
+ "size": "种子大小",
+ "seeder": "做种人数",
+ "delete_size_range": "动态删种阈值"
+ }
+
+ for attr, desc in config_number_attr_to_desc.items():
+ value = config.get(attr)
+ if value and not self.__is_number(value):
+ self.__log_and_notify_error(f"站点刷流任务出错,{desc}设置错误:{value}")
+ config[attr] = None
+ found_error = True # 更新错误标志
+
+ for attr, desc in config_range_number_attr_to_desc.items():
+ value = config.get(attr)
+ # 检查 value 是否存在且是否符合数字或数字-数字的模式
+ if value and not self.__is_number_or_range(str(value)):
+ self.__log_and_notify_error(f"站点刷流任务出错,{desc}设置错误:{value}")
+ config[attr] = None
+ found_error = True # 更新错误标志
+
+ active_time_range = config.get("active_time_range")
+ if active_time_range and not self.__is_valid_time_range(time_range=active_time_range):
+ self.__log_and_notify_error(f"站点刷流任务出错,开启时间段设置错误:{active_time_range}")
+ config["active_time_range"] = None
+ found_error = True # 更新错误标志
+
+ # 如果发现任何错误,返回False;否则返回True
+ return not found_error
+
+ def __update_config(self, brush_config: BrushConfig = None):
+ """
+ 根据传入的BrushConfig实例更新配置
+ """
+ if brush_config is None:
+ brush_config = self._brush_config
+
+ if brush_config is None:
+ return
+
+ # 创建一个将配置属性名称映射到BrushConfig属性值的字典
+ config_mapping = {
+ "onlyonce": brush_config.onlyonce,
+ "enabled": brush_config.enabled,
+ "notify": brush_config.notify,
+ "brushsites": brush_config.brushsites,
+ "downloader": brush_config.downloader,
+ "disksize": brush_config.disksize,
+ "freeleech": brush_config.freeleech,
+ "hr": brush_config.hr,
+ "maxupspeed": brush_config.maxupspeed,
+ "maxdlspeed": brush_config.maxdlspeed,
+ "maxdlcount": brush_config.maxdlcount,
+ "include": brush_config.include,
+ "exclude": brush_config.exclude,
+ "size": brush_config.size,
+ "seeder": brush_config.seeder,
+ "pubtime": brush_config.pubtime,
+ "seed_time": brush_config.seed_time,
+ "seed_ratio": brush_config.seed_ratio,
+ "seed_size": brush_config.seed_size,
+ "download_time": brush_config.download_time,
+ "seed_avgspeed": brush_config.seed_avgspeed,
+ "seed_inactivetime": brush_config.seed_inactivetime,
+ "delete_size_range": brush_config.delete_size_range,
+ "up_speed": brush_config.up_speed,
+ "dl_speed": brush_config.dl_speed,
+ "save_path": brush_config.save_path,
+ "clear_task": brush_config.clear_task,
+ "archive_task": brush_config.archive_task,
+ "except_tags": brush_config.except_tags,
+ "except_subscribe": brush_config.except_subscribe,
+ "brush_sequential": brush_config.brush_sequential,
+ "proxy_download": brush_config.proxy_download,
+ "proxy_delete": brush_config.proxy_delete,
+ "active_time_range": brush_config.active_time_range,
+ "enable_site_config": brush_config.enable_site_config,
+ "site_config": brush_config.site_config
+ }
+
+ # 使用update_config方法或其等效方法更新配置
+ self.update_config(config_mapping)
+
+ def __setup_downloader(self):
+ """
+ 根据下载器类型初始化下载器实例
+ """
+ brush_config = self.__get_brush_config()
+
+ if brush_config.downloader == "qbittorrent":
+ self.qb = Qbittorrent()
+ if self.qb.is_inactive():
+ self.__log_and_notify_error("站点刷流任务出错:Qbittorrent未连接")
+ return False
+
+ elif brush_config.downloader == "transmission":
+ self.tr = Transmission()
+ if self.tr.is_inactive():
+ self.__log_and_notify_error("站点刷流任务出错:Transmission未连接")
+ return False
+
+ return True
def __get_downloader(self, dtype: str) -> Optional[Union[Transmission, Qbittorrent]]:
"""
@@ -1581,7 +2449,7 @@ class BrushFlow(_PluginBase):
return None
@staticmethod
- def __get_redict_url(url: str, ua: str = None, cookie: str = None) -> Optional[str]:
+ def __get_redict_url(url: str, proxies: str = None, ua: str = None, cookie: str = None) -> Optional[str]:
"""
获取下载链接, url格式:[base64]url
"""
@@ -1609,6 +2477,7 @@ class BrushFlow(_PluginBase):
# GET请求
res = RequestUtils(
ua=ua,
+ proxies=proxies,
cookies=cookie,
headers=headers
).get_res(url, params=req_params.get('params'))
@@ -1616,6 +2485,7 @@ class BrushFlow(_PluginBase):
# POST请求
res = RequestUtils(
ua=ua,
+ proxies=proxies,
cookies=cookie,
headers=headers
).post_res(url, params=req_params.get('params'))
@@ -1629,7 +2499,7 @@ class BrushFlow(_PluginBase):
data = data.get(key)
if not data:
return None
- logger.info(f"获取到下载地址:{data}")
+ # logger.info(f"获取到下载地址:{data}")
return data
return None
@@ -1638,22 +2508,29 @@ class BrushFlow(_PluginBase):
添加下载任务
"""
if not torrent.enclosure:
- return None
- # 上传限速
- up_speed = int(self._up_speed) if self._up_speed else None
- # 下载限速
- down_speed = int(self._dl_speed) if self._dl_speed else None
- # 获取下载链接
- content = torrent.enclosure
- if content.startswith("["):
- content = self.__get_redict_url(url=content,
- ua=torrent.site_ua,
- cookie=torrent.site_cookie)
- if not content:
logger.error(f"获取下载链接失败:{torrent.title}")
return None
- # 添加下载任务
- if self._downloader == "qbittorrent":
+
+ brush_config = self.__get_brush_config(torrent.site_name)
+
+ # 上传限速
+ up_speed = int(brush_config.up_speed) if brush_config.up_speed else None
+ # 下载限速
+ down_speed = int(brush_config.dl_speed) if brush_config.dl_speed else None
+ # 保存地址
+ download_dir = brush_config.save_path or None
+ # 获取下载链接
+ torrent_content = torrent.enclosure
+ if torrent_content.startswith("["):
+ torrent_content = self.__get_redict_url(url=torrent_content,
+ proxies=settings.PROXY if torrent.site_proxy else None,
+ ua=torrent.site_ua,
+ cookie=torrent.site_cookie)
+ if not torrent_content:
+ logger.error(f"获取下载链接失败:{torrent.title}")
+ return None
+
+ if brush_config.downloader == "qbittorrent":
if not self.qb:
return None
# 限速值转为bytes
@@ -1661,76 +2538,111 @@ class BrushFlow(_PluginBase):
down_speed = down_speed * 1024 if down_speed else None
# 生成随机Tag
tag = StringUtils.generate_random_str(10)
- # 添加下载
- if (self._offline_mode
- and not content.startswith("magnet")):
- torrent_res = RequestUtils(cookies=torrent.site_cookie,
- proxies=settings.PROXY if torrent.site_proxy else None,
- ua=torrent.site_ua).get_res(url=content)
- if torrent_res.ok:
- content = torrent_res.content
+ # 如果开启代理下载以及种子地址不是磁力地址,则请求种子到内存再传入下载器
+ if brush_config.proxy_download and not torrent_content.startswith("magnet"):
+ response = RequestUtils(cookies=torrent.site_cookie,
+ proxies=settings.PROXY if torrent.site_proxy else None,
+ ua=torrent.site_ua).get_res(url=torrent_content)
+ if response and response.ok:
+ torrent_content = response.content
else:
- logger.error('下载种子文件失败,继续提交种子链接进行下载')
- state = self.qb.add_torrent(content=content,
- download_dir=self._save_path or None,
- cookie=torrent.site_cookie,
- tag=["已整理", "刷流", tag],
- upload_limit=up_speed,
- download_limit=down_speed)
+ logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载')
+ if torrent_content:
+ state = True
+ state = self.qb.add_torrent(content=torrent_content,
+ download_dir=download_dir,
+ cookie=torrent.site_cookie,
+ tag=["已整理", brush_config.brush_tag, tag],
+ upload_limit=up_speed,
+ download_limit=down_speed)
if not state:
return None
else:
# 获取种子Hash
torrent_hash = self.qb.get_torrent_id_by_tag(tags=tag)
if not torrent_hash:
- logger.error(f"{self._downloader} 下载任务添加成功,但获取任务信息失败!")
+ logger.error(f"{brush_config.downloader} 获取种子Hash失败")
return None
return torrent_hash
- elif self._downloader == "transmission":
+
+ elif brush_config.downloader == "transmission":
if not self.tr:
return None
- # 添加任务
- torrent = self.tr.add_torrent(content=content,
- download_dir=self._save_path or None,
- cookie=torrent.site_cookie,
- labels=["已整理", "刷流"])
- if not torrent:
- return None
- else:
- if self._up_speed or self._dl_speed:
- self.tr.change_torrent(hash_string=torrent.hashString,
- upload_limit=up_speed,
- download_limit=down_speed)
- return torrent.hashString
+ # 如果开启代理下载以及种子地址不是磁力地址,则请求种子到内存再传入下载器
+ if brush_config.proxy_download and not torrent_content.startswith("magnet"):
+ response = RequestUtils(cookies=torrent.site_cookie,
+ proxies=settings.PROXY if torrent.site_proxy else None,
+ ua=torrent.site_ua).get_res(url=torrent_content)
+ if response and response.ok:
+ torrent_content = response.content
+ else:
+ logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载')
+ if torrent_content:
+ torrent = self.tr.add_torrent(content=torrent.enclosure,
+ download_dir=download_dir,
+ cookie=torrent.site_cookie,
+ labels=["已整理", brush_config.brush_tag])
+ if not torrent:
+ return None
+ else:
+ if brush_config.up_speed or brush_config.dl_speed:
+ self.tr.change_torrent(hash_string=torrent.hashString,
+ upload_limit=up_speed,
+ download_limit=down_speed)
+ return torrent.hashString
return None
def __get_hash(self, torrent: Any):
"""
获取种子hash
"""
+ brush_config = self.__get_brush_config()
try:
- return torrent.get("hash") if self._downloader == "qbittorrent" else torrent.hashString
+ return torrent.get("hash") if brush_config.downloader == "qbittorrent" else torrent.hashString
except Exception as e:
print(str(e))
return ""
+
+ def __get_all_hashes(self, torrents):
+ """
+ 获取torrents列表中所有种子的Hash值
+
+ :param torrents: 包含种子信息的列表
+ :return: 包含所有Hash值的列表
+ """
+ brush_config = self.__get_brush_config()
+ try:
+ all_hashes = []
+ for torrent in torrents:
+ # 根据下载器类型获取Hash值
+ hash_value = torrent.get("hash") if brush_config.downloader == "qbittorrent" else torrent.hashString
+ if hash_value:
+ all_hashes.append(hash_value)
+ return all_hashes
+ except Exception as e:
+ print(str(e))
+ return []
def __get_label(self, torrent: Any):
"""
获取种子标签
"""
+ brush_config = self.__get_brush_config()
try:
return [str(tag).strip() for tag in torrent.get("tags").split(',')] \
- if self._downloader == "qbittorrent" else torrent.labels or []
+ if brush_config.downloader == "qbittorrent" else torrent.labels or []
except Exception as e:
print(str(e))
return []
def __get_torrent_info(self, torrent: Any) -> dict:
-
- # 当前时间戳
+ """
+ 获取种子信息
+ """
date_now = int(time.time())
+ brush_config = self.__get_brush_config()
# QB
- if self._downloader == "qbittorrent":
+ if brush_config.downloader == "qbittorrent":
"""
{
"added_on": 1693359031,
@@ -1820,7 +2732,12 @@ class BrushFlow(_PluginBase):
# 种子大小
total_size = torrent.get("total_size")
# 添加时间
- add_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(torrent.get("added_on") or 0))
+ add_on = (torrent.get("added_on") or 0)
+ add_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(add_on))
+ # 种子标签
+ tags = torrent.get("tags")
+ # tracker
+ tracker = torrent.get("tracker")
# TR
else:
# ID
@@ -1859,9 +2776,13 @@ class BrushFlow(_PluginBase):
# 种子大小
total_size = torrent.total_size
# 添加时间
- add_time = time.strftime('%Y-%m-%d %H:%M:%S',
- time.localtime(torrent.date_added.timestamp() if torrent.date_added else 0))
-
+ add_on = (torrent.date_added.timestamp() if torrent.date_added else 0)
+ add_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(add_on))
+ # 种子标签
+ tags = torrent.get("tags")
+ # tracker
+ tracker = torrent.get("tracker")
+
return {
"hash": torrent_id,
"title": torrent_title,
@@ -1873,54 +2794,81 @@ class BrushFlow(_PluginBase):
"iatime": iatime,
"dltime": dltime,
"total_size": total_size,
- "add_time": add_time
+ "add_time": add_time,
+ "add_on": add_on,
+ "tags": tags,
+ "tracker": tracker
}
- def __send_delete_message(self, site_name: str, torrent_title: str, reason: str):
+ def __log_and_notify_error(self, message):
+ """
+ 记录错误日志并发送系统通知
+ """
+ logger.error(message)
+ self.systemmessage.put(message)
+
+ def __send_delete_message(self, site_name: str, torrent_title: str, torrent_desc: str, reason: str, title: str = "【刷流任务种子删除】"):
"""
发送删除种子的消息
"""
- if not self._notify:
+ brush_config = self.__get_brush_config()
+ if not brush_config.notify:
return
- self.post_message(
- mtype=NotificationType.SiteMessage,
- title=f"【刷流任务删种】",
- text=f"站点:{site_name}\n"
- f"标题:{torrent_title}\n"
- f"原因:{reason}"
- )
+ msg_text = ""
+ if site_name:
+ msg_text = f"站点:{site_name}"
+ if torrent_title:
+ msg_text = f"{msg_text}\n标题:{torrent_title}"
+ if torrent_desc:
+ msg_text = f"{msg_text}\n内容:{torrent_desc}"
+ if reason:
+ msg_text = f"{msg_text}\n原因:{reason}"
+
+ self.post_message(mtype=NotificationType.SiteMessage, title=title, text=msg_text)
+
+ def __build_add_message_text(self, torrent):
+ """
+ 构建消息文本,兼容TorrentInfo对象和torrent_task字典
+ """
+ # 定义一个辅助函数来统一获取数据的方式
+ def get_data(key, default=None):
+ if isinstance(torrent, dict):
+ return torrent.get(key, default)
+ else:
+ return getattr(torrent, key, default)
- def __send_add_message(self, torrent: TorrentInfo):
+ # 构造消息文本,确保使用中文标签
+ msg_parts = []
+ label_mapping = {
+ "site_name": "站点",
+ "title": "标题",
+ "description": "内容",
+ "size": "大小",
+ "pubdate": "发布时间",
+ "seeders": "做种数",
+ "volume_factor": "促销",
+ "hit_and_run": "Hit&Run"
+ }
+ for key in label_mapping:
+ value = get_data(key)
+ if key == "size" and value and str(value).replace(".", "", 1).isdigit():
+ value = StringUtils.str_filesize(value)
+ if value:
+ msg_parts.append(f"{label_mapping[key]}:{'是' if key == 'hit_and_run' and value else value}")
+
+ return "\n".join(msg_parts)
+
+ def __send_add_message(self, torrent, title: str = "【刷流任务种子下载】"):
"""
发送添加下载的消息
"""
- if not self._notify:
+ brush_config = self.__get_brush_config()
+ if not brush_config.notify:
return
- msg_text = ""
- if torrent.site_name:
- msg_text = f"站点:{torrent.site_name}"
- if torrent.title:
- msg_text = f"{msg_text}\n标题:{torrent.title}"
- if torrent.size:
- if str(torrent.size).replace(".", "").isdigit():
- size = StringUtils.str_filesize(torrent.size)
- else:
- size = torrent.size
- msg_text = f"{msg_text}\n大小:{size}"
- if torrent.pubdate:
- msg_text = f"{msg_text}\n发布时间:{torrent.pubdate}"
- if torrent.seeders:
- msg_text = f"{msg_text}\n做种数:{torrent.seeders}"
- if torrent.volume_factor:
- msg_text = f"{msg_text}\n促销:{torrent.volume_factor}"
- if torrent.hit_and_run:
- msg_text = f"{msg_text}\nHit&Run:是"
- self.post_message(
- mtype=NotificationType.SiteMessage,
- title="【刷流任务种子下载】",
- text=msg_text
- )
+ # 使用辅助方法构建消息文本
+ msg_text = self.__build_add_message_text(torrent)
+ self.post_message(mtype=NotificationType.SiteMessage, title=title, text=msg_text)
def __get_torrents_size(self) -> int:
"""
@@ -1963,14 +2911,14 @@ class BrushFlow(_PluginBase):
"""
获取正在下载的任务数量
"""
- downlader = self.__get_downloader(self._downloader)
+ brush_config = self.__get_brush_config()
+ downlader = self.__get_downloader(brush_config.downloader)
if not downlader:
return 0
torrents = downlader.get_downloading_torrents()
return len(torrents) or 0
- @staticmethod
- def __get_pubminutes(pubdate: str) -> float:
+ def __get_pubminutes(self, pubdate: str) -> float:
"""
将字符串转换为时间,并计算与当前时间差)(分钟)
"""
@@ -1984,3 +2932,266 @@ class BrushFlow(_PluginBase):
except Exception as e:
print(str(e))
return 0
+
+ def __adjust_site_pubminutes(self, pub_minutes: float, torrent: TorrentInfo) -> float:
+ """
+ 处理部分站点的时区逻辑
+ """
+ try:
+ if not torrent:
+ return pub_minutes
+
+ if torrent.site_name == "我堡":
+ # 获取当前时区的UTC偏移量(以秒为单位)
+ utc_offset_seconds = time.timezone
+
+ # 将UTC偏移量转换为分钟
+ utc_offset_minutes = utc_offset_seconds / 60
+
+ # 增加UTC偏移量到pub_minutes
+ adjusted_pub_minutes = pub_minutes + utc_offset_minutes
+
+ return adjusted_pub_minutes
+
+ return pub_minutes
+ except Exception as e:
+ logger.error(str(e))
+ return 0
+
+ def __filter_torrents_by_tag(self, torrents: List[Any], exclude_tag: str) -> List[Any]:
+ """
+ 根据标签过滤torrents
+ """
+ filter_torrents = []
+ for torrent in torrents:
+ # 使用 __get_label 方法获取每个 torrent 的标签列表
+ labels = self.__get_label(torrent)
+ # 如果排除的标签不在这个列表中,则添加到过滤后的列表
+ if exclude_tag not in labels:
+ filter_torrents.append(torrent)
+ return filter_torrents
+
+ def __get_subscribe_titles(self) -> Set[str]:
+ """
+ 获取当前订阅的所有标题,返回一个不包含None的集合
+ """
+ self.subscribeoper = SubscribeOper()
+ subscribes = self.subscribeoper.list()
+
+ # 使用 filter() 函数筛选出有 'name' 属性且该属性值不为 None 的 Subscribe 对象
+ # 然后使用 map() 函数获取每个对象的 'name' 属性值
+ # 最后,使用 set() 函数将结果转换为集合,自动去除重复项和 None
+ subscribe_titles = set(filter(None, map(lambda sub: getattr(sub, 'name', None), subscribes)))
+
+ # 返回不包含 None 的名称集合
+ return subscribe_titles
+
+ def __filter_torrents_contains_subscribe(self, torrents : Any):
+ subscribe_titles = self.__get_subscribe_titles()
+ logger.info(f"当前订阅的名称集合为:{subscribe_titles}")
+
+ # 初始化两个列表,一个用于收集未被排除的种子,一个用于记录被排除的种子
+ included_torrents = []
+ excluded_torrents = []
+
+ # 单次遍历处理
+ for torrent in torrents:
+ # 确保title和description至少是空字符串
+ title = torrent.title or ''
+ description = torrent.description or ''
+
+ if any(subscribe_title in title or subscribe_title in description for subscribe_title in subscribe_titles):
+ # 如果种子的标题或描述包含订阅标题中的任一项,则记录为被排除
+ excluded_torrents.append(torrent)
+ logger.info(f"命中订阅内容,排除种子:{title}|{description}")
+ else:
+ # 否则,收集为未被排除的种子
+ included_torrents.append(torrent)
+
+ if not excluded_torrents:
+ logger.info(f"没有命中订阅内容,不需要排除种子")
+
+ # 返回未被排除的种子列表
+ return included_torrents
+
+ def __bytes_to_gb(self, size_in_bytes: int) -> float:
+ """
+ 将字节单位的大小转换为千兆字节(GB)。
+
+ :param size_in_bytes: 文件大小,单位为字节。
+ :return: 文件大小,单位为千兆字节(GB)。
+ """
+ return size_in_bytes / (1024 ** 3)
+
+ def __is_number_or_range(self, value):
+ """
+ 检查字符串是否表示单个数字或数字范围(如'5'或'5-10')
+ """
+ return bool(re.match(r"^\d+(-\d+)?$", value))
+
+ def __is_number(self, value):
+ """
+ 检查给定的值是否可以被转换为数字(整数或浮点数)
+ """
+ try:
+ float(value)
+ return True
+ except ValueError:
+ return False
+
+ def __calculate_seeding_torrents_size(self, torrent_tasks: Dict[str, dict]) -> int:
+ """
+ 计算保种种子体积
+ """
+ return sum(task.get("size", 0) for task in torrent_tasks.values() if not task.get("deleted", False))
+
+ def __archive_tasks(self):
+ """
+ 归档已经删除的种子数据
+ """
+ torrent_tasks: Dict[str, dict] = self.get_data("torrents") or {}
+
+ # 用于存储已删除的数据
+ archived_tasks: Dict[str, dict] = self.get_data("archived") or {}
+
+ # 准备一个列表,记录所有需要从原始数据中删除的键
+ keys_to_delete = []
+
+ # 遍历所有 torrent 条目
+ for key, value in torrent_tasks.items():
+ # 检查是否标记为已删除
+ if value.get("deleted"):
+ # 如果是,加入到归档字典中
+ archived_tasks[key] = value
+ # 记录键,稍后删除
+ keys_to_delete.append(key)
+
+ # 从原始字典中移除已删除的条目
+ for key in keys_to_delete:
+ del torrent_tasks[key]
+
+ self.save_data("archived", archived_tasks)
+ self.save_data("torrents", torrent_tasks)
+ # 归档需要更新一下统计数据
+ self.__update_and_save_statistic_info(torrent_tasks=torrent_tasks)
+
+ def __clear_tasks(self):
+ """
+ 清除统计数据(清理已删除和已归档的数据,保留活跃种子数据)
+ """
+ torrent_tasks: Dict[str, dict] = self.get_data("torrents") or {}
+
+ to_delete = [key for key, value in torrent_tasks.items() if value.get("deleted")]
+ for key in to_delete:
+ del torrent_tasks[key]
+
+ self.save_data("torrents", torrent_tasks)
+ self.save_data("archived", {})
+ self.save_data("unmanaged", {})
+ # 需要更新一下统计数据
+ self.__update_and_save_statistic_info(torrent_tasks=torrent_tasks)
+
+ def __get_statistic_info(self) -> Dict[str, dict]:
+ """
+ 获取统计数据
+ """
+ statistic_info = self.get_data("statistic") or {"count": 0, "deleted": 0, "uploaded": 0, "downloaded": 0, "unarchived": 0, "active": 0, "active_uploaded": 0, "active_downloaded": 0}
+ return statistic_info
+
+ def __get_demo_site_config(self) -> str:
+ desc = ("以下为配置示例,请参考 "
+ "https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md "
+ "进行配置,请注意,只需要保留实际配置内容(删除这段)\n")
+ config = """[{
+ "sitename": "测试站点",
+ "freeleech": "free",
+ "hr": "no",
+ "include": "",
+ "exclude": "",
+ "size": "10-500",
+ "seeder": "1",
+ "pubtime": "5-120",
+ "seed_time": 120,
+ "seed_ratio": "",
+ "seed_size": "",
+ "download_time": "",
+ "seed_avgspeed": "",
+ "seed_inactivetime": "",
+ "save_path": "/downloads/site1",
+ "proxy_download": false
+}]"""
+ return desc + config
+
+ def __is_valid_time_range(self, time_range: str) -> bool:
+ """检查时间范围字符串是否有效:格式为"HH:MM-HH:MM",且时间有效"""
+ if not time_range:
+ return False
+
+ # 使用正则表达式匹配格式
+ pattern = re.compile(r'^\d{2}:\d{2}-\d{2}:\d{2}$')
+ if not pattern.match(time_range):
+ return False
+
+ try:
+ start_str, end_str = time_range.split('-')
+ datetime.strptime(start_str, '%H:%M').time()
+ datetime.strptime(end_str, '%H:%M').time()
+ except Exception:
+ return False
+
+ return True
+
+ def __is_current_time_in_range(self) -> bool:
+ """判断当前时间是否在开启时间区间内"""
+
+ brush_config = self.__get_brush_config()
+ active_time_range = brush_config.active_time_range
+
+ if not self.__is_valid_time_range(active_time_range):
+ # 如果时间范围格式不正确或不存在,说明当前没有开启时间段,返回True
+ return True
+
+ start_str, end_str = active_time_range.split('-')
+ start_time = datetime.strptime(start_str, '%H:%M').time()
+ end_time = datetime.strptime(end_str, '%H:%M').time()
+ now = datetime.now().time()
+
+ if start_time <= end_time:
+ # 情况1: 时间段不跨越午夜
+ return start_time <= now <= end_time
+ else:
+ # 情况2: 时间段跨越午夜
+ return now >= start_time or now <= end_time
+
+ def __get_site_by_tracker(self, trackers: list[str]) -> Tuple[int, str]:
+ """
+ 根据tracker获取站点信息
+ """
+ if not trackers:
+ return (0, "未知")
+
+ # 特定tracker到域名的映射
+ tracker_mappings = {
+ "chdbits.xyz": "ptchdbits.co",
+ "agsvpt.trackers.work": "agsvpt.com",
+ "tracker.cinefiles.info": "audiences.me",
+ }
+
+ for tracker in trackers:
+ # 检查tracker是否包含特定的关键字,并进行相应的映射
+ for key, mapped_domain in tracker_mappings.items():
+ if key in tracker:
+ domain = mapped_domain
+ break
+ else:
+ # 使用StringUtils工具类获取tracker的域名
+ domain = StringUtils.get_url_domain(tracker)
+
+ site_info = self.siteshelper.get_indexer(domain)
+ if site_info:
+ return (site_info.get("id"), site_info.get("name"))
+
+ # 当找不到对应的站点信息时,返回一个默认值
+ return (0, domain)
+
+
\ No newline at end of file