From c6c6cc033097b7f3b530b69c27e9ebb0729038c1 Mon Sep 17 00:00:00 2001 From: Allen Date: Fri, 12 Apr 2024 15:36:30 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E3=80=90=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=87=AA=E5=8A=A8=E5=8D=87=E7=BA=A7=E3=80=91=E6=8F=92?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/PluginAutoUpgrade.png | Bin 0 -> 50208 bytes package.json | 8 + plugins/pluginautoupgrade/__init__.py | 469 ++++++++++++++++++++++++++ 3 files changed, 477 insertions(+) create mode 100644 icons/PluginAutoUpgrade.png create mode 100644 plugins/pluginautoupgrade/__init__.py diff --git a/icons/PluginAutoUpgrade.png b/icons/PluginAutoUpgrade.png new file mode 100644 index 0000000000000000000000000000000000000000..104c5b6053e93f11978bebc6765d82124de2e597 GIT binary patch literal 50208 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 bool: + """ + 获取插件状态 + """ + state = True if self.__get_config_item(config_key='enable') \ + and self.__get_config_item(config_key='cron') \ + else False + return state + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + pass + + def get_api(self) -> List[Dict[str, Any]]: + """ + 获取插件API + """ + pass + + def get_service(self) -> List[Dict[str, Any]]: + """ + 注册插件公共服务 + """ + try: + if self.get_state(): + cron = self.__get_config_item(config_key='cron') + return [{ + "id": "PluginAutoUpgradeTimerService", + "name": f"{self.plugin_name}定时服务", + "trigger": CronTrigger.from_crontab(cron), + "func": self.__try_run, + "kwargs": {} + }] + else: + return [] + except Exception as e: + logger.error(f"注册插件公共服务异常: {str(e)}", exc_info=True) + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + # 建议的配置 + config_suggest = {} + # 合并默认配置 + config_suggest.update(self.__config_default) + # 定时周期 + cron = self.__config_default.get('cron') + # 已安装的在线插件下拉框数据 + installed_online_plugin_options = self.__get_installed_online_plugin_options() + form = [{ + 'component': 'VForm', + 'content': [{ # 业务无关总控 + 'component': 'VRow', + 'content': [{ + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSwitch', + 'props': { + 'model': 'enable', + 'label': '启用插件', + 'hint': '插件总开关' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSwitch', + 'props': { + 'model': 'enable_notify', + 'label': '发送通知', + 'hint': '执行插件任务后是否发送通知' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSwitch', + 'props': { + 'model': 'run_once', + 'label': '立即运行一次', + 'hint': '保存插件配置后是否立即触发一次插件任务运行' + } + }] + }] + }, { + 'component': 'VRow', + 'content': [{ + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '定时执行周期', + 'placeholder': cron, + 'hint': f'设置插件任务执行周期。支持5位cron表达式,应避免任务执行过于频繁,缺省时为:【{cron}】' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSelect', + 'props': { + 'model': 'include_plugins', + 'label': '包含的插件', + 'multiple': True, + 'chips': True, + 'items': installed_online_plugin_options, + 'hint': '选择哪些插件需要自动升级,不选时默认全部已安装插件。' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSelect', + 'props': { + 'model': 'exclude_plugins', + 'label': '排除的插件', + 'multiple': True, + 'chips': True, + 'items': installed_online_plugin_options, + 'hint': '选择哪些插件需要排除升级(在【包含的插件】的基础上排除),不选时默认不排除。' + } + }] + }] + }] + }] + return form, config_suggest + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + try: + logger.info('尝试停止插件服务...') + self.__exit_event.set() + self.__stop_scheduler() + logger.info('插件服务停止成功') + except Exception as e: + logger.error(f"插件服务停止异常: {str(e)}", exc_info=True) + finally: + self.__exit_event.clear() + + def __get_config_item(self, config_key: str, use_default: bool = True) -> Any: + """ + 获取插件配置项 + :param config_key: 配置键 + :param use_default: 是否使用缺省值 + :return: 配置值 + """ + if not config_key: + return None + config = self.__config if self.__config else {} + config_value = config.get(config_key) + if config_value is None and use_default: + config_default = self.__config_default if self.__config_default else {} + config_value = config_default.get(config_key) + return config_value + + @classmethod + def __get_local_plugins(cls) -> List[schemas.Plugin]: + """ + 获取所有本地插件信息 + """ + local_plugins = cls.__plugin_manager.get_local_plugins() + return local_plugins + + @classmethod + def __get_installed_local_plugins(cls) -> List[schemas.Plugin]: + """ + 获取所有已安装的本地插件信息 + """ + local_plugins = cls.__get_local_plugins() + installed_local_plugins = [local_plugin for local_plugin in local_plugins if local_plugin and local_plugin.installed] + return installed_local_plugins + + @classmethod + def __get_installed_local_plugin(cls, plugin_id: str) -> List[schemas.Plugin]: + """ + 获取指定的已安装的本地插件信息 + """ + if not plugin_id: + return None + # 已安装的本地插件 + installed_plugins = cls.__get_installed_local_plugins() + for installed_plugin in installed_plugins: + if installed_plugin and installed_plugin.id and installed_plugin.id == plugin_id: + return installed_plugin + return None + + @classmethod + def __get_online_plugins(cls) -> List[schemas.Plugin]: + """ + 获取所有在线插件 + """ + online_plugins = cls.__plugin_manager.get_online_plugins() + return online_plugins + + @classmethod + def __get_installed_online_plugins(cls) -> List[schemas.Plugin]: + """ + 获取所有已安装的在线插件 + """ + online_plugins = cls.__get_online_plugins() + installed_online_plugins = [online_plugin for online_plugin in online_plugins if online_plugin and online_plugin.installed] + return installed_online_plugins + + @classmethod + def __get_installed_online_plugin_options(cls) -> Dict[str, Any]: + """ + 获取所有已安装的在线插件的选项数据 + """ + installed_online_plugin_options = [] + installed_online_plugins = cls.__get_installed_online_plugins() + for installed_online_plugin in installed_online_plugins: + if not installed_online_plugin: + continue + installed_online_plugin_options.append({ + 'value': installed_online_plugin.id, + 'title': installed_online_plugin.plugin_name + }) + return installed_online_plugin_options + + @classmethod + def __get_has_update_online_plugins(cls) -> List[schemas.Plugin]: + """ + 获取所有可升级的在线插件 + """ + installed_online_plugins = cls.__get_installed_online_plugins() + if not installed_online_plugins: + return None + has_update_online_plugins = [installed_online_plugin for installed_online_plugin in installed_online_plugins if installed_online_plugin and installed_online_plugin.has_update] + return has_update_online_plugins + + def __start_scheduler(self, timezone=None) -> bool: + """ + 启动调度器 + :param timezone: 时区 + """ + try: + if not self.__scheduler: + if not timezone: + timezone = settings.TZ + self.__scheduler = BackgroundScheduler(timezone=timezone) + logger.debug(f"插件服务调度器初始化完成: timezone = {str(timezone)}") + if not self.__scheduler.running: + self.__scheduler.start() + logger.debug(f"插件服务调度器启动成功") + self.__scheduler.print_jobs() + return True + except Exception as e: + logger.error(f"插件服务调度器启动异常: {str(e)}", exc_info=True) + return False + + def __stop_scheduler(self): + """ + 停止调度器 + """ + try: + logger.info('尝试停止插件服务调度器...') + if self.__scheduler: + self.__scheduler.remove_all_jobs() + if self.__scheduler.running: + self.__scheduler.shutdown() + self.__scheduler = None + logger.info('插件服务调度器停止成功') + else: + logger.info('插件未启用服务调度器,无须停止') + except Exception as e: + logger.error(f"插件服务调度器停止异常: {str(e)}", exc_info=True) + + def __check_allow_upgrade(self, plugin_id: str) -> bool: + """ + 判断插件是否允许升级:包含、排除 + """ + if not plugin_id: + return False + exclude_plugins = self.__get_config_item('exclude_plugins') + if exclude_plugins and plugin_id in exclude_plugins: + return False + include_plugins = self.__get_config_item('include_plugins') + if not include_plugins or plugin_id in include_plugins: + return True + else: + return False + + def __try_run(self): + """ + 尝试运行插件任务 + """ + if not self.__task_lock.acquire(blocking=False): + logger.info('已有进行中的任务,本次不执行') + return + try: + self.__run() + finally: + self.__task_lock.release() + + def __run(self): + """" + 运行插件任务 + """ + self.__upgrade_batch() + + def __upgrade_batch(self): + """ + 批量升级 + """ + has_update_online_plugins = self.__get_has_update_online_plugins() + upgrade_results = [] + for has_update_online_plugin in has_update_online_plugins: + upgrade_result = self.__upgrade_single(has_update_online_plugin) + if upgrade_result: + upgrade_results.append(upgrade_result) + self.__send_notify(results=upgrade_results) + + def __upgrade_single(self, online_plugin: schemas.Plugin) -> Dict[str, Any]: + """ + 单个升级 + """ + if not online_plugin or not online_plugin.has_update or not online_plugin.id or not online_plugin.repo_url or not self.__check_allow_upgrade(plugin_id=online_plugin.id): + return None + installed_local_plugin = self.__get_installed_local_plugin(plugin_id=online_plugin.id) + if not installed_local_plugin: + return None + response = install(plugin_id=online_plugin.id, repo_url=online_plugin.repo_url, force=True) + logger.info(f"插件升级结果: plugin_name = {online_plugin.plugin_name}, plugin_version = v{installed_local_plugin.plugin_version} -> v{online_plugin.plugin_version}, success = {response.success}, message = {response.message}") + return { + 'success': response.success, + 'message': response.message, + 'plugin_id': online_plugin.id, + 'plugin_name': online_plugin.plugin_name, + 'new_plugin_version': online_plugin.plugin_version, + 'old_plugin_version': installed_local_plugin.plugin_version + } + + def __send_notify(self, results: List[Dict[str, Any]]): + """ + 发送通知 + :param results: 插件升级结果 + """ + if not results or not self.__get_config_item('enable_notify'): + return + text = self.__build_notify_message(results=results) + if not text: + return + self.post_message(title=f'{self.plugin_name}任务执行结果', text=text) + + @staticmethod + def __build_notify_message(results: List[Dict[str, Any]]) -> str: + """ + 构建通知消息内容 + """ + text = '' + if not results: + return text + for result in results: + if not result: + continue + if result.get('success'): + text += f"{result.get('plugin_name')}升级[v{result.get('old_plugin_version')} -> v{result.get('new_plugin_version')}]成功\n" + else: + text += f"{result.get('plugin_name')}升级[v{result.get('old_plugin_version')} -> v{result.get('new_plugin_version')}]失败:{result.get('message')}\n" + return text From 0c24b1fcf8b240e62a44c64745f10d5a839b2a25 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:16:40 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix=20brushflow=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=A7=8D=E5=AD=90=E8=A7=84=E5=88=99=E8=B0=83?= =?UTF-8?q?=E6=95=B4=EF=BC=8C=E7=AB=99=E7=82=B9=E7=8B=AC=E7=AB=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96=E3=80=81=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=BC=98=E5=8C=96=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- plugins/brushflow/__init__.py | 252 ++++++++++++++++++++++------------ 2 files changed, 170 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index 5c34843..94c0fbb 100644 --- a/package.json +++ b/package.json @@ -238,11 +238,12 @@ "BrushFlow": { "name": "站点刷流", "description": "自动托管刷流,将会提高对应站点的访问频率。", - "version": "2.6", + "version": "2.7", "icon": "brush.jpg", "author": "jxxghp,InfinityPacer", "level": 2, "history": { + "v2.7": "动态删除种子规则调整(请注意查阅插件文档),站点独立配置样式优化、日志优化,修复部分配置项无法配置小数的问题,修复部分场景可能导致重复下载的问题", "v2.6": "修复排除订阅功能", "v2.5": "增加H&R做种时间、下载器监控配置项,刷流前置条件逻辑调整,代理下载种子默认为关闭" } diff --git a/plugins/brushflow/__init__.py b/plugins/brushflow/__init__.py index 03df5f7..2a865cc 100644 --- a/plugins/brushflow/__init__.py +++ b/plugins/brushflow/__init__.py @@ -85,7 +85,11 @@ class BrushConfig: def __initialize_site_config(self): if not self.site_config: + logger.error(f"没有设置站点配置,已关闭站点独立配置并恢复默认配置示例,请检查配置项") + self.site_config = self.__get_demo_site_config() self.group_site_configs = {} + self.enable_site_config = False + return # 定义允许覆盖的字段列表 allowed_fields = { @@ -130,6 +134,47 @@ class BrushConfig: self.enable_site_config = False self.enabled = False + @staticmethod + def __get_demo_site_config() -> str: + desc = ("//以下为配置示例,请参考 " + "https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md " + "进行配置,请注意,只需要保留实际配置内容(删除这段)\n") + config = """[{ + "sitename": "站点1", + "seed_time": 96, + "hr_seed_time": 144 +}, { + "sitename": "站点2", + "hr": "yes", + "size": "10-500", + "seeder": "5-10", + "pubtime": "5-120", + "seed_time": 96, + "save_path": "/downloads/site2", + "proxy_download": true, + "hr_seed_time": 144 +}, { + "sitename": "站点3", + "freeleech": "free", + "hr": "yes", + "include": "", + "exclude": "", + "size": "10-500", + "seeder": "1", + "pubtime": "5-120", + "seed_time": 120, + "hr_seed_time": 144, + "seed_ratio": "", + "seed_size": "", + "download_time": "", + "seed_avgspeed": "", + "seed_inactivetime": "", + "save_path": "/downloads/site1", + "proxy_download": false, + "proxy_delete": false +}]""" + return desc + config + def get_site_config(self, sitename): """ 根据站点名称获取特定的BrushConfig实例。如果没有找到站点特定的配置,则返回全局的BrushConfig实例。 @@ -192,7 +237,7 @@ class BrushFlow(_PluginBase): # 插件图标 plugin_icon = "brush.jpg" # 插件版本 - plugin_version = "2.6" + plugin_version = "2.7" # 插件作者 plugin_author = "jxxghp,InfinityPacer" # 作者主页 @@ -239,10 +284,6 @@ class BrushFlow(_PluginBase): logger.info("站点刷流任务出错,无法获取插件配置") return False - # 如果没有站点配置时,增加默认的配置项 - if not config.get("site_config"): - config["site_config"] = self.__get_demo_site_config() - # 如果配置校验没有通过,那么这里修改配置文件后退出 if not self.__validate_and_fix_config(config=config): self._brush_config = BrushConfig(config=config) @@ -941,12 +982,6 @@ class BrushFlow(_PluginBase): } ] }, - { - 'component': 'VRow', - 'content': [ - - ] - }, { 'component': 'VRow', 'content': [ @@ -1148,7 +1183,7 @@ class BrushFlow(_PluginBase): "component": "VDialog", "props": { "model": "dialog_closed", - "max-width": "80rem", + "max-width": "65rem", "overlay-class": "v-dialog--scrollable v-overlay--scroll-blocked", "content-class": "v-card v-card--density-default v-card--variant-elevated rounded-t" }, @@ -1174,12 +1209,12 @@ class BrushFlow(_PluginBase): }, 'content': [ { - "component": "VTextarea", + "component": "VAceEditor", "props": { - "model": "site_config", - "placeholder": "请输入站点配置", - "label": "站点配置", - "rows": 16 + 'modelvalue': 'site_config', + 'lang': 'json', + 'theme': 'monokai', + 'style': 'height: 30rem', } } ] @@ -1747,12 +1782,14 @@ class BrushFlow(_PluginBase): size_condition_passed, reason = self.__evaluate_size_condition_for_brush(torrents_size=torrents_size) self.__log_brush_conditions(passed=size_condition_passed, reason=reason) if not size_condition_passed: + logger.info(f"刷流任务执行完成") return # 判断能否通过刷流前置条件 pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush() self.__log_brush_conditions(passed=pre_condition_passed, reason=reason) if not pre_condition_passed: + logger.info(f"刷流任务执行完成") return statistic_info = self.__get_statistic_info() @@ -1823,7 +1860,7 @@ class BrushFlow(_PluginBase): for torrent in torrents: # 判断能否通过刷流前置条件 pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(include_network_conditions=False) - self.__log_brush_conditions(passed=pre_condition_passed, reason=reason, torrent=torrent) + self.__log_brush_conditions(passed=pre_condition_passed, reason=reason) if not pre_condition_passed: return False @@ -1975,10 +2012,19 @@ class BrushFlow(_PluginBase): """ brush_config = self.__get_brush_config(torrent.site_name) + # 排除重复种子 + # 默认根据标题和站点名称进行排除 task_key = f"{torrent.site_name}{torrent.title}" if any(task_key == f"{task.get('site_name')}{task.get('title')}" for task in torrent_tasks.values()): return False, "重复种子" + # 部分站点标题会上新时携带后缀,这里进一步根据种子详情地址进行排除 + if torrent.page_url: + task_page_url = f"{torrent.site_name}{torrent.page_url}" + if any(task_page_url == f"{task.get('site_name')}{task.get('page_url')}" for task in + torrent_tasks.values()): + return False, "重复种子" + # 促销条件 if brush_config.freeleech and torrent.downloadvolumefactor != 0: return False, "非免费种子" @@ -2011,7 +2057,7 @@ class BrushFlow(_PluginBase): # 做种人数 if brush_config.seeder: - seeders_range = [int(n) for n in brush_config.seeder.split("-")] + seeders_range = [float(n) for n in brush_config.seeder.split("-")] # 检查是否仅指定了一个数字,即做种人数需要小于等于该数字 if len(seeders_range) == 1: # 当做种人数大于该数字时,不符合条件 @@ -2027,7 +2073,7 @@ class BrushFlow(_PluginBase): pubdate_minutes = self.__get_pubminutes(torrent.pubdate) pubdate_minutes = self.__adjust_site_pubminutes(pubdate_minutes, torrent) if brush_config.pubtime: - pubtimes = [int(n) for n in brush_config.pubtime.split("-")] + pubtimes = [float(n) for n in brush_config.pubtime.split("-")] if len(pubtimes) == 1: # 单个值:选择发布时间小于等于该值的种子 if pubdate_minutes > pubtimes[0]: @@ -2045,7 +2091,7 @@ class BrushFlow(_PluginBase): """ if not passed: if not torrent: - logger.warn(f"种子没有通过前置刷流条件校验,原因:{reason}") + logger.warn(f"没有通过前置刷流条件校验,原因:{reason}") else: brush_config = self.__get_brush_config() if brush_config.log_more: @@ -2112,7 +2158,7 @@ class BrushFlow(_PluginBase): need_delete_hashes = [] - # 如果配置了删种阈值,则根据动态删种进行分组处理 + # 如果配置了动态删除以及删种阈值,则根据动态删种进行分组处理 if brush_config.proxy_delete and brush_config.delete_size_range: logger.info("已开启动态删种,按系统默认动态删种条件开始检查任务") proxy_delete_hashs = self.__delete_torrent_for_proxy(torrents=check_torrents, @@ -2300,6 +2346,22 @@ class BrushFlow(_PluginBase): return True, reason if not hit_and_run else "H&R种子(未设置H&R条件)," + reason + def __evaluate_proxy_pre_conditions_for_delete(self, site_name: str, torrent_info: dict) -> Tuple[bool, str]: + """ + 评估动态删除前置条件并返回是否应删除种子及其原因 + """ + brush_config = self.__get_brush_config(sitename=site_name) + + reason = "未能满足动态删除设置的前置删除条件" + + if brush_config.download_time and torrent_info.get("downloaded") < torrent_info.get( + "total_size") and torrent_info.get("dltime") >= float(brush_config.download_time) * 3600: + reason = f"下载耗时 {torrent_info.get('dltime') / 3600:.1f} 小时,大于 {brush_config.download_time} 小时" + else: + return False, reason + + return True, reason + def __delete_torrent_for_evaluate_conditions(self, torrents: List[Any], torrent_tasks: Dict[str, dict], proxy_delete: bool = False) -> List: """ @@ -2336,11 +2398,54 @@ class BrushFlow(_PluginBase): return delete_hashs - def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List: + def __delete_torrent_for_evaluate_proxy_pre_conditions(self, torrents: List[Any], + torrent_tasks: Dict[str, dict]) -> List: """ - 支持动态删除种子,当设置了动态删种(全局)和删除阈值时,当做种体积达到删除阈值时,优先按设置规则进行删除,若还没有达到阈值,则排除HR种子后按加入时间倒序进行删除 - 删除阈值:100,当做种体积 > 100G 时,则开始删除种子,直至降低至 100G - 删除阈值:50-100,当做种体积 > 100G 时,则开始删除种子,直至降至为 50G + 根据动态删除前置条件排除H&R种子后删除种子并获取已删除列表 + """ + brush_config = self.__get_brush_config() + delete_hashs = [] + + for torrent in torrents: + torrent_hash = self.__get_hash(torrent) + torrent_task = torrent_tasks.get(torrent_hash, None) + # 如果找不到种子任务,说明不在管理的种子范围内,直接跳过 + if not torrent_task: + continue + + # 如果是H&R种子,前置条件中不进行处理 + if torrent_task.get('hit_and_run', False): + continue + + site_name = torrent_task.get("site_name", "") + torrent_title = torrent_task.get("title", "") + torrent_desc = torrent_task.get("description", "") + + torrent_info = self.__get_torrent_info(torrent) + + # 删除种子的具体实现可能会根据实际情况略有不同 + should_delete, reason = self.__evaluate_proxy_pre_conditions_for_delete(site_name=site_name, + torrent_info=torrent_info) + if should_delete: + delete_hashs.append(torrent_hash) + self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, + reason=reason) + logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}") + else: + if brush_config.log_more: + logger.info(f"站点:{site_name},{reason},不删除种子:{torrent_title}|{torrent_desc}") + + return delete_hashs + + def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List: + """ + 动态删除种子,删除规则如下; + - 不管做种体积是否超过设定的动态删除阈值,默认优先执行排除H&R种子后满足「下载超时时间」的种子 + - 上述规则执行完成后,当做种体积依旧超过设定的动态删除阈值时,继续执行下述种子删除规则 + - 优先删除满足用户设置删除规则的全部种子,即便在删除过程中已经低于了阈值下限,也会继续删除 + - 若删除后还没有达到阈值,则在已完成种子中排除H&R种子后按做种时间倒序进行删除 + - 动态删除阈值:100,当做种体积 > 100G 时,则开始删除种子,直至降低至 100G + - 动态删除阈值:50-100,当做种体积 > 100G 时,则开始删除种子,直至降至为 50G """ brush_config = self.__get_brush_config() @@ -2348,29 +2453,49 @@ class BrushFlow(_PluginBase): if not (brush_config.proxy_delete and brush_config.delete_size_range): return [] + # 获取种子信息Map + torrent_info_map = {self.__get_hash(torrent): self.__get_torrent_info(torrent=torrent) for torrent in torrents} + + # 计算当前总做种体积 + total_torrent_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks) + + logger.info( + f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,正在准备计算满足动态前置删除条件的种子") + + # 执行排除H&R种子后满足前置删除条件的种子 + pre_delete_hashes = self.__delete_torrent_for_evaluate_proxy_pre_conditions(torrents=torrents, + torrent_tasks=torrent_tasks) or [] + + # 如果存在前置删除种子,这里进行额外判断,总做种体积排除前置删除种子的体积 + if pre_delete_hashes: + pre_delete_total_size = sum(torrent_info_map[self.__get_hash(torrent)].get("total_size", 0) + for torrent in torrents if self.__get_hash(torrent) in pre_delete_hashes) + total_torrent_size = total_torrent_size - pre_delete_total_size + torrents = [torrent for torrent in torrents if self.__get_hash(torrent) not in pre_delete_hashes] + logger.info( + f"满足动态删除前置条件的种子共 {len(pre_delete_hashes)} 个,体积 {self.__bytes_to_gb(pre_delete_total_size):.1f} GB," + f"删除种子后,当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB") + else: + logger.info(f"没有找到任何满足动态删除前置条件的种子") + # 解析删除阈值范围 sizes = [float(size) * 1024 ** 3 for size in brush_config.delete_size_range.split("-")] min_size = sizes[0] # 至少需要达到的做种体积 max_size = sizes[1] if len(sizes) > 1 else sizes[0] # 触发删除操作的做种体积上限 - torrent_info_map = {self.__get_hash(torrent): self.__get_torrent_info(torrent=torrent) for torrent in torrents} - - # 计算当前总做种体积 - # total_torrent_size = sum(info.get("total_size", 0) for info in torrent_info_map.values()) - total_torrent_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks) - # 当总体积未超过最大阈值时,不需要执行删除操作 if total_torrent_size < max_size: logger.info( f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB," - f"下限 {self.__bytes_to_gb(min_size):.1f} GB,未触发动态删除") - return [] + f"下限 {self.__bytes_to_gb(min_size):.1f} GB,未进一步触发动态删除") + return pre_delete_hashes or [] else: logger.info( f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB," - f"下限 {self.__bytes_to_gb(min_size):.1f} GB,触发动态删除") + f"下限 {self.__bytes_to_gb(min_size):.1f} GB,进一步触发动态删除") need_delete_hashes = [] + need_delete_hashes.extend(pre_delete_hashes) # 即使开了动态删除,但是也有可能部分站点单独设置了关闭,这里根据种子托管进行分组,先处理不需要托管的种子,按设置的规则进行删除 proxy_delete_torrents, not_proxy_delete_torrents = self.__group_torrents_by_proxy_delete(torrents=torrents, @@ -2395,7 +2520,6 @@ class BrushFlow(_PluginBase): self.__get_hash(torrent) in proxy_delete_hashes) # 在完成初始删除步骤后,如果总体积仍然超过最小阈值,则进一步找到已完成种子并排除HR种子后按做种时间正序进行删除 - sites_names = set() if total_torrent_size > min_size: # 重新计算当前的种子列表,排除已删除的种子 remaining_hashes = list( @@ -2428,14 +2552,15 @@ class BrushFlow(_PluginBase): torrent_desc = torrent_task.get("description", "") seeding_time = torrent_task.get("seeding_time", 0) if seeding_time: - sites_names.add(site_name) reason = f"触发动态删除,做种时间 {seeding_time / 3600:.1f} 小时,系统自动删除" self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, reason=reason) logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}") - msg = (f"站点:{','.join(sites_names)}\n内容:已完成 {len(need_delete_hashes)} 个种子删除," + delete_sites = {torrent_tasks[hash_key].get('site_name', '') for hash_key in need_delete_hashes if + hash_key in torrent_tasks} + msg = (f"站点:{','.join(delete_sites)}\n内容:已完成 {len(need_delete_hashes)} 个种子删除," f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB\n原因:触发动态删除") logger.info(msg) self.__send_message(title="【刷流任务状态更新】", text=msg) @@ -2817,7 +2942,7 @@ class BrushFlow(_PluginBase): if response and response.ok: torrent_content = response.content else: - logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载') + logger.error('尝试通过MP下载种子失败,继续尝试传递种子地址到下载器进行下载') if torrent_content: state = self.qb.add_torrent(content=torrent_content, download_dir=download_dir, @@ -2832,7 +2957,7 @@ class BrushFlow(_PluginBase): torrent_hash = self.qb.get_torrent_id_by_tag(tags=tag) if not torrent_hash: logger.error(f"{brush_config.downloader} 获取种子Hash失败" - f"{',请尝试开启代理下载种子' if not brush_config.proxy_download else ''}") + f"{',请尝试启用「代理下载种子」配置项' if not brush_config.proxy_download else ''}") return None return torrent_hash return None @@ -2848,7 +2973,7 @@ class BrushFlow(_PluginBase): if response and response.ok: torrent_content = response.content else: - logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载') + logger.error('尝试通过MP下载种子失败,继续尝试传递种子地址到下载器进行下载') if torrent_content: torrent = self.tr.add_torrent(content=torrent_content, download_dir=download_dir, @@ -3364,9 +3489,9 @@ class BrushFlow(_PluginBase): @staticmethod def __is_number_or_range(value): """ - 检查字符串是否表示单个数字或数字范围(如'5'或'5-10') + 检查字符串是否表示单个数字或数字范围(如'5', '5.5', '5-10' 或 '5.5-10.2') """ - return bool(re.match(r"^\d+(-\d+)?$", value)) + return bool(re.match(r"^\d+(\.\d+)?(-\d+(\.\d+)?)?$", value)) @staticmethod def __is_number(value): @@ -3442,47 +3567,6 @@ class BrushFlow(_PluginBase): } return statistic_info - @staticmethod - def __get_demo_site_config() -> str: - desc = ("以下为配置示例,请参考 " - "https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md " - "进行配置,请注意,只需要保留实际配置内容(删除这段)\n") - config = """[{ - "sitename": "站点1", - "seed_time": 96, - "hr_seed_time": 144 - }, { - "sitename": "站点2", - "hr": "yes", - "size": "10-500", - "seeder": "5-10", - "pubtime": "5-120", - "seed_time": 96, - "save_path": "/downloads/site2", - "proxy_download": true, - "hr_seed_time": 144 - }, { - "sitename": "站点3", - "freeleech": "free", - "hr": "yes", - "include": "", - "exclude": "", - "size": "10-500", - "seeder": "1", - "pubtime": "5-120", - "seed_time": 120, - "hr_seed_time": 144, - "seed_ratio": "", - "seed_size": "", - "download_time": "", - "seed_avgspeed": "", - "seed_inactivetime": "", - "save_path": "/downloads/site1", - "proxy_download": false, - "proxy_delete": false, - }]""" - return desc + config - @staticmethod def __is_valid_time_range(time_range: str) -> bool: """检查时间范围字符串是否有效:格式为"HH:MM-HH:MM",且时间有效""" From 15ed04b6593214de80786b352ab20921f8fb48bd Mon Sep 17 00:00:00 2001 From: ljmeng Date: Sun, 14 Apr 2024 05:03:21 +0800 Subject: [PATCH 3/6] feat: cross_seed jump stopped site --- package.json | 7 +++++-- plugins/crossseed/__init__.py | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9eeb504..e31b4d8 100644 --- a/package.json +++ b/package.json @@ -198,10 +198,13 @@ "CrossSeed": { "name": "青蛙辅种助手", "description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。", - "version": "2.1", + "version": "2.2", "icon": "qingwa.png", "author": "233@qingwa", - "level": 2 + "level": 2, + "history": { + "v2.2": "站点停用后会同步暂停对该站点的辅种" + } }, "VCBAnimeMonitor": { "name": "整理VCB动漫压制组作品", diff --git a/plugins/crossseed/__init__.py b/plugins/crossseed/__init__.py index b16e3e6..c309c60 100644 --- a/plugins/crossseed/__init__.py +++ b/plugins/crossseed/__init__.py @@ -177,11 +177,11 @@ class CrossSeed(_PluginBase): # 插件图标 plugin_icon = "qingwa.png" # 插件版本 - plugin_version = "2.1" + plugin_version = "2.2" # 插件作者 plugin_author = "233@qingwa" # 作者主页 - author_url = "https://new.qingwa.pro/" + author_url = "https://qingwapt.com/" # 插件配置项ID前缀 plugin_config_prefix = "cross_seed_" # 加载顺序 @@ -955,6 +955,11 @@ class CrossSeed(_PluginBase): # 逐个站点查询可辅种数据 chunk_size = 100 for site_config in self._site_cs_infos: + # 检查站点是否已经停用 + db_site = self.siteoper.get(site_config.id) + if db_site and not db_site.is_active: + logger.info(f"站点{site_config.name}已停用,跳过辅种") + return remote_tors: List[TorInfo] = [] total_size = len(pieces_hashes) for i in range(0, len(pieces_hashes), chunk_size): From 0aea5e8180ce5d8757edc635dd201e630ea40a42 Mon Sep 17 00:00:00 2001 From: ljmeng Date: Sun, 14 Apr 2024 06:07:34 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E6=94=B9=E4=B8=BA=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/crossseed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/crossseed/__init__.py b/plugins/crossseed/__init__.py index c309c60..c33fb51 100644 --- a/plugins/crossseed/__init__.py +++ b/plugins/crossseed/__init__.py @@ -959,7 +959,7 @@ class CrossSeed(_PluginBase): db_site = self.siteoper.get(site_config.id) if db_site and not db_site.is_active: logger.info(f"站点{site_config.name}已停用,跳过辅种") - return + continue remote_tors: List[TorInfo] = [] total_size = len(pieces_hashes) for i in range(0, len(pieces_hashes), chunk_size): From 211e2412a60bbe9ee9965efa36f6f478cbfe102c Mon Sep 17 00:00:00 2001 From: xuzhi Date: Mon, 15 Apr 2024 03:21:59 +0000 Subject: [PATCH 5/6] roll back removelink to v1.6 --- package.json | 5 +- plugins/removelink/__init__.py | 93 +++++++--------------------------- 2 files changed, 20 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index e31b4d8..096c56a 100644 --- a/package.json +++ b/package.json @@ -385,13 +385,12 @@ "RemoveLink": { "name": "清理硬链接", "description": "监控目录内文件被删除时,同步删除监控目录内所有和它硬链接的文件", - "version": "1.8", + "version": "1.6", "icon": "Ombi_A.png", "author": "DzAvril", "level": 1, "history": { - "v1.8": "修复空目录删除逻辑", - "v1.7": "增加定时清理空目录功能" + "v1.6": "提升插件性能" } }, "LinkMonitor": { diff --git a/plugins/removelink/__init__.py b/plugins/removelink/__init__.py index f741380..2bc66f0 100644 --- a/plugins/removelink/__init__.py +++ b/plugins/removelink/__init__.py @@ -11,7 +11,6 @@ from watchdog.observers import Observer from app.log import logger from app.plugins import _PluginBase from app.schemas import Notification, NotificationType -from app.utils.timer import TimerUtils state_lock = threading.Lock() @@ -91,7 +90,7 @@ class RemoveLink(_PluginBase): # 插件图标 plugin_icon = "Ombi_A.png" # 插件版本 - plugin_version = "1.8" + plugin_version = "1.6" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -175,40 +174,6 @@ class RemoveLink(_PluginBase): def get_api(self) -> List[Dict[str, Any]]: pass - def get_service(self) -> List[Dict[str, Any]]: - """ - 注册插件公共服务 - [{ - "id": "服务ID", - "name": "服务名称", - "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", - "func": self.xxx, - "kwargs": {} # 定时器参数 - }] - """ - if self._enabled: - # 随机时间 - triggers = TimerUtils.random_scheduler( - num_executions=1, - begin_hour=0, - end_hour=1, - min_interval=1, - max_interval=60, - ) - ret_jobs = [] - for trigger in triggers: - ret_jobs.append( - { - "id": f"RemoveLink|{trigger.hour}:{trigger.minute}", - "name": "清理空文件夹", - "trigger": "cron", - "func": self.delete_empty_folders, - "kwargs": {"hour": trigger.hour, "minute": trigger.minute}, - } - ) - return ret_jobs - return [] - def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: return [ { @@ -308,26 +273,26 @@ class RemoveLink(_PluginBase): ], }, { - "component": "VRow", - "content": [ + 'component': 'VRow', + 'content': [ { - "component": "VCol", - "props": { - "cols": 12, + 'component': 'VCol', + 'props': { + 'cols': 12, }, - "content": [ + 'content': [ { - "component": "VAlert", - "props": { - "type": "info", - "variant": "tonal", - "text": "监控目录如有多个需换行,源目录和硬链接目录都需要添加到监控目录中;如需实现删除硬链接时不删除源文件,可把源文件目录配置到不删除目录中。", - }, + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '监控目录如有多个需换行,源目录和硬链接目录都需要添加到监控目录中;如需实现删除硬链接时不删除源文件,可把源文件目录配置到不删除目录中。' + } } - ], + ] } - ], - }, + ] + } ], } ], { @@ -364,25 +329,6 @@ class RemoveLink(_PluginBase): return True return False - def delete_empty_folders(self): - """ - 删除空目录 - """ - for mon_path in self.monitor_dirs.split("\n"): - for subdir, dirs, files in os.walk(mon_path, topdown=False): - for dir in dirs: - dir_path = os.path.join(subdir, dir) - # 检查当前目录是否为空 - if not os.listdir(dir_path) and not self.__is_excluded(dir_path): - os.rmdir(dir_path) - logger.info(f"删除空目录:{dir_path}") - if self._notify: - self.post_message( - mtype=NotificationType.SiteMessage, - title=f"【清理硬链接】", - text=f"清理空文件夹:[{dir_path}]\n", - ) - def handle_deleted(self, file_path: Path): """ 处理删除事件 @@ -412,10 +358,7 @@ class RemoveLink(_PluginBase): mtype=NotificationType.SiteMessage, title=f"【清理硬链接】", text=f"监控到删除源文件:[{file_path}]\n" - f"同步删除硬链接文件:[{path}]", + f"同步删除硬链接文件:[{path}]", ) - except Exception as e: - logger.error( - "删除硬链接文件发生错误:%s - %s" % (str(e), traceback.format_exc()) - ) + logger.error("删除硬链接文件发生错误:%s - %s" % (str(e), traceback.format_exc())) From c0d6436ce5133e52890db4deb90e7eb7d216e653 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 16 Apr 2024 18:04:03 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c72cc4..f9e7cf0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ MoviePilot官方插件市场:https://github.com/jxxghp/MoviePilot-Plugins - 插件命名请勿与官方库插件中的插件冲突,否则会在MoviePilot版本升级时被官方插件覆盖。 ### 4. 依赖 -- 可在插件目录中放置`requirement.txt`文件,用于指定插件依赖的第三方库,MoviePilot会在插件安装时自动安装依赖库。 +- 可在插件目录中放置`requirements.txt`文件,用于指定插件依赖的第三方库,MoviePilot会在插件安装时自动安装依赖库。 ### 5. 界面开发 - 插件支持`插件配置`及`详情展示`两个展示页面,通过配置化的方式组装,使用 [Vuetify](https://vuetifyjs.com/) 组件库,所有该组件库有的组件都可以通过Json配置使用。