mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-06-02 23:16:46 +00:00
- 数据模型重构: 全面引入 Pydantic 模型(ClashConfig, Proxy, ProxyGroup 等)替代原有字典结构,提供更严格的数据验证与类型安全。 - 数据迁移机制: 新增 v2.1.0 数据升级脚本,支持将旧版代理、策略组及规则数据自动迁移至新架构。 - 配置补丁系统: 实现基于 JSON Patch 的细粒度配置修补机制,替代旧版覆盖逻辑,提升配置修改的灵活性。 - 服务层优化: 重写 ClashRuleProviderService 以适配新对象模型,增强代码可维护性与扩展性。 - API模型同步: 更新相关 API 数据模型以保持与内部数据结构的一致性。 - 用户界面: 批量规则管理和数据项隐藏支持
145 lines
5.6 KiB
Python
145 lines
5.6 KiB
Python
from typing import Any, Callable, Dict, List, Optional, Union, Iterator
|
|
|
|
from pydantic import TypeAdapter, ValidationError
|
|
|
|
from ..models.metadata import Metadata
|
|
from ..models.rule import Action, RoutingRuleType, MatchRule, ClashRule, LogicRule
|
|
from ..models.ruleitem import RuleItem, RuleData
|
|
|
|
|
|
class ClashRuleManager:
|
|
"""Clash rule manager"""
|
|
def __init__(self):
|
|
self.rules: List[RuleItem] = []
|
|
|
|
def import_rules(self, rules_list: List[Dict[str, Any]]):
|
|
self.rules.clear()
|
|
for r in rules_list:
|
|
try:
|
|
rule = RuleItem.model_validate(r)
|
|
except ValidationError:
|
|
continue
|
|
self.rules.append(rule)
|
|
|
|
def export_rules(self) -> List[Dict[str, str]]:
|
|
adapter = TypeAdapter(list[RuleItem])
|
|
return adapter.dump_python(self.rules, mode='json')
|
|
|
|
def append_rules(self, clash_rules: List[RuleItem]):
|
|
self.rules.extend(clash_rules)
|
|
|
|
def insert_rule_at_priority(self, clash_rule: RuleItem, priority: int):
|
|
self.rules.insert(priority, clash_rule)
|
|
|
|
def update_rule_at_priority(self, clash_rule: RuleItem, src_priority: int, dst_priority) -> bool:
|
|
if len(self.rules) > src_priority >= 0:
|
|
if src_priority == dst_priority:
|
|
self.rules[src_priority] = clash_rule
|
|
else:
|
|
self.remove_rule_at_priority(src_priority)
|
|
self.insert_rule_at_priority(clash_rule, dst_priority)
|
|
return True
|
|
return False
|
|
|
|
def get_rule_at_priority(self, priority: int) -> Optional[RuleItem]:
|
|
"""Get rule item by priority"""
|
|
if len(self.rules) > priority >= 0:
|
|
return self.rules[priority]
|
|
return None
|
|
|
|
def remove_rule_at_priority(self, priority: int) -> Optional[RuleItem]:
|
|
"""Remove rule at specific priority"""
|
|
if 0 <= priority < len(self.rules):
|
|
return self.rules.pop(priority)
|
|
return None
|
|
|
|
def remove_rules_at_priorities(self, priorities: list[int]) -> list[RuleItem]:
|
|
"""Remove rules at specific priorities"""
|
|
removed = []
|
|
# Sort priorities in descending order to avoid index shift issues during removal
|
|
for priority in sorted(priorities, reverse=True):
|
|
if 0 <= priority < len(self.rules):
|
|
removed.append(self.rules.pop(priority))
|
|
return removed
|
|
|
|
def remove_rules_by_lambda(self, condition: Callable[[RuleItem], bool]):
|
|
"""Remove rules by lambda"""
|
|
initial_count = len(self.rules)
|
|
i = 0
|
|
while i < len(self.rules):
|
|
if condition(self.rules[i]):
|
|
del self.rules[i]
|
|
else:
|
|
i += 1
|
|
return initial_count - len(self.rules)
|
|
|
|
def move_rule_priority(self, from_priority: int, to_priority: int) -> bool:
|
|
"""Move rule priority to priority"""
|
|
clash_rule = self.remove_rule_at_priority(from_priority)
|
|
if not clash_rule:
|
|
return False
|
|
self.insert_rule_at_priority(clash_rule, to_priority)
|
|
return True
|
|
|
|
def filter_rules_by_condition(self, condition: Callable[[RuleItem], bool]):
|
|
"""Filter rules by condition"""
|
|
return [clash_rule for clash_rule in self.rules if condition(clash_rule)]
|
|
|
|
def filter_rules_by_type(self, rule_type: RoutingRuleType) -> List[RuleItem]:
|
|
"""Filter rules by type"""
|
|
return [clash_rule for clash_rule in self.rules
|
|
if isinstance(clash_rule.rule, ClashRule) and clash_rule.rule.rule_type == rule_type]
|
|
|
|
def filter_rules_by_action(self, action: Union[Action, str]) -> List[RuleItem]:
|
|
"""Filter rules by action"""
|
|
return [clash_rule for clash_rule in self.rules if clash_rule.rule.action == action]
|
|
|
|
def has_rule(self, clash_rule: Union[ClashRule, LogicRule, MatchRule]) -> bool:
|
|
"""Check if there is an identical rule"""
|
|
return any(r.rule == clash_rule for r in self.rules)
|
|
|
|
def has_rule_item(self, clash_rule: RuleItem) -> bool:
|
|
return any(clash_rule.meta.source == r.meta.source and r.rule == clash_rule.rule for r in self.rules)
|
|
|
|
def reorder_rules(self, moved_priority: int, target_priority: int) -> RuleItem:
|
|
"""Reorder the rules"""
|
|
if not (0 <= moved_priority < len(self.rules)):
|
|
raise IndexError("moved_priority out of range")
|
|
if not (0 <= target_priority < len(self.rules)):
|
|
raise IndexError("target_priority out of range")
|
|
rule = self.rules.pop(moved_priority)
|
|
self.rules.insert(target_priority, rule)
|
|
return rule
|
|
|
|
def update_rules_at_priorities(self, priorities: dict[int, bool]) -> list[RuleItem]:
|
|
"""Disable rules"""
|
|
updated = []
|
|
for priority, disabled in priorities.items():
|
|
if 0 <= priority < len(self.rules):
|
|
self.rules[priority].meta.disabled = disabled
|
|
updated.append(self.rules[priority])
|
|
return updated
|
|
|
|
def update_rule_meta_at_priority(self, priority: int, meta: Metadata) -> bool:
|
|
"""Update rule metadata at priority"""
|
|
if 0 <= priority < len(self.rules):
|
|
self.rules[priority].meta = meta
|
|
return True
|
|
return False
|
|
|
|
def to_list(self) -> list[RuleData]:
|
|
"""Convert parsed rules to a list"""
|
|
result: list[RuleData] = []
|
|
for priority, rule_item in enumerate(self.rules):
|
|
result.append(RuleData.from_rule_item(rule_item, priority))
|
|
return result
|
|
|
|
def clear(self):
|
|
self.rules.clear()
|
|
|
|
def __len__(self) -> int:
|
|
return len(self.rules)
|
|
|
|
def __iter__(self) -> Iterator[RuleItem]:
|
|
return iter(self.rules)
|