mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
* fix(filter): match rule groups to effective media categories * docs(architecture): sync dependency count for category matching
824 lines
31 KiB
Python
824 lines
31 KiB
Python
"""
|
||
规则域:用户规则组配置访问、内置规则定义与规则解析器,
|
||
过滤模块与 Agent 工具共享同一事实来源。
|
||
"""
|
||
|
||
import copy
|
||
import threading
|
||
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
||
from dataclasses import dataclass
|
||
from typing import Any, Dict, List, Optional, Protocol
|
||
|
||
from pyparsing import (
|
||
Combine,
|
||
Forward,
|
||
Literal,
|
||
ParseResults,
|
||
Word,
|
||
alphanums,
|
||
alphas,
|
||
infix_notation,
|
||
nums,
|
||
opAssoc,
|
||
)
|
||
|
||
from app.application.classification.reference import classification_category_resolver_snapshot
|
||
from app.application.configuration import (
|
||
SystemConfigStagingPort,
|
||
get_configured_system_config,
|
||
)
|
||
from app.application.outbox import SyncUnitOfWork
|
||
from app.application.subscription.contract import (
|
||
SubscriptionPatch,
|
||
SubscriptionReferenceStagingPort,
|
||
SubscriptionSnapshot,
|
||
)
|
||
from app.domain.context import MediaInfo
|
||
from app.schemas.common import JsonData
|
||
from app.schemas.rule import CustomRule, FilterRuleGroup
|
||
from app.schemas.types import SystemConfigKey
|
||
|
||
_RULE_GROUP_LIST_CONFIG_KEYS = (
|
||
SystemConfigKey.SearchFilterRuleGroups,
|
||
SystemConfigKey.SubscribeFilterRuleGroups,
|
||
SystemConfigKey.BestVersionFilterRuleGroups,
|
||
)
|
||
_RULE_GROUP_DEFAULT_CONFIG_KEYS = (
|
||
SystemConfigKey.DefaultMovieSubscribeConfig,
|
||
SystemConfigKey.DefaultTvSubscribeConfig,
|
||
SystemConfigKey.DefaultMusicSubscribeConfig,
|
||
)
|
||
|
||
|
||
class FilterRuleParser(Protocol):
|
||
"""
|
||
描述可选的过滤规则加速解析能力。
|
||
"""
|
||
|
||
def parse_filter_rule(self, expression: str) -> Optional[list[Any]]:
|
||
"""
|
||
返回加速解析结构;不可用时返回 None。
|
||
|
||
:param expression: 由规则标识、逻辑运算和优先级组成的过滤表达式
|
||
:return: 解析后的规则结构,原生能力不可用时为 None,由调用方回退到 Python 解析
|
||
"""
|
||
...
|
||
|
||
|
||
_filter_rule_parser: Optional[FilterRuleParser] = None
|
||
|
||
|
||
def configure_filter_rule_parser(parser: Optional[FilterRuleParser]) -> None:
|
||
"""
|
||
由启动组合根注入或清除可选规则解析加速器。
|
||
"""
|
||
global _filter_rule_parser
|
||
_filter_rule_parser = parser
|
||
|
||
|
||
class AsyncRuleGroupUnitOfWork(Protocol):
|
||
"""
|
||
异步规则组修改事务的最小提交与回滚端口。
|
||
"""
|
||
|
||
async def commit(self) -> None:
|
||
"""
|
||
提交规则定义及全部引用更新。
|
||
"""
|
||
|
||
async def rollback(self) -> None:
|
||
"""
|
||
回滚规则定义及全部引用更新。
|
||
"""
|
||
|
||
|
||
class RuleGroupMutationConflictError(RuntimeError):
|
||
"""
|
||
规则定义已被并发修改,拒绝用过期快照覆盖新值。
|
||
"""
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class RuleGroupSubscriptionChange:
|
||
"""
|
||
一条已提交的订阅规则组引用变更。
|
||
|
||
:param subscribe_id: 受本次规则组变更影响的订阅主键
|
||
:param name: 订阅展示名称
|
||
:param season: 订阅季号,非剧集订阅可为 None
|
||
:param filter_groups: 引用更新后订阅实际使用的规则组名称
|
||
"""
|
||
|
||
subscribe_id: int
|
||
name: str
|
||
season: Optional[int]
|
||
filter_groups: tuple[str, ...]
|
||
|
||
def to_dict(self) -> dict[str, JsonData]:
|
||
"""
|
||
返回 Agent 响应可直接序列化的兼容字典。
|
||
"""
|
||
return {
|
||
"subscribe_id": self.subscribe_id,
|
||
"name": self.name,
|
||
"season": self.season,
|
||
"filter_groups": list(self.filter_groups),
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class RuleGroupMutation:
|
||
"""
|
||
规则定义和全部引用在同一事务提交后的稳定结果。
|
||
|
||
:param configurations: 本次事务提交后的规则定义与相关系统配置值
|
||
:param subscriptions: 本次事务中已更新规则组引用的订阅记录
|
||
"""
|
||
|
||
configurations: Mapping[SystemConfigKey, JsonData]
|
||
subscriptions: tuple[RuleGroupSubscriptionChange, ...]
|
||
|
||
def to_dict(self) -> dict[str, JsonData]:
|
||
"""
|
||
返回现有 Agent 工具使用的引用更新响应结构。
|
||
"""
|
||
return {
|
||
"global_settings": {
|
||
key.value: copy.deepcopy(value)
|
||
for key, value in self.configurations.items()
|
||
if key != SystemConfigKey.UserFilterRuleGroups
|
||
},
|
||
"subscribes": [change.to_dict() for change in self.subscriptions],
|
||
}
|
||
|
||
|
||
def _map_rule_group_names(
|
||
values: object,
|
||
previous_name: Optional[str],
|
||
current_name: Optional[str],
|
||
valid_names: set[str],
|
||
) -> list[str]:
|
||
"""
|
||
按删除或改名意图映射引用,并按最终定义清理悬空名称。
|
||
"""
|
||
result: list[str] = []
|
||
for raw_name in values if isinstance(values, list) else []:
|
||
name = str(raw_name)
|
||
if previous_name is not None and name == previous_name:
|
||
if current_name is None:
|
||
continue
|
||
name = current_name
|
||
if name in valid_names and name not in result:
|
||
result.append(name)
|
||
return result
|
||
|
||
|
||
def _rewrite_default_config(
|
||
value: object,
|
||
previous_name: Optional[str],
|
||
current_name: Optional[str],
|
||
valid_names: set[str],
|
||
) -> dict[str, JsonData]:
|
||
"""
|
||
复制默认订阅配置并只重写其中的规则组引用。
|
||
"""
|
||
original = copy.deepcopy(value) if isinstance(value, dict) else {}
|
||
original_groups = original.get("filter_groups")
|
||
updated_groups = _map_rule_group_names(
|
||
original_groups, previous_name, current_name, valid_names
|
||
)
|
||
if updated_groups == (original_groups or []):
|
||
return original
|
||
original["filter_groups"] = updated_groups
|
||
return original
|
||
|
||
|
||
def _subscription_change(
|
||
subscription: SubscriptionSnapshot,
|
||
filter_groups: list[str],
|
||
) -> RuleGroupSubscriptionChange:
|
||
"""
|
||
把事务内订阅快照投影为提交结果。
|
||
"""
|
||
return RuleGroupSubscriptionChange(
|
||
subscribe_id=subscription.id,
|
||
name=subscription.name,
|
||
season=subscription.season,
|
||
filter_groups=tuple(filter_groups),
|
||
)
|
||
|
||
|
||
class SyncRuleGroupMutationService:
|
||
"""
|
||
在一个同步 UoW 内原子提交规则定义、配置引用和订阅引用。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
configuration: SystemConfigStagingPort,
|
||
subscriptions: SubscriptionReferenceStagingPort,
|
||
unit_of_work: SyncUnitOfWork,
|
||
publish: Callable[[Mapping[SystemConfigKey, JsonData]], None],
|
||
) -> None:
|
||
"""
|
||
注入共享 Session 的端口、UoW 和必填提交后快照发布器。
|
||
"""
|
||
self._configuration = configuration
|
||
self._subscriptions = subscriptions
|
||
self._unit_of_work = unit_of_work
|
||
self._publish = publish
|
||
|
||
def apply(
|
||
self,
|
||
rule_groups: list[dict[str, JsonData]],
|
||
*,
|
||
expected_rule_groups: list[dict[str, JsonData]],
|
||
custom_rules: Optional[list[dict[str, JsonData]]] = None,
|
||
expected_custom_rules: Optional[list[dict[str, JsonData]]] = None,
|
||
previous_name: Optional[str] = None,
|
||
current_name: Optional[str] = None,
|
||
) -> RuleGroupMutation:
|
||
"""
|
||
原子保存规则组及可选自定义规则,并重写全部规则组引用。
|
||
"""
|
||
if (custom_rules is None) != (expected_custom_rules is None):
|
||
raise ValueError("自定义规则写入必须同时提供当前快照和目标值")
|
||
try:
|
||
result = self._stage(
|
||
rule_groups,
|
||
expected_rule_groups,
|
||
custom_rules,
|
||
expected_custom_rules,
|
||
previous_name,
|
||
current_name,
|
||
)
|
||
self._unit_of_work.commit()
|
||
except Exception:
|
||
self._unit_of_work.rollback()
|
||
raise
|
||
self._publish(copy.deepcopy(dict(result.configurations)))
|
||
return result
|
||
|
||
def _stage(
|
||
self,
|
||
rule_groups: list[dict[str, JsonData]],
|
||
expected_rule_groups: list[dict[str, JsonData]],
|
||
custom_rules: Optional[list[dict[str, JsonData]]],
|
||
expected_custom_rules: Optional[list[dict[str, JsonData]]],
|
||
previous_name: Optional[str],
|
||
current_name: Optional[str],
|
||
) -> RuleGroupMutation:
|
||
"""
|
||
在同步 Session 中锁定事实源并暂存全部变化。
|
||
"""
|
||
changes: dict[SystemConfigKey, JsonData] = {}
|
||
definitions = copy.deepcopy(rule_groups)
|
||
valid_names = {
|
||
str(group["name"])
|
||
for group in definitions
|
||
if isinstance(group.get("name"), str) and group["name"]
|
||
}
|
||
original_definitions = self._configuration.get_for_update(
|
||
SystemConfigKey.UserFilterRuleGroups
|
||
)
|
||
if (
|
||
(original_definitions or []) != expected_rule_groups
|
||
and original_definitions != definitions
|
||
):
|
||
raise RuleGroupMutationConflictError("规则组已被其他请求修改,请重新读取后再试")
|
||
if original_definitions != definitions:
|
||
definition_payload = list[JsonData](definitions)
|
||
self._configuration.stage_set(
|
||
SystemConfigKey.UserFilterRuleGroups, definition_payload
|
||
)
|
||
changes[SystemConfigKey.UserFilterRuleGroups] = definition_payload
|
||
|
||
if custom_rules is not None:
|
||
assert expected_custom_rules is not None
|
||
custom_definitions = copy.deepcopy(custom_rules)
|
||
original_custom_rules = self._configuration.get_for_update(
|
||
SystemConfigKey.CustomFilterRules
|
||
)
|
||
if (
|
||
(original_custom_rules or []) != expected_custom_rules
|
||
and original_custom_rules != custom_definitions
|
||
):
|
||
raise RuleGroupMutationConflictError(
|
||
"自定义规则已被其他请求修改,请重新读取后再试"
|
||
)
|
||
if original_custom_rules != custom_definitions:
|
||
custom_payload = list[JsonData](custom_definitions)
|
||
self._configuration.stage_set(
|
||
SystemConfigKey.CustomFilterRules,
|
||
custom_payload,
|
||
)
|
||
changes[SystemConfigKey.CustomFilterRules] = custom_payload
|
||
|
||
for key in _RULE_GROUP_LIST_CONFIG_KEYS:
|
||
original = self._configuration.get_for_update(key)
|
||
list_updated = _map_rule_group_names(
|
||
original, previous_name, current_name, valid_names
|
||
)
|
||
if list_updated != (original or []):
|
||
list_payload = list[JsonData](list_updated)
|
||
self._configuration.stage_set(key, list_payload)
|
||
changes[key] = list_payload
|
||
|
||
for key in _RULE_GROUP_DEFAULT_CONFIG_KEYS:
|
||
original = self._configuration.get_for_update(key)
|
||
default_updated = _rewrite_default_config(
|
||
original, previous_name, current_name, valid_names
|
||
)
|
||
if default_updated != (original or {}):
|
||
self._configuration.stage_set(key, default_updated)
|
||
changes[key] = default_updated
|
||
|
||
subscription_changes = []
|
||
for subscription in self._subscriptions.list_for_reference_rewrite():
|
||
original = list(subscription.filter_groups or [])
|
||
updated = _map_rule_group_names(
|
||
original, previous_name, current_name, valid_names
|
||
)
|
||
if updated == original:
|
||
continue
|
||
self._subscriptions.stage_update(
|
||
subscription.id,
|
||
SubscriptionPatch({"filter_groups": list[JsonData](updated)}),
|
||
)
|
||
subscription_changes.append(_subscription_change(subscription, updated))
|
||
return RuleGroupMutation(changes, tuple(subscription_changes))
|
||
|
||
|
||
class AsyncRuleGroupMutationService:
|
||
"""
|
||
在一个异步 UoW 内原子提交规则定义、配置引用和订阅引用。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
configuration: SystemConfigStagingPort,
|
||
subscriptions: SubscriptionReferenceStagingPort,
|
||
unit_of_work: AsyncRuleGroupUnitOfWork,
|
||
publish: Callable[[Mapping[SystemConfigKey, JsonData]], Awaitable[None]],
|
||
) -> None:
|
||
"""
|
||
注入共享 AsyncSession 的端口、UoW 和必填提交后快照发布器。
|
||
"""
|
||
self._configuration = configuration
|
||
self._subscriptions = subscriptions
|
||
self._unit_of_work = unit_of_work
|
||
self._publish = publish
|
||
|
||
async def apply(
|
||
self,
|
||
rule_groups: list[dict[str, JsonData]],
|
||
*,
|
||
expected_rule_groups: list[dict[str, JsonData]],
|
||
custom_rules: Optional[list[dict[str, JsonData]]] = None,
|
||
expected_custom_rules: Optional[list[dict[str, JsonData]]] = None,
|
||
previous_name: Optional[str] = None,
|
||
current_name: Optional[str] = None,
|
||
) -> RuleGroupMutation:
|
||
"""
|
||
异步原子保存规则组及可选自定义规则,并重写全部引用。
|
||
"""
|
||
if (custom_rules is None) != (expected_custom_rules is None):
|
||
raise ValueError("自定义规则写入必须同时提供当前快照和目标值")
|
||
try:
|
||
result = await self._stage(
|
||
rule_groups,
|
||
expected_rule_groups,
|
||
custom_rules,
|
||
expected_custom_rules,
|
||
previous_name,
|
||
current_name,
|
||
)
|
||
await self._unit_of_work.commit()
|
||
except Exception:
|
||
await self._unit_of_work.rollback()
|
||
raise
|
||
await self._publish(copy.deepcopy(dict(result.configurations)))
|
||
return result
|
||
|
||
async def _stage(
|
||
self,
|
||
rule_groups: list[dict[str, JsonData]],
|
||
expected_rule_groups: list[dict[str, JsonData]],
|
||
custom_rules: Optional[list[dict[str, JsonData]]],
|
||
expected_custom_rules: Optional[list[dict[str, JsonData]]],
|
||
previous_name: Optional[str],
|
||
current_name: Optional[str],
|
||
) -> RuleGroupMutation:
|
||
"""
|
||
在 AsyncSession 中锁定事实源并暂存全部变化。
|
||
"""
|
||
changes: dict[SystemConfigKey, JsonData] = {}
|
||
definitions = copy.deepcopy(rule_groups)
|
||
valid_names = {
|
||
str(group["name"])
|
||
for group in definitions
|
||
if isinstance(group.get("name"), str) and group["name"]
|
||
}
|
||
original_definitions = await self._configuration.async_get_for_update(
|
||
SystemConfigKey.UserFilterRuleGroups
|
||
)
|
||
if (
|
||
(original_definitions or []) != expected_rule_groups
|
||
and original_definitions != definitions
|
||
):
|
||
raise RuleGroupMutationConflictError("规则组已被其他请求修改,请重新读取后再试")
|
||
if original_definitions != definitions:
|
||
definition_payload = list[JsonData](definitions)
|
||
await self._configuration.async_stage_set(
|
||
SystemConfigKey.UserFilterRuleGroups, definition_payload
|
||
)
|
||
changes[SystemConfigKey.UserFilterRuleGroups] = definition_payload
|
||
|
||
if custom_rules is not None:
|
||
assert expected_custom_rules is not None
|
||
custom_definitions = copy.deepcopy(custom_rules)
|
||
original_custom_rules = (
|
||
await self._configuration.async_get_for_update(
|
||
SystemConfigKey.CustomFilterRules
|
||
)
|
||
)
|
||
if (
|
||
(original_custom_rules or []) != expected_custom_rules
|
||
and original_custom_rules != custom_definitions
|
||
):
|
||
raise RuleGroupMutationConflictError(
|
||
"自定义规则已被其他请求修改,请重新读取后再试"
|
||
)
|
||
if original_custom_rules != custom_definitions:
|
||
custom_payload = list[JsonData](custom_definitions)
|
||
await self._configuration.async_stage_set(
|
||
SystemConfigKey.CustomFilterRules,
|
||
custom_payload,
|
||
)
|
||
changes[SystemConfigKey.CustomFilterRules] = custom_payload
|
||
|
||
for key in _RULE_GROUP_LIST_CONFIG_KEYS:
|
||
original = await self._configuration.async_get_for_update(key)
|
||
list_updated = _map_rule_group_names(
|
||
original, previous_name, current_name, valid_names
|
||
)
|
||
if list_updated != (original or []):
|
||
list_payload = list[JsonData](list_updated)
|
||
await self._configuration.async_stage_set(key, list_payload)
|
||
changes[key] = list_payload
|
||
|
||
for key in _RULE_GROUP_DEFAULT_CONFIG_KEYS:
|
||
original = await self._configuration.async_get_for_update(key)
|
||
default_updated = _rewrite_default_config(
|
||
original, previous_name, current_name, valid_names
|
||
)
|
||
if default_updated != (original or {}):
|
||
await self._configuration.async_stage_set(key, default_updated)
|
||
changes[key] = default_updated
|
||
|
||
subscription_changes = []
|
||
subscriptions = await self._subscriptions.async_list_for_reference_rewrite()
|
||
for subscription in subscriptions:
|
||
original = list(subscription.filter_groups or [])
|
||
updated = _map_rule_group_names(
|
||
original, previous_name, current_name, valid_names
|
||
)
|
||
if updated == original:
|
||
continue
|
||
await self._subscriptions.async_stage_update(
|
||
subscription.id,
|
||
SubscriptionPatch({"filter_groups": list[JsonData](updated)}),
|
||
)
|
||
subscription_changes.append(_subscription_change(subscription, updated))
|
||
return RuleGroupMutation(changes, tuple(subscription_changes))
|
||
|
||
|
||
class RuleHelper:
|
||
"""
|
||
读取用户过滤规则配置,并按媒体上下文选择适用规则组。
|
||
"""
|
||
|
||
@staticmethod
|
||
def get_rule_groups() -> List[FilterRuleGroup]:
|
||
"""
|
||
返回用户配置的全部过滤规则组。
|
||
"""
|
||
rule_groups: List[dict[str, Any]] = get_configured_system_config().get(
|
||
SystemConfigKey.UserFilterRuleGroups
|
||
)
|
||
if not rule_groups:
|
||
return []
|
||
return [FilterRuleGroup(**group) for group in rule_groups]
|
||
|
||
def get_rule_group(self, group_name: str) -> Optional[FilterRuleGroup]:
|
||
"""
|
||
按名称返回过滤规则组。
|
||
"""
|
||
return next(
|
||
(group for group in self.get_rule_groups() if group.name == group_name),
|
||
None,
|
||
)
|
||
|
||
def get_rule_group_by_media(
|
||
self,
|
||
media: Optional[MediaInfo] = None,
|
||
group_names: Optional[list[str]] = None,
|
||
) -> List[FilterRuleGroup]:
|
||
"""
|
||
按媒体类型、生效分类和候选名称筛选适用规则组。
|
||
|
||
规则组的分类名称通过活动分类策略关联媒体,兼容已有目录路径条件。
|
||
|
||
:param media: 已识别并应用最终分类的媒体;未提供时仅匹配无类型限制的规则组
|
||
:param group_names: 限定规则组名称;为空时检查全部配置规则组
|
||
:return: 保持配置顺序的适用规则组,分类不跨主媒体类型匹配
|
||
"""
|
||
rule_groups = self.get_rule_groups()
|
||
if group_names:
|
||
rule_groups = [
|
||
group for group in rule_groups if group.name in group_names
|
||
]
|
||
category_resolver = classification_category_resolver_snapshot()
|
||
return [
|
||
group
|
||
for group in rule_groups
|
||
if not group.media_type
|
||
or (
|
||
media
|
||
and media.type
|
||
and group.media_type == media.type.value
|
||
and (not group.category or category_resolver.matches_media_category(media, group.category))
|
||
)
|
||
]
|
||
|
||
@staticmethod
|
||
def get_custom_rules() -> List[CustomRule]:
|
||
"""
|
||
返回用户配置的全部自定义过滤规则。
|
||
"""
|
||
rules: List[dict[str, Any]] = get_configured_system_config().get(
|
||
SystemConfigKey.CustomFilterRules
|
||
)
|
||
if not rules:
|
||
return []
|
||
return [CustomRule(**rule) for rule in rules]
|
||
|
||
def get_custom_rule(self, rule_id: str) -> Optional[CustomRule]:
|
||
"""
|
||
按 ID 返回一条自定义过滤规则。
|
||
"""
|
||
return next(
|
||
(rule for rule in self.get_custom_rules() if rule.id == rule_id),
|
||
None,
|
||
)
|
||
|
||
|
||
def replace_group_name_in_list(
|
||
values: Optional[Iterable[str]],
|
||
old_name: str,
|
||
new_name: str,
|
||
) -> list[str]:
|
||
"""
|
||
更新配置里的规则组名引用,并顺手去重。
|
||
"""
|
||
result = []
|
||
for value in values or []:
|
||
mapped = new_name if value == old_name else value
|
||
if mapped not in result:
|
||
result.append(mapped)
|
||
return result
|
||
|
||
|
||
# 内置规则只在这里维护一份,便于过滤模块和 Agent 工具共享同一套事实来源。
|
||
BUILTIN_RULE_SET: Dict[str, dict[str, Any]] = {
|
||
# 蓝光原盘
|
||
"BLU": {
|
||
"include": [
|
||
r"(?i)(\bBlu-?Ray\b.*\b(?:VC-?1|AVC|MPEG-?2)\b|\b(?:UHD|4K|2160p)\b(?:.*Blu-?Ray)?.*\b(?:HEVC|H\.?265)\b|\bBlu-?Ray\b.*\b(?:UHD|4K|2160p)\b.*\b(?:HEVC|H\.?265)\b|\b(?:COMPLETE|FULL)\b.*\b(?:(?:UHD|4K|2160p)\b.*)?Blu-?Ray\b|\b(BD25|BD50|BD66|BD100|BDMV|MiniBD)\b)"
|
||
],
|
||
"exclude": [
|
||
r"(?i)(\b[XH]\.?264\b|\b[XH]\.?265\b|\bWEB-?DL\b|\bWEB-?RIP\b|\bHDTV(?:RIP)?\b|\bREMUX\b|\bBDRip\b|\bBRRip\b|\bHDRip\b|\bENCODE\b|\b(?<!WEB-|HDTV)RIP\b)"
|
||
],
|
||
},
|
||
# 4K 超高清分辨率
|
||
"4K": {
|
||
"include": [r"4k|2160p|x2160"],
|
||
"exclude": [],
|
||
},
|
||
# 1080P 全高清分辨率
|
||
"1080P": {
|
||
"include": [r"1080[pi]|x1080"],
|
||
"exclude": [],
|
||
},
|
||
# 720P 高清分辨率
|
||
"720P": {
|
||
"include": [r"720[pi]|x720"],
|
||
"exclude": [],
|
||
},
|
||
# 中字
|
||
"CNSUB": {
|
||
"include": [
|
||
r"[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]"
|
||
r"|繁體|简体|[中国國][字配]|国语|國語|中文|中字|简日|繁日|简繁|繁体"
|
||
r"|([\s,.-\[])(chs|cht)(|[\s,.-\]])"
|
||
r"|(?<![a-z0-9])(?<!\d\s)(gb|big5)(?![a-z0-9])"
|
||
],
|
||
"exclude": [],
|
||
"tmdb": {
|
||
"original_language": "zh,cn",
|
||
},
|
||
},
|
||
# 官种
|
||
"GZ": {
|
||
"include": [r"官方", r"官种", r"官组"],
|
||
"match": ["labels"],
|
||
},
|
||
# 特效字幕
|
||
"SPECSUB": {
|
||
"include": [r"特效"],
|
||
"exclude": [],
|
||
},
|
||
# 蓝光来源
|
||
"BLURAY": {
|
||
"include": [r"Blu-?Ray"],
|
||
"exclude": [],
|
||
},
|
||
# 超高清蓝光来源
|
||
"UHD": {
|
||
"include": [r"UHD|UltraHD"],
|
||
"exclude": [],
|
||
},
|
||
# H.265 视频编码
|
||
"H265": {
|
||
"include": [r"[Hx].?265|HEVC"],
|
||
"exclude": [],
|
||
},
|
||
# H.264 视频编码
|
||
"H264": {
|
||
"include": [r"[Hx].?264|AVC"],
|
||
"exclude": [],
|
||
},
|
||
# 杜比视界
|
||
"DOLBY": {
|
||
"include": [r"Dolby[\s.]+Vision|DOVI|[\s.]+DV[\s.]+|杜比视界"],
|
||
"exclude": [],
|
||
},
|
||
# 杜比全景声
|
||
"ATMOS": {
|
||
"include": [r"Dolby[\s.+]+Atmos|Atmos|杜比全景[声聲]"],
|
||
"exclude": [],
|
||
},
|
||
# 高动态范围图像
|
||
"HDR": {
|
||
"include": [r"[\s.]+HDR[\s.]+|HDR10|HDR10\+|HDRVivid"],
|
||
"exclude": [],
|
||
},
|
||
# 标准动态范围图像
|
||
"SDR": {
|
||
"include": [r"[\s.]+SDR[\s.]+"],
|
||
"exclude": [],
|
||
},
|
||
# 重编码
|
||
"REMUX": {
|
||
"include": [r"REMUX"],
|
||
"exclude": [],
|
||
},
|
||
# 流媒体下载来源
|
||
"WEBDL": {
|
||
"include": [r"WEB-?DL|WEB-?RIP"],
|
||
"exclude": [],
|
||
},
|
||
# 免费
|
||
"FREE": {
|
||
"downloadvolumefactor": 0,
|
||
},
|
||
# 国语配音
|
||
"CNVOI": {
|
||
"include": [r"[国國][语語]配音|[国國]配|[国國][语語]"],
|
||
"exclude": [],
|
||
"tmdb": {
|
||
"original_language": "zh",
|
||
},
|
||
},
|
||
# 粤语配音
|
||
"HKVOI": {
|
||
"include": [r"粤语配音|粤语"],
|
||
"exclude": [],
|
||
},
|
||
# 每秒 60 帧视频
|
||
"60FPS": {
|
||
"include": [r"60fps|60帧"],
|
||
"exclude": [],
|
||
},
|
||
# 立体视频
|
||
"3D": {
|
||
"include": [r"3D"],
|
||
"exclude": [],
|
||
},
|
||
# Hi-Res 无损音频
|
||
"HIRES": {
|
||
"include": [r"(?i)\b(?:Hi[ ._-]?Res(?:olution)?|DSD(?:64|128|256|512)?)\b|高解析|(?:24|32)\s*(?:-?bit|位)"],
|
||
"exclude": [],
|
||
},
|
||
# 无损音频
|
||
"LOSSLESS": {
|
||
"include": [r"(?i)\b(?:Lossless|FLAC|ALAC|APE|WAV|WAVE|AIFF?|PCM|DSD|DSF|DFF)\b|无损"],
|
||
"exclude": [],
|
||
},
|
||
"FLAC": {"include": [r"(?i)(?<![A-Z0-9])FLAC(?![A-Z0-9])"], "exclude": []},
|
||
"ALAC": {"include": [r"(?i)(?<![A-Z0-9])ALAC(?![A-Z0-9])"], "exclude": []},
|
||
"APE": {"include": [r"(?i)(?<![A-Z0-9])APE(?![A-Z0-9])"], "exclude": []},
|
||
"WAV": {"include": [r"(?i)(?<![A-Z0-9])WAV(?:E)?(?![A-Z0-9])"], "exclude": []},
|
||
"DSD": {"include": [r"(?i)(?<![A-Z0-9])(?:DSD(?:64|128|256|512)?|DSF|DFF)(?![A-Z0-9])"], "exclude": []},
|
||
"MP3": {"include": [r"(?i)(?<![A-Z0-9])MP3(?![A-Z0-9])"], "exclude": []},
|
||
"AAC": {"include": [r"(?i)(?<![A-Z0-9])(?:AAC|M4A)(?![A-Z0-9])"], "exclude": []},
|
||
"OPUS": {"include": [r"(?i)(?<![A-Z0-9])OPUS(?![A-Z0-9])"], "exclude": []},
|
||
"BITRATE320": {"include": [r"(?i)(?<!\d)320\s*k(?:bps?|b(?:it)?/?s?)?(?![a-z])"], "exclude": []},
|
||
"BITRATE256": {"include": [r"(?i)(?<!\d)256\s*k(?:bps?|b(?:it)?/?s?)?(?![a-z])"], "exclude": []},
|
||
"BITRATE192": {"include": [r"(?i)(?<!\d)192\s*k(?:bps?|b(?:it)?/?s?)?(?![a-z])"], "exclude": []},
|
||
}
|
||
|
||
|
||
class RuleParser:
|
||
"""
|
||
按线程隔离解析器状态,解析用户规则表达式。
|
||
"""
|
||
|
||
_lock = threading.Lock()
|
||
_thread_local = threading.local()
|
||
|
||
def __init__(self) -> None:
|
||
"""
|
||
定义语法规则
|
||
"""
|
||
with self._lock:
|
||
if not hasattr(self._thread_local, 'initialized'):
|
||
# 表达式
|
||
expr: Forward = Forward()
|
||
# 原子
|
||
atom: Combine = Combine(Word(alphas, alphanums) | (Word(nums) + Word(alphas, alphanums)))
|
||
# 逻辑非操作符
|
||
operator_not: Literal = Literal('!').set_parse_action(lambda t: 'not')
|
||
# 逻辑或操作符
|
||
operator_or: Literal = Literal('|').set_parse_action(lambda t: 'or')
|
||
# 逻辑与操作符
|
||
operator_and: Literal = Literal('&').set_parse_action(lambda t: 'and')
|
||
# 定义表达式的语法规则
|
||
expr <<= (operator_not + expr) | atom | ('(' + expr + ')')
|
||
|
||
# 运算符优先级
|
||
self.expr = infix_notation(expr,
|
||
[(operator_not, 1, opAssoc.RIGHT),
|
||
(operator_and, 2, opAssoc.LEFT),
|
||
(operator_or, 2, opAssoc.LEFT)])
|
||
|
||
self._thread_local.expr = self.expr
|
||
self._thread_local.initialized = True
|
||
else:
|
||
self.expr = self._thread_local.expr
|
||
|
||
def parse(self, expression: str) -> ParseResults:
|
||
"""
|
||
解析给定的表达式。
|
||
|
||
:param expression: 待解析的完整规则表达式
|
||
:return: 逻辑表达式解析结果
|
||
:raises ParseException: 包含不合法语法或无法消费的尾部内容
|
||
"""
|
||
if _filter_rule_parser is not None:
|
||
try:
|
||
accelerated = _filter_rule_parser.parse_filter_rule(expression)
|
||
except Exception:
|
||
accelerated = None
|
||
if accelerated is not None:
|
||
return _RustParseResults(accelerated)
|
||
return self.expr.parse_string(expression, parse_all=True)
|
||
|
||
|
||
class _RustParseResults(list[Any]):
|
||
"""
|
||
包装 Rust 解析结果,提供本模块调用方使用的 as_list/asList 接口。
|
||
"""
|
||
|
||
def as_list(self) -> list[Any]:
|
||
"""
|
||
返回兼容 pyparsing.ParseResults.as_list 的列表结构。
|
||
"""
|
||
return list(self)
|
||
|
||
# 保留 pyparsing 的历史驼峰方法名,兼容既有调用方。
|
||
def asList(self) -> list[Any]: # noqa: N802
|
||
"""
|
||
返回兼容 pyparsing.ParseResults.asList 的列表结构。
|
||
"""
|
||
return self.as_list()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
expression_str = """
|
||
SPECSUB & CNVOI & 4K & !BLU & !REMUX & !WEBDL & 60FPS & !DOLBY & !SDR & !3D > CNSUB & CNVOI & 4K & !BLU & !REMUX & !WEBDL & 60FPS & !DOLBY & !SDR & !3D > SPECSUB & 4K & !BLU & !REMUX & !WEBDL & 60FPS & !DOLBY & !SDR & !3D > CNSUB & 4K & !BLU & !REMUX & !WEBDL & 60FPS & !DOLBY & !SDR & !3D > SPECSUB & CNVOI & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & HDR & !3D > CNSUB & CNVOI & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & HDR & !3D > SPECSUB & CNVOI & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & !3D > CNSUB & CNVOI & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & !3D > SPECSUB & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & HDR & !3D > CNSUB & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & HDR & !3D > SPECSUB & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & !3D > CNSUB & 4K & !BLU & !REMUX & !WEBDL & !DOLBY & !3D > SPECSUB & CNVOI & 4K & WEBDL & 60FPS & !DOLBY & !SDR & !3D > CNSUB & CNVOI & 4K & WEBDL & 60FPS & !DOLBY & !SDR & !3D > SPECSUB & 4K & WEBDL & 60FPS & !DOLBY & !SDR & !3D > CNSUB & 4K & WEBDL & 60FPS & !DOLBY & !SDR & !3D > SPECSUB & CNVOI & 4K & WEBDL & !DOLBY & HDR & !3D > CNSUB & CNVOI & 4K & WEBDL & !DOLBY & HDR & !3D > SPECSUB & CNVOI & 4K & WEBDL & !DOLBY & !3D > CNSUB & CNVOI & 4K & WEBDL & !DOLBY & !3D > SPECSUB & 4K & WEBDL & !DOLBY & HDR & !3D > CNSUB & 4K & WEBDL & !DOLBY & HDR & !3D > SPECSUB & 4K & WEBDL & !DOLBY & !3D > CNSUB & 4K & WEBDL & !DOLBY & !3D > SPECSUB & CNVOI & 4K & !BLU & !WEBDL & !DOLBY & HDR & !3D > CNSUB & CNVOI & 4K & !BLU & !WEBDL & !DOLBY & HDR & !3D > SPECSUB & CNVOI & 4K & !BLU & !WEBDL & !DOLBY & !3D > CNSUB & CNVOI & 4K & !BLU & !WEBDL & !DOLBY & !3D > SPECSUB & 4K & !BLU & !WEBDL & !DOLBY & HDR & !3D > CNSUB & 4K & !BLU & !WEBDL & !DOLBY & HDR & !3D > SPECSUB & 4K & !BLU & !WEBDL & !DOLBY & !SDR & !3D > CNSUB & 4K & !BLU & !WEBDL & !DOLBY & !SDR & !3D > 4K & !BLU & !REMUX & !DOLBY & HDR & !3D > 4K & !BLURAY & !REMUX & !DOLBY & !3D > SPECSUB & 1080P & !BLU & !REMUX & !WEBDL & !DOLBY & HDR & !3D > CNSUB & 1080P & !BLU & !REMUX & !WEBDL & !DOLBY & HDR & !3D > SPECSUB & 1080P & !BLU & !REMUX & !WEBDL & !DOLBY & !3D > CNSUB & 1080P & !BLU & !REMUX & !WEBDL & !DOLBY & !3D > SPECSUB & 1080P & !BLU & !WEBDL & !DOLBY & HDR & !3D > CNSUB & 1080P & !BLU & !WEBDL & !DOLBY & HDR & !3D > SPECSUB & 1080P & !BLU & !WEBDL & !DOLBY & !3D > CNSUB & 1080P & !BLU & !WEBDL & !DOLBY & !3D > SPECSUB & 1080P & WEBDL & !DOLBY & HDR & !3D > CNSUB & 1080P & WEBDL & !DOLBY & HDR & !3D > SPECSUB & 1080P & WEBDL & !DOLBY & !3D > CNSUB & 1080P & WEBDL & !DOLBY & !3D > 1080P & !BLU & !REMUX & !DOLBY & HDR & !3D > 1080P & !BLU & !REMUX & !DOLBY & !3D
|
||
"""
|
||
for exp in expression_str.split('>'):
|
||
parsed_expr = RuleParser().parse(exp.strip())
|
||
print(parsed_expr.asList())
|