diff --git a/package.json b/package.json index 5dbb07f..0d8bcaa 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "BrushFlow": { "name": "站点刷流", "description": "自动托管刷流,将会提高对应站点的访问频率。", - "version": "1.6", + "version": "1.7", "icon": "brush.jpg", "author": "jxxghp", "level": 2 diff --git a/plugins/brushflow/__init__.py b/plugins/brushflow/__init__.py index d0227fb..1a29396 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 = "站点刷流" # 插件描述 @@ -34,9 +179,9 @@ class BrushFlow(_PluginBase): # 插件图标 plugin_icon = "brush.jpg" # 插件版本 - plugin_version = "1.6" + plugin_version = "1.7" # 插件作者 - 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': "
部分配置项以及细节请参考https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md
" + } + ] } ] }, @@ -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': "
注意:只有启用站点独立配置时,该配置项才会生效,详细配置参考https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md
" + } + ] + } + ] + } + ] + } + ] + } + ] } ] } @@ -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