From a06f46537f691de2858fd248707ec780d56a2e6b Mon Sep 17 00:00:00 2001 From: InfinityPacer Date: Tue, 28 Jul 2026 16:09:07 +0800 Subject: [PATCH] fix(trafficassistant): support BrushFlow V5 tasks --- package.v2.json | 3 +- plugins.v2/trafficassistant/__init__.py | 54 +++++- .../test_brushflow_compatibility.py | 177 ++++++++++++++++++ 3 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 tests/v2/trafficassistant/test_brushflow_compatibility.py diff --git a/package.v2.json b/package.v2.json index 71e8cdaa..d7a15858 100644 --- a/package.v2.json +++ b/package.v2.json @@ -234,11 +234,12 @@ "name": "站点流量管理", "description": "自动管理流量,保障站点分享率。", "labels": "站点,订阅,刷流", - "version": "1.6", + "version": "1.7", "icon": "https://raw.githubusercontent.com/InfinityPacer/MoviePilot-Plugins/main/icons/trafficassistant.png", "author": "InfinityPacer", "level": 2, "history": { + "v1.7": "兼容站点刷流 V5 任务开关,同时保留 V4 站点配置联动", "v1.6": "支持站点独立配置,可按站点覆盖分享率阈值和流量管理动作", "v1.5": "优化执行周期输入,需要MoviePilot v2.2.1+", "v1.4": "MoviePilot V2 版本站点流量管理插件" diff --git a/plugins.v2/trafficassistant/__init__.py b/plugins.v2/trafficassistant/__init__.py index a8a43bae..74d001ef 100644 --- a/plugins.v2/trafficassistant/__init__.py +++ b/plugins.v2/trafficassistant/__init__.py @@ -16,11 +16,12 @@ from app.db.systemconfig_oper import SystemConfigOper from app.log import logger from app.plugins import _PluginBase -from app.plugins.trafficassistant.trafficconfig import BaseConfig, TrafficConfig from app.scheduler import Scheduler from app.schemas import NotificationType from app.schemas.types import EventType, SystemConfigKey +from .trafficconfig import BaseConfig, TrafficConfig + lock = threading.Lock() @@ -32,7 +33,7 @@ class TrafficAssistant(_PluginBase): # 插件图标 plugin_icon = "https://raw.githubusercontent.com/InfinityPacer/MoviePilot-Plugins/main/icons/trafficassistant.png" # 插件版本 - plugin_version = "1.6" + plugin_version = "1.7" # 插件作者 plugin_author = "InfinityPacer" # 作者主页 @@ -832,13 +833,24 @@ def __update_subscription_sites(self, site_id: int, remove: bool) -> [bool, str] return action_performed, action_msg def __update_brush_sites(self, site_id: int, enable: bool, plugin_id: str) -> [bool, str]: - """更新或配置刷流插件站点""" + """按刷流插件配置契约更新目标站点的自动刷流状态""" plugin_config = self.get_config(plugin_id=plugin_id) if not plugin_config: action_msg = "刷流站点:获取插件配置失败" logger.warning(action_msg) return False, action_msg + tasks = plugin_config.get("tasks") + if isinstance(tasks, list): + config_needs_update, actions = self.__update_brush_tasks( + plugin_config=plugin_config, + site_id=site_id, + enable=enable, + ) + if config_needs_update: + self.update_config(config=plugin_config, plugin_id=plugin_id) + return config_needs_update, ",".join(actions) + actions = [] config_needs_update = False plugin_enabled = plugin_config.get("enabled", False) @@ -865,6 +877,42 @@ def __update_brush_sites(self, site_id: int, enable: bool, plugin_id: str) -> [b return config_needs_update, ",".join(actions) + @staticmethod + def __update_brush_tasks(plugin_config: dict, site_id: int, enable: bool) -> Tuple[bool, List[str]]: + """更新 V5 中目标站点的全部任务,并在启用任务时保证全局开关可用""" + matching_tasks = [ + task for task in plugin_config["tasks"] + if isinstance(task, dict) and task.get("site_id") == site_id + ] + if not matching_tasks: + action_msg = "刷流任务:未找到对应站点" + logger.warning(action_msg) + return False, [action_msg] + + actions = [] + config_needs_update = False + if enable and not plugin_config.get("enabled", False): + plugin_config["enabled"] = True + actions.append("刷流插件:已启用") + config_needs_update = True + + changed_count = 0 + for task in matching_tasks: + if bool(task.get("enabled", True)) != enable: + task["enabled"] = enable + changed_count += 1 + + if changed_count: + action = "启用" if enable else "暂停" + actions.append(f"刷流任务:已{action} {changed_count} 个") + config_needs_update = True + else: + actions.append("刷流任务:无需调整") + + for action_msg in actions: + logger.info(action_msg) + return config_needs_update, actions + def __reload_plugin(self, plugin_id: str): logger.info(f"准备热加载插件: {plugin_id}") diff --git a/tests/v2/trafficassistant/test_brushflow_compatibility.py b/tests/v2/trafficassistant/test_brushflow_compatibility.py new file mode 100644 index 00000000..93ff921b --- /dev/null +++ b/tests/v2/trafficassistant/test_brushflow_compatibility.py @@ -0,0 +1,177 @@ +"""站点流量管理对不同版本站点刷流配置契约的兼容性测试。""" + +from types import SimpleNamespace +from unittest.mock import patch + +from trafficassistant import TrafficAssistant +from trafficassistant.trafficconfig import TrafficConfig + + +def _update_brush_site(plugin_config: dict, site_id: int, enable: bool): + """调用刷流联动入口并返回保存配置的 mock。""" + plugin = object.__new__(TrafficAssistant) + with ( + patch.object(TrafficAssistant, "get_config", return_value=plugin_config), + patch.object(TrafficAssistant, "update_config") as update_config, + ): + result = plugin._TrafficAssistant__update_brush_sites( + site_id=site_id, + enable=enable, + plugin_id="BrushFlow", + ) + return result, update_config + + +def test_v4_enables_plugin_and_adds_site(): + config = {"enabled": False, "brushsites": [2]} + + (changed, _), update_config = _update_brush_site(config, site_id=1, enable=True) + + assert changed is True + assert config == {"enabled": True, "brushsites": [2, 1]} + update_config.assert_called_once_with(config=config, plugin_id="BrushFlow") + + +def test_v4_disables_only_matching_site(): + config = {"enabled": True, "brushsites": [1, 2]} + + (changed, _), update_config = _update_brush_site(config, site_id=1, enable=False) + + assert changed is True + assert config == {"enabled": True, "brushsites": [2]} + update_config.assert_called_once_with(config=config, plugin_id="BrushFlow") + + +def test_v5_enables_all_matching_tasks_and_global_switch(): + config = { + "enabled": False, + "tasks": [ + {"id": "site-1-a", "site_id": 1, "enabled": False}, + {"id": "site-2", "site_id": 2, "enabled": False}, + {"id": "site-1-b", "site_id": 1, "enabled": True}, + ], + } + + (changed, _), update_config = _update_brush_site(config, site_id=1, enable=True) + + assert changed is True + assert config["enabled"] is True + assert [task["enabled"] for task in config["tasks"]] == [True, False, True] + update_config.assert_called_once_with(config=config, plugin_id="BrushFlow") + + +def test_v5_disables_all_matching_tasks_without_disabling_plugin(): + config = { + "enabled": True, + "tasks": [ + {"id": "site-1-a", "site_id": 1, "enabled": True}, + {"id": "site-2", "site_id": 2, "enabled": True}, + {"id": "site-1-b", "site_id": 1, "enabled": False}, + ], + } + + (changed, _), update_config = _update_brush_site(config, site_id=1, enable=False) + + assert changed is True + assert config["enabled"] is True + assert [task["enabled"] for task in config["tasks"]] == [False, True, False] + update_config.assert_called_once_with(config=config, plugin_id="BrushFlow") + + +def test_v5_missing_site_does_not_enable_global_switch(): + config = { + "enabled": False, + "tasks": [{"id": "site-2", "site_id": 2, "enabled": False}], + } + + (changed, message), update_config = _update_brush_site(config, site_id=1, enable=True) + + assert changed is False + assert config["enabled"] is False + assert "未找到" in message + update_config.assert_not_called() + + +def _traffic_config(site_ids: list[int]) -> TrafficConfig: + """构造启用低分享率刷流动作的多站点配置。""" + return TrafficConfig( + ratio_lower_limit=1, + ratio_upper_limit=5, + enable_auto_brush_if_below=True, + brush_plugin="BrushFlow", + site_infos={ + site_id: SimpleNamespace(name=f"站点{site_id}") + for site_id in site_ids + }, + ) + + +def _site_statistics(site_ids: list[int]) -> dict: + """构造触发低分享率动作的站点统计。""" + return { + f"站点{site_id}": { + "success": True, + "ratio": 0.5, + "statistic_time": "2026-07-28", + } + for site_id in site_ids + } + + +def test_v5_batch_saves_each_changed_site_before_single_reload(): + traffic_config = _traffic_config([1, 2]) + brush_config = { + "enabled": True, + "tasks": [ + {"id": "site-1", "site_id": 1, "enabled": False}, + {"id": "site-2", "site_id": 2, "enabled": False}, + ], + } + operations = [] + plugin = object.__new__(TrafficAssistant) + plugin._traffic_config = traffic_config + + with ( + patch.object(TrafficAssistant, "get_config", return_value=brush_config), + patch.object( + TrafficAssistant, + "update_config", + side_effect=lambda **_kwargs: operations.append("save"), + ), + patch.object( + TrafficAssistant, + "_TrafficAssistant__reload_plugin", + side_effect=lambda **_kwargs: operations.append("reload"), + ), + ): + plugin._TrafficAssistant__auto_traffic( + traffic_config=traffic_config, + site_statistics=_site_statistics([1, 2]), + ) + + assert operations == ["save", "save", "reload"] + assert [task["enabled"] for task in brush_config["tasks"]] == [True, True] + + +def test_v5_batch_does_not_reload_without_matching_task(): + traffic_config = _traffic_config([3]) + brush_config = { + "enabled": False, + "tasks": [{"id": "site-1", "site_id": 1, "enabled": False}], + } + plugin = object.__new__(TrafficAssistant) + plugin._traffic_config = traffic_config + + with ( + patch.object(TrafficAssistant, "get_config", return_value=brush_config), + patch.object(TrafficAssistant, "update_config") as update_config, + patch.object(TrafficAssistant, "_TrafficAssistant__reload_plugin") as reload_plugin, + ): + plugin._TrafficAssistant__auto_traffic( + traffic_config=traffic_config, + site_statistics=_site_statistics([3]), + ) + + assert brush_config["enabled"] is False + update_config.assert_not_called() + reload_plugin.assert_not_called()