Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 版本站点流量管理插件"
Expand Down
54 changes: 51 additions & 3 deletions plugins.v2/trafficassistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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"
# 作者主页
Expand Down Expand Up @@ -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)
Expand All @@ -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}")

Expand Down
177 changes: 177 additions & 0 deletions tests/v2/trafficassistant/test_brushflow_compatibility.py
Original file line number Diff line number Diff line change
@@ -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()