mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-05-24 23:16:49 +00:00
- 数据模型重构: 全面引入 Pydantic 模型(ClashConfig, Proxy, ProxyGroup 等)替代原有字典结构,提供更严格的数据验证与类型安全。 - 数据迁移机制: 新增 v2.1.0 数据升级脚本,支持将旧版代理、策略组及规则数据自动迁移至新架构。 - 配置补丁系统: 实现基于 JSON Patch 的细粒度配置修补机制,替代旧版覆盖逻辑,提升配置修改的灵活性。 - 服务层优化: 重写 ClashRuleProviderService 以适配新对象模型,增强代码可维护性与扩展性。 - API模型同步: 更新相关 API 数据模型以保持与内部数据结构的一致性。 - 用户界面: 批量规则管理和数据项隐藏支持
284 lines
10 KiB
Python
284 lines
10 KiB
Python
from itertools import chain
|
|
from typing import Any, Generator, Callable
|
|
|
|
from pydantic import TypeAdapter
|
|
|
|
from app.core.cache import Cache
|
|
from app.db.plugindata_oper import PluginDataOper
|
|
|
|
from .config import PluginConfig
|
|
from .helper.clashrulemanager import ClashRuleManager
|
|
from .helper.utilsprovider import UtilsProvider
|
|
from .models import RuleProviderData, ProxyProviderData, ProxyGroupData, Hosts, ProxyGroups, RuleProviders, \
|
|
RuleProvider, Metadata, Proxies, ProxyData
|
|
from .models.configuration import ClashConfig
|
|
from .models.types import DataSource, RuleSet, DataKey
|
|
from .models.datapatch import DataPatch
|
|
from .models.api import SubscriptionsInfo
|
|
from .models.datamodel import GeoRules, PersistState
|
|
|
|
|
|
class PluginState:
|
|
"""
|
|
A DAL to manage the runtime state of ClashRuleProvider.
|
|
"""
|
|
def __init__(self, plugin_id: str, config: PluginConfig = None):
|
|
self.plugin_id = plugin_id
|
|
self.config = config or PluginConfig()
|
|
self.plugin_data = PluginDataOper()
|
|
self.cache = Cache(maxsize=256, ttl=self.config.cache_ttl)
|
|
self.cache_region = f"app.plugins.{self.plugin_id.lower()}"
|
|
|
|
# Build schemas from PersistState model
|
|
self._schemas: dict[str, tuple[TypeAdapter, Callable[[], Any]]] = {}
|
|
for _, field in PersistState.model_fields.items():
|
|
alias = field.alias
|
|
if alias:
|
|
self._schemas[alias] = (TypeAdapter(field.annotation), field.default_factory)
|
|
|
|
# Rule and Proxy Managers (Runtime)
|
|
self.top_rules_manager: ClashRuleManager = ClashRuleManager()
|
|
self.ruleset_rules_manager: ClashRuleManager = ClashRuleManager()
|
|
|
|
# Runtime variables (not persisted directly or persisted via config)
|
|
self.clash_template: ClashConfig = ClashConfig()
|
|
|
|
def _get_val(self, key: str) -> Any:
|
|
# Check cache
|
|
if self.cache.exists(key, region=self.cache_region):
|
|
return self.cache.get(key, region=self.cache_region)
|
|
|
|
data = self.plugin_data.get_data(self.plugin_id, key)
|
|
adapter, default_factory = self._schemas.get(key, (None, None))
|
|
|
|
if data is None:
|
|
if default_factory:
|
|
val = default_factory()
|
|
self.cache.set(key, val, region=self.cache_region)
|
|
return val
|
|
return None
|
|
|
|
if adapter:
|
|
val = adapter.validate_python(data)
|
|
else:
|
|
val = data
|
|
|
|
self.cache.set(key, val, region=self.cache_region)
|
|
return val
|
|
|
|
def _set_val(self, key: str, value: Any):
|
|
adapter, _ = self._schemas.get(key, (None, None))
|
|
if adapter:
|
|
data = adapter.dump_python(value, mode="json", by_alias=True, exclude_none=True)
|
|
else:
|
|
data = value
|
|
self.plugin_data.save(self.plugin_id, key, data)
|
|
self.cache.set(key, value, region=self.cache_region)
|
|
|
|
@property
|
|
def proxies(self) -> Proxies:
|
|
return self._get_val(DataKey.PROXIES)
|
|
|
|
@proxies.setter
|
|
def proxies(self, value: Proxies):
|
|
self._set_val(DataKey.PROXIES, value)
|
|
|
|
@property
|
|
def proxy_groups(self) -> ProxyGroups:
|
|
return self._get_val(DataKey.PROXY_GROUPS)
|
|
|
|
@proxy_groups.setter
|
|
def proxy_groups(self, value: ProxyGroups):
|
|
self._set_val(DataKey.PROXY_GROUPS, value)
|
|
|
|
@property
|
|
def subscription_info(self) -> SubscriptionsInfo:
|
|
return self._get_val(DataKey.SUB_INFO)
|
|
|
|
@subscription_info.setter
|
|
def subscription_info(self, value: SubscriptionsInfo):
|
|
self._set_val(DataKey.SUB_INFO, value)
|
|
|
|
@property
|
|
def rule_provider(self) -> dict[str, RuleProvider]:
|
|
return self._get_val(DataKey.AUTO_RULE_PROVIDERS)
|
|
|
|
@rule_provider.setter
|
|
def rule_provider(self, value: dict[str, RuleProvider]):
|
|
self._set_val(DataKey.AUTO_RULE_PROVIDERS, value)
|
|
|
|
@property
|
|
def rule_providers(self) -> RuleProviders:
|
|
return self._get_val(DataKey.RULE_PROVIDERS)
|
|
|
|
@rule_providers.setter
|
|
def rule_providers(self, value: RuleProviders):
|
|
self._set_val(DataKey.RULE_PROVIDERS, value)
|
|
|
|
@property
|
|
def ruleset_names(self) -> dict[str, str]:
|
|
return self._get_val(DataKey.RULESET_NAMES)
|
|
|
|
@ruleset_names.setter
|
|
def ruleset_names(self, value: dict[str, str]):
|
|
self._set_val(DataKey.RULESET_NAMES, value)
|
|
|
|
@property
|
|
def acl4ssr_providers(self) -> RuleProviders:
|
|
return self._get_val(DataKey.ACL4SSR)
|
|
|
|
@acl4ssr_providers.setter
|
|
def acl4ssr_providers(self, value: RuleProviders):
|
|
self._set_val(DataKey.ACL4SSR, value)
|
|
|
|
@property
|
|
def sub_configs(self) -> dict[str, ClashConfig]:
|
|
sub_conf = self._get_val(DataKey.SUB_CONFIGS)
|
|
return sub_conf
|
|
|
|
@sub_configs.setter
|
|
def sub_configs(self, value: dict[str, ClashConfig]):
|
|
self._set_val(DataKey.SUB_CONFIGS, value)
|
|
|
|
@property
|
|
def hosts(self) -> Hosts:
|
|
return self._get_val(DataKey.HOSTS)
|
|
|
|
@hosts.setter
|
|
def hosts(self, value: Hosts):
|
|
self._set_val(DataKey.HOSTS, value)
|
|
|
|
@property
|
|
def proxy_group_patch(self) -> DataPatch:
|
|
return self._get_val(DataKey.PROXY_GROUP_PATCH)
|
|
|
|
@proxy_group_patch.setter
|
|
def proxy_group_patch(self, value: DataPatch):
|
|
self._set_val(DataKey.PROXY_GROUP_PATCH, value)
|
|
|
|
@property
|
|
def proxy_patch(self) -> DataPatch:
|
|
return self._get_val(DataKey.PROXY_PATCH)
|
|
|
|
@proxy_patch.setter
|
|
def proxy_patch(self, value: DataPatch):
|
|
self._set_val(DataKey.PROXY_PATCH, value)
|
|
|
|
@property
|
|
def rule_provider_patch(self) -> DataPatch:
|
|
return self._get_val(DataKey.RULE_PROVIDER_PATCH)
|
|
|
|
@rule_provider_patch.setter
|
|
def rule_provider_patch(self, value: DataPatch):
|
|
self._set_val(DataKey.RULE_PROVIDER_PATCH, value)
|
|
|
|
@property
|
|
def geo_rules(self) -> GeoRules:
|
|
return self._get_val(DataKey.GEO_RULES)
|
|
|
|
@geo_rules.setter
|
|
def geo_rules(self, value: GeoRules):
|
|
self._set_val(DataKey.GEO_RULES, value)
|
|
|
|
def get_data(self, key: str) -> Any:
|
|
return self.plugin_data.get_data(self.plugin_id, key)
|
|
|
|
def save_data(self, key: str, value: Any):
|
|
self.plugin_data.save(self.plugin_id, key, value)
|
|
|
|
def get_rule_manager(self, ruleset: RuleSet) -> ClashRuleManager:
|
|
if ruleset == RuleSet.RULESET:
|
|
return self.ruleset_rules_manager
|
|
return self.top_rules_manager
|
|
|
|
def get_sub_config(self, url: str) -> ClashConfig:
|
|
conf = self.sub_configs.get(url)
|
|
if conf is None:
|
|
return ClashConfig()
|
|
ret = ClashConfig()
|
|
sub_options = self.config.get_sub_conf(url)
|
|
for field_name in sub_options.model_fields.keys():
|
|
if getattr(sub_options, field_name) is True and field_name in ret.model_fields:
|
|
setattr(ret, field_name, getattr(conf, field_name))
|
|
return ret
|
|
|
|
def set_rule_providers(self, rule_providers: dict[str, dict[str, Any]]):
|
|
self.rule_provider.clear()
|
|
for name, rp in rule_providers.items():
|
|
self.rule_providers[name] = RuleProvider(**rp)
|
|
|
|
def rule_providers_from_subs(self) -> Generator[RuleProviderData, None, None]:
|
|
for url, conf in self.sub_configs.items():
|
|
if self.config.get_sub_conf(url).rule_providers:
|
|
for name, rp in conf.rule_providers.items():
|
|
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
|
yield RuleProviderData(name=name, data=rp, meta=meta)
|
|
|
|
def rule_providers_from_template(self) -> Generator[RuleProviderData, None, None]:
|
|
for name, rp in self.clash_template.rule_providers.items():
|
|
yield RuleProviderData(meta=Metadata(source=DataSource.TEMPLATE), name=name, data=rp)
|
|
|
|
def proxy_providers_from_subs(self) -> Generator[ProxyProviderData, None, None]:
|
|
for url, conf in self.sub_configs.items():
|
|
if self.config.get_sub_conf(url).proxy_providers:
|
|
for name, pp in conf.proxy_providers.items():
|
|
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
|
yield ProxyProviderData(meta=meta, name=name, data=pp)
|
|
|
|
def proxy_providers_from_template(self) -> Generator[ProxyProviderData, None, None]:
|
|
for name, pp in self.clash_template.proxy_providers.items():
|
|
yield ProxyProviderData(meta=Metadata(source=DataSource.TEMPLATE), name=name, data=pp)
|
|
|
|
def proxy_groups_from_subs(self) -> Generator[ProxyGroupData, None, None]:
|
|
for url, conf in self.sub_configs.items():
|
|
if self.config.get_sub_conf(url).proxy_groups:
|
|
for pg in conf.proxy_groups:
|
|
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
|
yield ProxyGroupData(meta=meta, data=pg, name=pg.name)
|
|
|
|
def proxy_groups_from_template(self) -> Generator[ProxyGroupData, None, None]:
|
|
for pg in self.clash_template.proxy_groups:
|
|
yield ProxyGroupData(meta=Metadata(source=DataSource.TEMPLATE), data=pg, name=pg.name)
|
|
|
|
def proxies_from_subs(self) -> Generator[ProxyData, None, None]:
|
|
for url, conf in self.sub_configs.items():
|
|
for p in conf.proxies:
|
|
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
|
yield ProxyData(meta=meta, data=p, name=p.name, v2ray_link=conf.raw_proxies.get(p.name))
|
|
|
|
def proxies_from_template(self) -> Generator[ProxyData, None, None]:
|
|
for p in self.clash_template.proxies:
|
|
yield ProxyData(meta=Metadata(source=DataSource.TEMPLATE), data=p, name=p.name)
|
|
|
|
@property
|
|
def all_rule_providers(self) -> list[RuleProviderData]:
|
|
return list(chain(
|
|
self.rule_providers,
|
|
self.rule_providers_from_template(),
|
|
self.rule_providers_from_subs(),
|
|
self.acl4ssr_providers
|
|
))
|
|
|
|
@property
|
|
def all_proxy_providers(self) -> list[ProxyProviderData]:
|
|
return list(chain(
|
|
self.proxy_providers_from_subs(),
|
|
self.proxy_providers_from_template()
|
|
))
|
|
|
|
@property
|
|
def all_proxy_groups(self) -> list[ProxyGroupData]:
|
|
return list(chain(
|
|
self.proxy_groups,
|
|
self.proxy_groups_from_subs(),
|
|
self.proxy_groups_from_template()
|
|
))
|
|
|
|
@property
|
|
def all_proxies(self) -> list[ProxyData]:
|
|
return list(chain(
|
|
self.proxies,
|
|
self.proxies_from_subs(),
|
|
self.proxies_from_template()
|
|
))
|