Add Rust filter trace logging support

This commit is contained in:
jxxghp
2026-06-21 21:25:50 +08:00
parent 8938ae7baa
commit 3c74f1bf58
3 changed files with 318 additions and 164 deletions
+23
View File
@@ -161,6 +161,8 @@ class FilterModule(_ModuleBase):
torrent_list=torrent_list, torrent_list=torrent_list,
mediainfo=mediainfo mediainfo=mediainfo
) )
matched_orders, traces = self.__parse_rust_filter_result(matched_orders)
self.__log_rust_filter_traces(traces)
ret_torrents = [] ret_torrents = []
for index, pri_order in matched_orders: for index, pri_order in matched_orders:
torrent = torrent_list[index] torrent = torrent_list[index]
@@ -169,6 +171,27 @@ class FilterModule(_ModuleBase):
return ret_torrents return ret_torrents
return torrent_list return torrent_list
@staticmethod
def __parse_rust_filter_result(result) -> Tuple[list, list]:
"""
兼容新旧 Rust 过滤返回值,统一拆出匹配结果和调试日志。
"""
if (
isinstance(result, tuple)
and len(result) == 2
and isinstance(result[1], list)
):
return result
return result, []
@staticmethod
def __log_rust_filter_traces(traces: list) -> None:
"""
输出 Rust 过滤路径返回的规则级调试日志。
"""
for trace in traces:
logger.debug(trace)
def __filter_torrents_with_python(self, groups: List[dict], def __filter_torrents_with_python(self, groups: List[dict],
torrent_list: List[TorrentInfo], torrent_list: List[TorrentInfo],
mediainfo: MediaInfo = None) -> List[TorrentInfo]: mediainfo: MediaInfo = None) -> List[TorrentInfo]:
+19 -5
View File
@@ -1,7 +1,8 @@
from typing import List, Optional import logging
from typing import List, Optional, Tuple
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger, log_settings
try: try:
import moviepilot_rust as _moviepilot_rust import moviepilot_rust as _moviepilot_rust
@@ -71,25 +72,38 @@ def filter_torrents(
rule_set: dict, rule_set: dict,
mediainfo=None, mediainfo=None,
metainfo_options: Optional[dict] = None, metainfo_options: Optional[dict] = None,
) -> Optional[list]: ) -> Optional[Tuple[list, list]]:
""" """
使用 Rust 执行完整种子过滤入口,返回原列表下标优先级。 使用 Rust 执行完整种子过滤入口,返回原列表下标优先级和可选调试日志
""" """
if not is_enabled(): if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.filter_torrents_fast( args = (
groups, groups,
torrent_list, torrent_list,
rule_set, rule_set,
mediainfo, mediainfo,
metainfo_options or {}, metainfo_options or {},
) )
if is_debug_log_enabled() and hasattr(_moviepilot_rust, "filter_torrents_with_trace_fast"):
matched_orders, traces = _moviepilot_rust.filter_torrents_with_trace_fast(*args)
return matched_orders, traces
return _moviepilot_rust.filter_torrents_fast(*args), []
except BaseException as err: except BaseException as err:
_raise_non_rust_panic(err) _raise_non_rust_panic(err)
logger.debug(f"Rust 种子过滤失败,回退 Python:{err}") logger.debug(f"Rust 种子过滤失败,回退 Python:{err}")
return None return None
def is_debug_log_enabled() -> bool:
"""
判断当前日志配置是否会实际输出 debug 日志。
"""
if log_settings.DEBUG:
return True
return getattr(logging, log_settings.LOG_LEVEL.upper(), logging.INFO) <= logging.DEBUG
def parse_indexer_torrents( def parse_indexer_torrents(
html_text: str, html_text: str,
domain: str, domain: str,
+149 -32
View File
@@ -1,4 +1,3 @@
import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
@@ -6,6 +5,7 @@ from unittest.mock import patch
from app.core.context import MediaInfo, TorrentInfo from app.core.context import MediaInfo, TorrentInfo
from app.helper.torrent import TorrentHelper from app.helper.torrent import TorrentHelper
from app.modules.filter import FilterModule from app.modules.filter import FilterModule
from app.utils import rust_accel
class _RuleHelper: class _RuleHelper:
@@ -14,15 +14,24 @@ class _RuleHelper:
""" """
def __init__(self, groups): def __init__(self, groups):
"""
保存测试规则组。
"""
self._groups = groups self._groups = groups
def get_rule_group_by_media(self, media=None, group_names=None): # noqa: ARG002 def get_rule_group_by_media(self, media=None, group_names=None): # noqa: ARG002
"""
按名称返回测试规则组。
"""
if not group_names: if not group_names:
return self._groups return self._groups
return [group for group in self._groups if group.name in group_names] return [group for group in self._groups if group.name in group_names]
def _build_filter_module(rule_string: str, rule_set: dict) -> FilterModule: def _build_filter_module(rule_string: str, rule_set: dict) -> FilterModule:
"""
构造绑定轻量规则仓库的过滤模块。
"""
module = FilterModule() module = FilterModule()
module.rulehelper = _RuleHelper( module.rulehelper = _RuleHelper(
[SimpleNamespace(name="test", rule_string=rule_string)] [SimpleNamespace(name="test", rule_string=rule_string)]
@@ -31,9 +40,10 @@ def _build_filter_module(rule_string: str, rule_set: dict) -> FilterModule:
return module return module
class TorrentFilterTest(unittest.TestCase): def test_filter_torrents_keeps_priority_and_boolean_rule_semantics():
"""
def test_filter_torrents_keeps_priority_and_boolean_rule_semantics(self): 过滤规则应保持优先级和布尔表达式语义。
"""
module = _build_filter_module( module = _build_filter_module(
rule_string="HDR & !BLU > DV", rule_string="HDR & !BLU > DV",
rule_set={ rule_set={
@@ -50,11 +60,15 @@ class TorrentFilterTest(unittest.TestCase):
filtered = module.filter_torrents(rule_groups=["test"], torrent_list=torrents) filtered = module.filter_torrents(rule_groups=["test"], torrent_list=torrents)
self.assertEqual(torrents[:2], filtered) assert torrents[:2] == filtered
self.assertEqual(100, filtered[0].pri_order) assert filtered[0].pri_order == 100
self.assertEqual(99, filtered[1].pri_order) assert filtered[1].pri_order == 99
def test_filter_torrents_keeps_lazy_priority_level_parsing(self):
def test_filter_torrents_keeps_lazy_priority_level_parsing():
"""
命中高优先级规则后不应解析低优先级坏规则。
"""
module = _build_filter_module( module = _build_filter_module(
rule_string="KEEP > (", rule_string="KEEP > (",
rule_set={"KEEP": {"include": "Movie"}}, rule_set={"KEEP": {"include": "Movie"}},
@@ -63,10 +77,14 @@ class TorrentFilterTest(unittest.TestCase):
filtered = module.filter_torrents(rule_groups=["test"], torrent_list=[torrent]) filtered = module.filter_torrents(rule_groups=["test"], torrent_list=[torrent])
self.assertEqual([torrent], filtered) assert [torrent] == filtered
self.assertEqual(100, torrent.pri_order) assert torrent.pri_order == 100
def test_filter_torrents_keeps_sequential_rule_group_semantics(self):
def test_filter_torrents_keeps_sequential_rule_group_semantics():
"""
多个规则组应按顺序逐轮过滤。
"""
module = FilterModule() module = FilterModule()
module.rulehelper = _RuleHelper( module.rulehelper = _RuleHelper(
[ [
@@ -83,10 +101,14 @@ class TorrentFilterTest(unittest.TestCase):
filtered = module.filter_torrents(rule_groups=["first", "second"], torrent_list=[keep, drop]) filtered = module.filter_torrents(rule_groups=["first", "second"], torrent_list=[keep, drop])
self.assertEqual([keep], filtered) assert [keep] == filtered
self.assertEqual(100, keep.pri_order) assert keep.pri_order == 100
def test_filter_torrents_supports_full_rule_fields_in_rust_entry(self):
def test_filter_torrents_supports_full_rule_fields_in_rust_entry():
"""
Rust 过滤入口应支持完整规则字段。
"""
module = _build_filter_module( module = _build_filter_module(
rule_string="TMDB & LABEL & SIZE & SEED & PUB & SITE", rule_string="TMDB & LABEL & SIZE & SEED & PUB & SITE",
rule_set={ rule_set={
@@ -112,10 +134,14 @@ class TorrentFilterTest(unittest.TestCase):
filtered = module.filter_torrents(rule_groups=["test"], torrent_list=[torrent], mediainfo=mediainfo) filtered = module.filter_torrents(rule_groups=["test"], torrent_list=[torrent], mediainfo=mediainfo)
self.assertEqual([torrent], filtered) assert [torrent] == filtered
self.assertEqual(100, torrent.pri_order) assert torrent.pri_order == 100
def test_filter_torrents_uses_rust_entry_without_python_match_fallback(self):
def test_filter_torrents_uses_rust_entry_without_python_match_fallback():
"""
Rust 返回旧格式结果时应保持兼容,并且不进入 Python 过滤兜底。
"""
module = _build_filter_module( module = _build_filter_module(
rule_string="KEEP", rule_string="KEEP",
rule_set={"KEEP": {"include": "Movie"}}, rule_set={"KEEP": {"include": "Movie"}},
@@ -136,9 +162,102 @@ class TorrentFilterTest(unittest.TestCase):
torrent_list=[TorrentInfo(title="Movie", description="")], torrent_list=[TorrentInfo(title="Movie", description="")],
) )
self.assertEqual(1, len(filtered)) assert len(filtered) == 1
def test_filter_torrents_uses_python_fallback_when_rust_disabled(self):
def test_filter_torrents_logs_rust_trace_details():
"""
Rust trace 返回的规则明细应写入过滤模块 debug 日志。
"""
module = _build_filter_module(
rule_string="KEEP",
rule_set={"KEEP": {"include": "Movie"}},
)
with patch("app.modules.filter.rust_accel.filter_torrents",
return_value=([(0, 100)], ["种子 Alpha - Movie 优先级为 1"])) as rust_filter, \
patch("app.modules.filter.logger.debug") as log_debug:
filtered = module.filter_torrents(
rule_groups=["test"],
torrent_list=[TorrentInfo(site_name="Alpha", title="Movie", description="")],
)
assert len(filtered) == 1
rust_filter.assert_called_once()
log_debug.assert_called_with("种子 Alpha - Movie 优先级为 1")
def test_rust_accel_filter_torrents_uses_trace_entry_when_debug_enabled():
"""
debug 日志启用时 Rust 包装层应调用 trace 入口并返回规则明细。
"""
class FakeRustExtension:
"""
提供过滤入口的测试 Rust 扩展替身。
"""
def is_available(self):
"""
声明扩展可用。
"""
return True
def filter_torrents_with_trace_fast(self, *_args):
"""
返回带调试日志的过滤结果。
"""
return [(0, 100)], ["trace"]
def filter_torrents_fast(self, *_args):
"""
普通入口不应在本用例中被调用。
"""
raise AssertionError("trace entry should be used")
with patch.object(rust_accel, "_moviepilot_rust", FakeRustExtension()), \
patch.object(rust_accel, "is_debug_log_enabled", return_value=True):
result = rust_accel.filter_torrents([], [], {})
assert result == ([(0, 100)], ["trace"])
def test_rust_accel_filter_torrents_keeps_fast_entry_without_debug():
"""
debug 日志关闭时 Rust 包装层应继续调用原高速入口。
"""
class FakeRustExtension:
"""
提供过滤入口的测试 Rust 扩展替身。
"""
def is_available(self):
"""
声明扩展可用。
"""
return True
def filter_torrents_with_trace_fast(self, *_args):
"""
trace 入口不应在本用例中被调用。
"""
raise AssertionError("fast entry should be used")
def filter_torrents_fast(self, *_args):
"""
返回普通过滤结果。
"""
return [(0, 100)]
with patch.object(rust_accel, "_moviepilot_rust", FakeRustExtension()), \
patch.object(rust_accel, "is_debug_log_enabled", return_value=False):
result = rust_accel.filter_torrents([], [], {})
assert result == ([(0, 100)], [])
def test_filter_torrents_uses_python_fallback_when_rust_disabled():
""" """
Rust 加速关闭时应使用 Python 过滤路径,并保留规则优先级语义。 Rust 加速关闭时应使用 Python 过滤路径,并保留规则优先级语义。
""" """
@@ -159,11 +278,15 @@ class TorrentFilterTest(unittest.TestCase):
with patch("app.modules.filter.rust_accel.is_enabled", return_value=False): with patch("app.modules.filter.rust_accel.is_enabled", return_value=False):
filtered = module.filter_torrents(rule_groups=["test"], torrent_list=torrents) filtered = module.filter_torrents(rule_groups=["test"], torrent_list=torrents)
self.assertEqual(torrents[:2], filtered) assert torrents[:2] == filtered
self.assertEqual(100, filtered[0].pri_order) assert filtered[0].pri_order == 100
self.assertEqual(99, filtered[1].pri_order) assert filtered[1].pri_order == 99
def test_filter_torrent_keeps_extra_filter_semantics(self):
def test_filter_torrent_keeps_extra_filter_semantics():
"""
普通过滤参数应保持包含、排除和大小规则语义。
"""
torrent = TorrentInfo( torrent = TorrentInfo(
title="Movie 1080p HDR", title="Movie 1080p HDR",
description="中字", description="中字",
@@ -173,8 +296,7 @@ class TorrentFilterTest(unittest.TestCase):
downloadvolumefactor=0, downloadvolumefactor=0,
) )
self.assertTrue( assert TorrentHelper.filter_torrent(
TorrentHelper.filter_torrent(
torrent_info=torrent, torrent_info=torrent,
filter_params={ filter_params={
"include": "中字|free", "include": "中字|free",
@@ -184,16 +306,11 @@ class TorrentFilterTest(unittest.TestCase):
"size": "1000-4000", "size": "1000-4000",
}, },
) )
) assert not TorrentHelper.filter_torrent(
self.assertFalse(
TorrentHelper.filter_torrent(
torrent_info=torrent, torrent_info=torrent,
filter_params={"exclude": "HDR"}, filter_params={"exclude": "HDR"},
) )
) assert not TorrentHelper.filter_torrent(
self.assertFalse(
TorrentHelper.filter_torrent(
torrent_info=torrent, torrent_info=torrent,
filter_params={"size": "<1000"}, filter_params={"size": "<1000"},
) )
)