mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
feat: add global RUST_ACCEL toggle to enable/disable rust acceleration at runtime
- Introduce RUST_ACCEL config to control all rust fast paths - Fallback to Python implementations when disabled, preserving filter semantics - Expose rust acceleration status in system info API - Update CLI docs to reflect new toggle - Add tests for runtime switch and fallback behavior
This commit is contained in:
@@ -42,6 +42,7 @@ from app.schemas import ConfigChangeEventData
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.utils.crypto import HashUtils
|
||||
from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from app.utils import rust_accel
|
||||
from app.utils.security import SecurityUtils
|
||||
from app.utils.url import UrlUtils
|
||||
from version import APP_VERSION
|
||||
@@ -514,6 +515,7 @@ async def get_env_setting(_: User = Depends(get_current_active_user_async)):
|
||||
"AUTH_VERSION": SitesHelper().auth_version,
|
||||
"INDEXER_VERSION": SitesHelper().indexer_version,
|
||||
"FRONTEND_VERSION": SystemChain().get_frontend_version(),
|
||||
"RUST_ACCEL_ENABLED": rust_accel.is_enabled(),
|
||||
}
|
||||
)
|
||||
return schemas.Response(success=True, data=info)
|
||||
|
||||
@@ -480,6 +480,8 @@ class ConfigModel(BaseModel):
|
||||
# ==================== 性能配置 ====================
|
||||
# 大内存模式
|
||||
BIG_MEMORY_MODE: bool = False
|
||||
# Rust 加速总开关,关闭时所有 Rust 快路径回退到 Python 实现
|
||||
RUST_ACCEL: bool = True
|
||||
# 是否启用编码探测的性能模式
|
||||
ENCODING_DETECTION_PERFORMANCE_MODE: bool = True
|
||||
# 编码探测的最低置信度阈值
|
||||
|
||||
@@ -4,7 +4,7 @@ from functools import lru_cache
|
||||
from typing import List, Tuple, Union, Dict, Optional
|
||||
|
||||
from app.core.context import TorrentInfo, MediaInfo
|
||||
from app.core.metainfo import MetaInfo, clear_rust_parse_options_cache, _rust_parse_options
|
||||
from app.core.metainfo import lear_rust_parse_options_cache, _rust_parse_options
|
||||
from app.helper.rule import RuleHelper
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
@@ -12,7 +12,6 @@ from app.modules.filter.RuleParser import RuleParser
|
||||
from app.modules.filter.builtin_rules import BUILTIN_RULE_SET
|
||||
from app.schemas.types import ModuleType, OtherModulesType, SystemConfigKey
|
||||
from app.utils import rust_accel
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
_SIZE_UNIT = 1024 * 1024
|
||||
@@ -155,6 +154,12 @@ class FilterModule(_ModuleBase):
|
||||
mediainfo=mediainfo,
|
||||
metainfo_options=_rust_parse_options() if self.__needs_metainfo_options(group_defs) else None,
|
||||
)
|
||||
if matched_orders is None:
|
||||
return self.__filter_torrents_with_python(
|
||||
groups=group_defs,
|
||||
torrent_list=torrent_list,
|
||||
mediainfo=mediainfo
|
||||
)
|
||||
ret_torrents = []
|
||||
for index, pri_order in matched_orders:
|
||||
torrent = torrent_list[index]
|
||||
@@ -163,6 +168,31 @@ class FilterModule(_ModuleBase):
|
||||
return ret_torrents
|
||||
return torrent_list
|
||||
|
||||
def __filter_torrents_with_python(self, groups: List[dict],
|
||||
torrent_list: List[TorrentInfo],
|
||||
mediainfo: MediaInfo = None) -> List[TorrentInfo]:
|
||||
"""
|
||||
使用 Python 旧路径过滤种子,供 Rust 加速关闭或不可用时兜底。
|
||||
"""
|
||||
ret_torrents = torrent_list
|
||||
parser = RuleParser()
|
||||
parsed_rule_cache = {}
|
||||
for group in groups:
|
||||
rule_string = group.get("rule_string")
|
||||
if not rule_string:
|
||||
continue
|
||||
ret_torrents = self.__filter_torrents(
|
||||
rule_string=rule_string,
|
||||
rule_name=group.get("name") or rule_string,
|
||||
torrent_list=ret_torrents,
|
||||
mediainfo=mediainfo,
|
||||
parser=parser,
|
||||
parsed_rule_cache=parsed_rule_cache,
|
||||
)
|
||||
if not ret_torrents:
|
||||
break
|
||||
return ret_torrents
|
||||
|
||||
def __needs_metainfo_options(self, groups: List[dict]) -> bool:
|
||||
"""
|
||||
判断当前规则链是否会触发 size_range,避免无大小规则时读取 MetaInfo 运行配置。
|
||||
|
||||
+37
-11
@@ -1,5 +1,6 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
|
||||
try:
|
||||
@@ -18,6 +19,31 @@ def is_available() -> bool:
|
||||
return bool(_moviepilot_rust and _moviepilot_rust.is_available())
|
||||
|
||||
|
||||
def is_config_enabled() -> bool:
|
||||
"""
|
||||
判断系统配置是否允许使用 Rust 加速。
|
||||
"""
|
||||
return bool(settings.RUST_ACCEL, True)
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""
|
||||
判断当前运行时是否实际启用 Rust 加速。
|
||||
"""
|
||||
return is_config_enabled() and is_available()
|
||||
|
||||
|
||||
def status() -> dict:
|
||||
"""
|
||||
返回 Rust 加速能力与开关状态,供系统配置接口展示。
|
||||
"""
|
||||
return {
|
||||
"available": is_available(),
|
||||
"enabled": is_enabled(),
|
||||
"import_error": str(_import_error) if _import_error else "",
|
||||
}
|
||||
|
||||
|
||||
def import_error() -> Optional[Exception]:
|
||||
"""
|
||||
返回 Rust 扩展导入失败的异常,便于调试构建问题。
|
||||
@@ -29,7 +55,7 @@ def parse_filter_rule(expression: str) -> Optional[list]:
|
||||
"""
|
||||
使用 Rust 解析过滤规则表达式,不可用时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_filter_rule_fast(expression)
|
||||
@@ -45,12 +71,12 @@ def filter_torrents(
|
||||
rule_set: dict,
|
||||
mediainfo=None,
|
||||
metainfo_options: Optional[dict] = None,
|
||||
) -> list:
|
||||
) -> Optional[list]:
|
||||
"""
|
||||
使用 Rust 执行完整种子过滤入口,返回原列表下标和优先级。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
raise RuntimeError(f"Rust 扩展不可用,无法执行种子过滤: {_import_error}")
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.filter_torrents_fast(
|
||||
groups,
|
||||
@@ -61,8 +87,8 @@ def filter_torrents(
|
||||
)
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
raise
|
||||
|
||||
logger.debug(f"Rust 种子过滤失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
def parse_indexer_torrents(
|
||||
html_text: str,
|
||||
@@ -75,7 +101,7 @@ def parse_indexer_torrents(
|
||||
"""
|
||||
使用 Rust 批量解析普通配置站点种子列表,不可用时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_indexer_torrents_fast(
|
||||
@@ -96,7 +122,7 @@ def parse_rss_items(xml_text: str, max_items: int = 1000) -> Optional[List[dict]
|
||||
"""
|
||||
使用 Rust 解析 RSS/Atom 条目,不可用或异常时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_rss_items_fast(xml_text, max_items)
|
||||
@@ -110,7 +136,7 @@ def parse_metainfo(title: str, subtitle: Optional[str] = None, options: Optional
|
||||
"""
|
||||
使用 Rust 从标题入口解析 MetaInfo,不可用或异常时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_metainfo_fast(title, subtitle, options or {})
|
||||
@@ -124,7 +150,7 @@ def parse_metainfo_path(path: str, options: Optional[dict] = None) -> Optional[d
|
||||
"""
|
||||
使用 Rust 从路径入口解析 MetaInfoPath,不可用或异常时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_metainfo_path_fast(path, options or {})
|
||||
@@ -138,7 +164,7 @@ def find_metainfo(title: str) -> Optional[dict]:
|
||||
"""
|
||||
使用 Rust 提取标题中的显式媒体标签,不可用或异常时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.find_metainfo_fast(title)
|
||||
|
||||
@@ -162,6 +162,7 @@ moviepilot install deps --config-dir /path/to/moviepilot-config
|
||||
- 会在安装 Python 依赖后构建并安装 `moviepilot_rust` 加速扩展,因此本机需要可用的 Rust `cargo`
|
||||
- 一键安装脚本会自动准备 Rust toolchain 和系统构建工具;手动执行 CLI 安装时,如果未安装 Rust 或本机编译器,请先安装后再执行 `moviepilot install deps`
|
||||
- 如需临时跳过加速扩展构建,可设置 `MOVIEPILOT_SKIP_RUST_ACCEL=1`,但相关核心处理会回退到 Python 实现,性能收益不会生效
|
||||
- 安装完成后可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速;如果后端未加载扩展,该开关会保持关闭且不可操作
|
||||
|
||||
安装前端 release:
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
from app.core.config import settings
|
||||
from app.utils import rust_accel
|
||||
|
||||
|
||||
class _DummyRustExtension:
|
||||
"""
|
||||
测试用 Rust 扩展替身,用来验证总开关会阻止扩展调用。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
"""
|
||||
模拟 Rust 扩展基础能力可用。
|
||||
"""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_filter_rule_fast(_expression: str) -> list:
|
||||
"""
|
||||
如果总开关关闭后仍调用扩展,测试应立即失败。
|
||||
"""
|
||||
raise AssertionError("Rust extension should not be called")
|
||||
|
||||
|
||||
def test_rust_accel_runtime_switch_disables_fast_paths(monkeypatch):
|
||||
"""
|
||||
RUST_ACCEL 关闭时,即便扩展可用也应回退到 Python 路径。
|
||||
"""
|
||||
monkeypatch.setattr(settings, "RUST_ACCEL", False)
|
||||
monkeypatch.setattr(rust_accel, "_moviepilot_rust", _DummyRustExtension())
|
||||
|
||||
assert rust_accel.is_available()
|
||||
assert not rust_accel.is_enabled()
|
||||
assert rust_accel.parse_filter_rule("HDR") is None
|
||||
|
||||
|
||||
def test_rust_accel_status_reports_enabled_state(monkeypatch):
|
||||
"""
|
||||
状态接口应同时体现扩展可用性和配置开关后的实际启用状态。
|
||||
"""
|
||||
monkeypatch.setattr(settings, "RUST_ACCEL", True)
|
||||
monkeypatch.setattr(rust_accel, "_moviepilot_rust", _DummyRustExtension())
|
||||
|
||||
assert rust_accel.status()["available"] is True
|
||||
assert rust_accel.status()["enabled"] is True
|
||||
@@ -1,6 +1,7 @@
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.core.context import MediaInfo, TorrentInfo
|
||||
from app.helper.torrent import TorrentHelper
|
||||
@@ -128,13 +129,40 @@ class TorrentFilterTest(unittest.TestCase):
|
||||
|
||||
module._FilterModule__filter_torrents = fail_python_fallback
|
||||
|
||||
filtered = module.filter_torrents(
|
||||
rule_groups=["test"],
|
||||
torrent_list=[TorrentInfo(title="Movie", description="")],
|
||||
)
|
||||
with patch("app.modules.filter.rust_accel.is_enabled", return_value=True), \
|
||||
patch("app.modules.filter.rust_accel.filter_torrents", return_value=[(0, 100)]):
|
||||
filtered = module.filter_torrents(
|
||||
rule_groups=["test"],
|
||||
torrent_list=[TorrentInfo(title="Movie", description="")],
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(filtered))
|
||||
|
||||
def test_filter_torrents_uses_python_fallback_when_rust_disabled(self):
|
||||
"""
|
||||
Rust 加速关闭时应使用 Python 过滤路径,并保留规则优先级语义。
|
||||
"""
|
||||
module = _build_filter_module(
|
||||
rule_string="HDR & !BLU > DV",
|
||||
rule_set={
|
||||
"HDR": {"include": "HDR"},
|
||||
"DV": {"include": "DOVI"},
|
||||
"BLU": {"include": "BluRay"},
|
||||
},
|
||||
)
|
||||
torrents = [
|
||||
TorrentInfo(title="Movie HDR WEB-DL", description=""),
|
||||
TorrentInfo(title="Movie DOVI", description=""),
|
||||
TorrentInfo(title="Movie HDR BluRay", description=""),
|
||||
]
|
||||
|
||||
with patch("app.modules.filter.rust_accel.is_enabled", return_value=False):
|
||||
filtered = module.filter_torrents(rule_groups=["test"], torrent_list=torrents)
|
||||
|
||||
self.assertEqual(torrents[:2], filtered)
|
||||
self.assertEqual(100, filtered[0].pri_order)
|
||||
self.assertEqual(99, filtered[1].pri_order)
|
||||
|
||||
def test_filter_torrent_keeps_extra_filter_semantics(self):
|
||||
torrent = TorrentInfo(
|
||||
title="Movie 1080p HDR",
|
||||
|
||||
Reference in New Issue
Block a user