from datetime import datetime, timedelta from threading import Event from typing import Any, List, Dict, Tuple import pytz from apscheduler.schedulers.background import BackgroundScheduler from app.chain.torrents import TorrentsChain from app.core.config import settings from app.helper.sites import SitesHelper from app.log import logger from app.plugins import _PluginBase from app.utils.string import StringUtils class BrushFlow(_PluginBase): # 插件名称 plugin_name = "站点刷流" # 插件描述 plugin_desc = "自动托管刷流,将会默认提高对应站点的种子刷新频率。" # 插件图标 plugin_icon = "fileupload.png" # 主题色 plugin_color = "#EC5665" # 插件版本 plugin_version = "1.0" # 插件作者 plugin_author = "jxxghp" # 作者主页 author_url = "https://github.com/jxxghp" # 插件配置项ID前缀 plugin_config_prefix = "brushflow_" # 加载顺序 plugin_order = 21 # 可使用的用户级别 auth_level = 3 # 私有属性 sites = None torrents = None # 添加种子定时 _cron = 10 # 检查种子定时 _check_interval = 5 # 退出事件 _event = Event() _scheduler = None _enabled = False _notify = True _onlyonce = False _brushsites = [] _downloader = "qbittorrent" _disksize = 0 _freeleech = "free" _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 = "" def init_plugin(self, config: dict = None): self.sites = SitesHelper() self.torrents = TorrentsChain() 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._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.stop_service() # 启动定时任务 & 立即运行一次 if self.get_state() or self._onlyonce: # 检查配置 if self._disksize and not StringUtils.is_number(self._disksize): logger.error(f"保种体积设置错误:{self._disksize}") self.systemmessage.put(f"保种体积设置错误:{self._disksize}") return if self._maxupspeed and not StringUtils.is_number(self._maxupspeed): logger.error(f"总上传带宽设置错误:{self._maxupspeed}") self.systemmessage.put(f"总上传带宽设置错误:{self._maxupspeed}") return if self._maxdlspeed and not StringUtils.is_number(self._maxdlspeed): logger.error(f"总下载带宽设置错误:{self._maxdlspeed}") self.systemmessage.put(f"总下载带宽设置错误:{self._maxdlspeed}") return if self._maxdlcount and not StringUtils.is_number(self._maxdlcount): logger.error(f"同时下载任务数设置错误:{self._maxdlcount}") self.systemmessage.put(f"同时下载任务数设置错误:{self._maxdlcount}") return if self._size and not StringUtils.is_number(self._size): logger.error(f"种子大小设置错误:{self._size}") self.systemmessage.put(f"种子大小设置错误:{self._size}") return if self._seeder and not StringUtils.is_number(self._seeder): logger.error(f"做种人数设置错误:{self._seeder}") self.systemmessage.put(f"做种人数设置错误:{self._seeder}") 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}") 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}") 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}") 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}") 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}") 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}") 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}") 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}") return # 检查必要条件 if not self._brushsites or not self._downloader: return # 启动任务 self._scheduler = BackgroundScheduler(timezone=settings.TZ) logger.info(f"站点刷流服务启动,周期:{self._cron}分钟") try: self._scheduler.add_job(self.brush, 'interval', minutes=self._cron) except Exception as e: logger.error(f"站点刷流服务启动失败:{e}") self.systemmessage(f"站点刷流服务启动失败:{e}") return 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)) # 关闭一次性开关 self._onlyonce = False self.update_config({ "onlyonce": False, "enabled": self._enabled, "notify": self._notify, "brushsites": self._brushsites, "downloader": self._downloader, "disksize": self._disksize, "freeleech": self._freeleech, "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 }) if self._scheduler.get_jobs(): # 增加检查任务 self._scheduler.add_job(self.check, 'interval', minutes=self._check_interval) # 启动服务 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 @staticmethod def get_command() -> List[Dict[str, Any]]: pass def get_api(self) -> List[Dict[str, Any]]: pass 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.sites.get_indexers()] return [ { 'component': 'VForm', 'content': [ { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, 'md': 4 }, 'content': [ { 'component': 'VSwitch', 'props': { 'model': 'enabled', 'label': '启用插件', } } ] }, { 'component': 'VCol', 'props': { 'cols': 12, 'md': 4 }, 'content': [ { 'component': 'VSwitch', 'props': { 'model': 'notify', 'label': '发送通知', } } ] }, { 'component': 'VCol', 'props': { 'cols': 12, 'md': 4 }, 'content': [ { 'component': 'VSwitch', 'props': { 'model': 'onlyonce', 'label': '立即运行一次', } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'content': [ { 'component': 'VSelect', 'props': { 'chips': True, 'multiple': True, 'model': 'brushsites', 'label': '刷流站点', 'items': site_options } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VSelect', 'props': { 'model': 'downloader', 'label': '下载器', 'items': [ {'title': 'Qbittorrent', 'value': 'qbittorrent'}, {'title': 'Transmission', 'value': 'transmission'} ] } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'disksize', 'label': '保种体积(GB)', 'placeholder': '达到后停止新增任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VSelect', 'props': { 'model': 'freeleech', 'label': '促销', 'items': [ {'title': '全部(包括普通)', 'value': ''}, {'title': '免费', 'value': 'free'}, {'title': '2X免费', 'value': '2xfree'}, ] } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'maxupspeed', 'label': '总上传带宽(KB/s)', 'placeholder': '达到后停止新增任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'maxdlspeed', 'label': '总下载带宽(KB/s)', 'placeholder': '达到后停止新增任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'maxdlcount', 'label': '同时下载任务数', 'placeholder': '达到后停止新增任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'include', 'label': '包含规则', 'placeholder': '支持正式表达式' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'exclude', 'label': '排除规则', 'placeholder': '支持正式表达式' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'size', 'label': '种子大小(GB)', 'placeholder': '如:5 或 5-10' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'seeder', 'label': '做种人数', 'placeholder': '如:5 或 5-10' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'pubtime', 'label': '发布时间(分钟)', 'placeholder': '如:5 或 5-10' } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'seed_time', 'label': '做种时间(小时)', 'placeholder': '达到后删除任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'seed_ratio', 'label': '分享率', 'placeholder': '达到后删除任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'seed_size', 'label': '上传量(GB)', 'placeholder': '达到后删除任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'download_time', 'label': '下载超时时间(小时)', 'placeholder': '达到后删除任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'seed_avgspeed', 'label': '平均上传速度(KB/s)', 'placeholder': '低于时删除任务' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'seed_inactivetime', 'label': '未活动时间(分钟) ', 'placeholder': '超过时删除任务' } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'up_speed', 'label': '单任务上传限速(KB/s)', 'placeholder': '种子上传限速' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'dl_speed', 'label': '单任务下载限速(KB/s)', 'placeholder': '种子下载限速' } } ] }, { 'component': 'VCol', 'props': { "cols": 12, "md": 4 }, 'content': [ { 'component': 'VTextField', 'props': { 'model': 'save_path', 'label': '保存目录', 'placeholder': '留空自动' } } ] } ] }, ] } ], { "enabled": False, "notify": True, "onlyonce": False, "freeleech": "free" } def get_page(self) -> List[dict]: pass def stop_service(self): """ 退出插件 """ try: if self._scheduler: self._scheduler.remove_all_jobs() if self._scheduler.running: self._event.set() self._scheduler.shutdown() self._event.clear() self._scheduler = None except Exception as e: print(str(e)) def brush(self): """ 执行刷流动作,添加下载任务 """ if not self._brushsites or not self._downloader: return logger.info(f"开始执行刷流任务 ...") logger.info(f"刷流任务执行完成") def check(self): """ 定时检查,删除下载任务 """ if not self._downloader: return logger.info(f"开始检查刷流下载任务 ...") logger.info(f"刷流下载任务检查完成")