diff --git a/app/agent/tools/impl/_filter_rule_utils.py b/app/agent/tools/impl/_filter_rule_utils.py index 6d8fd1dac..99aa6e38b 100644 --- a/app/agent/tools/impl/_filter_rule_utils.py +++ b/app/agent/tools/impl/_filter_rule_utils.py @@ -7,9 +7,9 @@ from typing import Any, Dict, Iterable, Optional from app.runtime.events import eventmanager from app.db.oper.subscribe import SubscribeOper from app.db.oper.systemconfig import SystemConfigOper -from app.application.filter import RuleHelper -from app.application.filter_rules import RuleParser -from app.application.filter_rules import BUILTIN_RULE_SET +from app.application.rules import RuleHelper +from app.application.rules import RuleParser +from app.application.rules import BUILTIN_RULE_SET from app.schemas import CustomRule, FilterRuleGroup from app.schemas.event import ConfigChangeEventData from app.schemas.types import EventType, SystemConfigKey diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index af033238e..e24aeb53e 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -41,7 +41,7 @@ from app.adapters.external.market import ( ) from app.application.messaging.message import MessageHelper from app.runtime.progress import ProgressHelper -from app.application.filter import RuleHelper +from app.application.rules import RuleHelper from app.adapters.external.server import MoviePilotServerHelper from app.runtime.state import SystemHelper from app.runtime.log import logger diff --git a/app/application/filter.py b/app/application/filter.py deleted file mode 100644 index 47ac7698c..000000000 --- a/app/application/filter.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import List, Optional - -from app.db.oper.systemconfig import SystemConfigOper -from app.domain.context import MediaInfo -from app.schemas import CustomRule, FilterRuleGroup -from app.schemas.types import SystemConfigKey - - -class RuleHelper: - """读取用户过滤规则配置,并按媒体上下文选择适用规则组。""" - - @staticmethod - def get_rule_groups() -> List[FilterRuleGroup]: - """返回用户配置的全部过滤规则组。""" - rule_groups: List[dict] = SystemConfigOper().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] = None, - ) -> List[FilterRuleGroup]: - """按媒体类型、分类和候选名称筛选适用规则组。""" - rule_groups = self.get_rule_groups() - if group_names: - rule_groups = [ - group for group in rule_groups if group.name in group_names - ] - return [ - group - for group in rule_groups - if not group.media_type - or ( - media - and ( - (not group.category and group.media_type == media.type.value) - or group.category == media.category - ) - ) - ] - - @staticmethod - def get_custom_rules() -> List[CustomRule]: - """返回用户配置的全部自定义过滤规则。""" - rules: List[dict] = SystemConfigOper().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, - ) diff --git a/app/application/filter_rules.py b/app/application/rules.py similarity index 80% rename from app/application/filter_rules.py rename to app/application/rules.py index 86905e227..e51f4c6a8 100644 --- a/app/application/filter_rules.py +++ b/app/application/rules.py @@ -1,11 +1,79 @@ -"""过滤规则解析器与内置规则定义,过滤模块与 Agent 工具共享同一事实来源。""" +""" +规则域:用户规则组配置访问、内置规则定义与规则解析器, +过滤模块与 Agent 工具共享同一事实来源。 +""" import threading -from typing import Dict +from typing import Dict, List, Optional from pyparsing import Forward, Literal, Word, alphas, infix_notation, opAssoc, alphanums, Combine, nums, ParseResults from app.adapters.system import rust as rust_accel +from app.db.oper.systemconfig import SystemConfigOper +from app.domain.context import MediaInfo +from app.schemas import CustomRule, FilterRuleGroup +from app.schemas.types import SystemConfigKey + + +class RuleHelper: + """读取用户过滤规则配置,并按媒体上下文选择适用规则组。""" + + @staticmethod + def get_rule_groups() -> List[FilterRuleGroup]: + """返回用户配置的全部过滤规则组。""" + rule_groups: List[dict] = SystemConfigOper().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] = None, + ) -> List[FilterRuleGroup]: + """按媒体类型、分类和候选名称筛选适用规则组。""" + rule_groups = self.get_rule_groups() + if group_names: + rule_groups = [ + group for group in rule_groups if group.name in group_names + ] + return [ + group + for group in rule_groups + if not group.media_type + or ( + media + and ( + (not group.category and group.media_type == media.type.value) + or group.category == media.category + ) + ) + ] + + @staticmethod + def get_custom_rules() -> List[CustomRule]: + """返回用户配置的全部自定义过滤规则。""" + rules: List[dict] = SystemConfigOper().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, + ) + # 内置规则只在这里维护一份,便于过滤模块和 Agent 工具共享同一套事实来源。 BUILTIN_RULE_SET: Dict[str, dict] = { diff --git a/app/modules/filter/__init__.py b/app/modules/filter/__init__.py index 5ce370aa2..2d5a590aa 100644 --- a/app/modules/filter/__init__.py +++ b/app/modules/filter/__init__.py @@ -5,11 +5,11 @@ from typing import List, Tuple, Union, Dict, Optional from app.domain.context import TorrentInfo, MediaInfo from app.domain.metainfo import MetaInfo, clear_rust_parse_options_cache, _rust_parse_options -from app.application.filter import RuleHelper +from app.application.rules import RuleHelper from app.runtime.log import logger from app.modules import _ModuleBase -from app.application.filter_rules import RuleParser -from app.application.filter_rules import BUILTIN_RULE_SET +from app.application.rules import RuleParser +from app.application.rules import BUILTIN_RULE_SET from app.schemas.types import ModuleType, OtherModulesType, SystemConfigKey from app.adapters.system import rust as rust_accel from app.foundation import size as size_tools diff --git a/app/runtime/compat/manifest.py b/app/runtime/compat/manifest.py index a40f2076a..9964774d0 100644 --- a/app/runtime/compat/manifest.py +++ b/app/runtime/compat/manifest.py @@ -543,7 +543,7 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = { introduced="v3.0.0", owner="application", ), "app.helper.rule": ModuleAlias( - target="app.application.filter", replacement="app.sdk.services", + target="app.application.rules", replacement="app.sdk.services", introduced="v3.0.0", owner="application", ), "app.helper.scraper": ModuleAlias( diff --git a/app/sdk/services.py b/app/sdk/services.py index a52c4de10..f91e89264 100644 --- a/app/sdk/services.py +++ b/app/sdk/services.py @@ -3,7 +3,7 @@ from app.runtime.extensions.service_registry import ServiceBaseHelper, ServiceConfigHelper from app.runtime.state import SystemHelper from app.application.downloader import DownloaderHelper -from app.application.filter import RuleHelper +from app.application.rules import RuleHelper from app.application.mediaserver import ( MediaServerIdentityHelper, MediaServerHelper, diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 80fc531d3..6e3712d71 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -379,7 +379,7 @@ policy. `app/db` therefore has no dependency on `app/domain`. | `app/adapters/system/resource.py` | Runtime resource detection/download/installation | | `app/adapters/system/fsproxy.py` | Timeout-guarded local filesystem operations in a killable subprocess (with colocated `fsworker.py`) | | `app/adapters/external/wechat_crypt.py` | WeChat enterprise-message XML encryption/decryption protocol | -| `app/application/filter_rules.py` | Built-in torrent filter rule set and rule parser | +| `app/application/rules.py` | Rule domain: user rule-group config access (`RuleHelper`), built-in torrent filter rule set and rule parser | | `app/adapters/external/market.py` | Plugin repository discovery and installation | | `app/application/security/url.py` | URL/path validation, SSRF protection and signed image policy | | `app/application/mediaserver.py` | Configured media-server discovery and identity matching | diff --git a/tests/test_system_nettest.py b/tests/test_system_nettest.py index 9a0ff15af..029bc2b6d 100644 --- a/tests/test_system_nettest.py +++ b/tests/test_system_nettest.py @@ -51,7 +51,7 @@ _STUB_MODULES = dict([ _stub("app.application.mediaserver", MediaServerHelper=_Dummy), _stub("app.application.messaging.message", MessageHelper=_Dummy), _stub("app.runtime.progress", ProgressHelper=_Dummy), - _stub("app.application.filter", RuleHelper=_Dummy), + _stub("app.application.rules", RuleHelper=_Dummy), _stub("app.adapters.external.server", MoviePilotServerHelper=_Dummy), _stub("app.runtime.state", SystemHelper=_Dummy), _stub("app.application.image", ImageHelper=_Dummy), diff --git a/tests/test_torrent_filter.py b/tests/test_torrent_filter.py index 135080edc..d1fb9e7dc 100644 --- a/tests/test_torrent_filter.py +++ b/tests/test_torrent_filter.py @@ -5,7 +5,7 @@ from unittest.mock import patch from app.domain.context import MediaInfo, TorrentInfo from app.application.torrent import TorrentHelper from app.modules.filter import FilterModule -from app.application.filter_rules import BUILTIN_RULE_SET +from app.application.rules import BUILTIN_RULE_SET from app.adapters.system import rust as rust_accel