mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-09 01:16:50 +08:00
Add Rust filter trace logging support
This commit is contained in:
@@ -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
@@ -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
@@ -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"},
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user