mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
fix title search filter rules
This commit is contained in:
+40
-3
@@ -530,7 +530,7 @@ class SearchChain(ChainBase):
|
||||
def search_by_title(self, title: str, page: Optional[int] = 0,
|
||||
sites: List[int] = None, cache_local: Optional[bool] = False) -> List[Context]:
|
||||
"""
|
||||
根据标题搜索资源,不识别不过滤,直接返回站点内容
|
||||
根据标题搜索资源,不识别媒体信息,按默认搜索过滤规则返回站点内容
|
||||
:param title: 标题,为空时返回所有站点首页内容
|
||||
:param page: 页码
|
||||
:param sites: 站点ID列表
|
||||
@@ -552,6 +552,10 @@ class SearchChain(ChainBase):
|
||||
if not torrents:
|
||||
logger.warn(f'{title} 未搜索到资源')
|
||||
return []
|
||||
torrents = self.__filter_title_search_torrents(torrents=torrents)
|
||||
if not torrents:
|
||||
logger.warn(f'{title} 没有符合过滤规则的资源')
|
||||
return []
|
||||
# 组装上下文
|
||||
contexts = [
|
||||
Context(
|
||||
@@ -791,7 +795,7 @@ class SearchChain(ChainBase):
|
||||
async def async_search_by_title(self, title: str, page: Optional[int] = 0,
|
||||
sites: List[int] = None, cache_local: Optional[bool] = False) -> List[Context]:
|
||||
"""
|
||||
根据标题异步搜索资源,不识别不过滤,直接返回站点内容
|
||||
根据标题异步搜索资源,不识别媒体信息,按默认搜索过滤规则返回站点内容
|
||||
:param title: 标题,为空时返回所有站点首页内容
|
||||
:param page: 页码
|
||||
:param sites: 站点ID列表
|
||||
@@ -813,6 +817,10 @@ class SearchChain(ChainBase):
|
||||
if not torrents:
|
||||
logger.warn(f'{title} 未搜索到资源')
|
||||
return []
|
||||
torrents = await run_in_threadpool(self.__filter_title_search_torrents, torrents=torrents)
|
||||
if not torrents:
|
||||
logger.warn(f'{title} 没有符合过滤规则的资源')
|
||||
return []
|
||||
# 组装上下文
|
||||
contexts = [
|
||||
Context(
|
||||
@@ -830,7 +838,7 @@ class SearchChain(ChainBase):
|
||||
sites: List[int] = None,
|
||||
cache_local: Optional[bool] = False) -> AsyncIterator[dict]:
|
||||
"""
|
||||
根据标题渐进式搜索资源,不识别不过滤,按站点完成顺序返回结果
|
||||
根据标题渐进式搜索资源,不识别媒体信息,按默认搜索过滤规则返回结果
|
||||
"""
|
||||
if cache_local:
|
||||
self.cancel_ai_recommend()
|
||||
@@ -845,8 +853,14 @@ class SearchChain(ChainBase):
|
||||
logger.info(f'开始渐进式浏览资源,站点:{sites} ...')
|
||||
|
||||
contexts: List[Context] = []
|
||||
rule_groups: List[str] = SystemConfigOper().get(SystemConfigKey.SearchFilterRuleGroups) or []
|
||||
async for event in self.__async_search_all_sites_stream(keyword=title, sites=sites, page=page):
|
||||
result = event.pop("items", []) or []
|
||||
result = await run_in_threadpool(
|
||||
self.__filter_title_search_torrents,
|
||||
torrents=result,
|
||||
rule_groups=rule_groups,
|
||||
)
|
||||
batch_contexts = [
|
||||
Context(
|
||||
meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description),
|
||||
@@ -876,6 +890,29 @@ class SearchChain(ChainBase):
|
||||
"total_items": len(contexts)
|
||||
}
|
||||
|
||||
def __filter_title_search_torrents(self,
|
||||
torrents: List[TorrentInfo],
|
||||
rule_groups: Optional[List[str]] = None) -> List[TorrentInfo]:
|
||||
"""
|
||||
对标题搜索结果应用默认搜索过滤规则,不执行媒体识别和标题精确匹配。
|
||||
"""
|
||||
if not torrents:
|
||||
return []
|
||||
|
||||
if rule_groups is None:
|
||||
rule_groups = SystemConfigOper().get(SystemConfigKey.SearchFilterRuleGroups) or []
|
||||
if not rule_groups:
|
||||
return torrents
|
||||
|
||||
logger.info(f'开始过滤标题搜索结果,使用规则组:{rule_groups} ...')
|
||||
filtered_torrents = self.filter_torrents(
|
||||
rule_groups=rule_groups,
|
||||
torrent_list=torrents,
|
||||
mediainfo=None,
|
||||
) or []
|
||||
logger.info(f'标题搜索过滤完成,剩余 {len(filtered_torrents)} 个资源')
|
||||
return filtered_torrents
|
||||
|
||||
async def async_search_by_id_stream(self, tmdbid: Optional[int] = None, doubanid: Optional[str] = None,
|
||||
mtype: MediaType = None, area: Optional[str] = "title",
|
||||
season: Optional[int] = None, sites: List[int] = None,
|
||||
|
||||
@@ -12,6 +12,10 @@ from app.utils.string import StringUtils
|
||||
|
||||
@dataclass
|
||||
class TorrentInfo:
|
||||
"""
|
||||
种子搜索结果信息。
|
||||
"""
|
||||
|
||||
# 站点ID
|
||||
site: int = None
|
||||
# 站点名称
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import asyncio
|
||||
import importlib.machinery
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.testing.bootstrap import ensure_optional_stub
|
||||
|
||||
ensure_optional_stub("qbittorrentapi", TorrentFilesList=list)
|
||||
ensure_optional_stub("transmission_rpc", File=object)
|
||||
ensure_optional_stub("psutil", __spec__=importlib.machinery.ModuleSpec("psutil", loader=None))
|
||||
|
||||
from app.chain import search as search_module
|
||||
from app.chain.search import SearchChain
|
||||
from app.core.context import TorrentInfo
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
def _make_chain() -> SearchChain:
|
||||
"""
|
||||
构造不触发外部依赖初始化的搜索链实例。
|
||||
"""
|
||||
chain = object.__new__(SearchChain)
|
||||
chain.cancel_ai_recommend = lambda: None
|
||||
chain.save_last_search_params = lambda **_kwargs: None
|
||||
chain.save_cache = lambda _cache, _filename: None
|
||||
chain.async_save_last_search_params = lambda **_kwargs: None
|
||||
chain.async_save_cache = lambda _cache, _filename: None
|
||||
return chain
|
||||
|
||||
|
||||
def _patch_search_filter_rule_groups(monkeypatch, rule_groups: list[str]) -> None:
|
||||
"""
|
||||
固定搜索默认过滤规则组,避免读取真实系统配置。
|
||||
"""
|
||||
oper = SimpleNamespace(
|
||||
get=lambda key: rule_groups if key == SystemConfigKey.SearchFilterRuleGroups else None
|
||||
)
|
||||
monkeypatch.setattr(search_module, "SystemConfigOper", lambda: oper)
|
||||
|
||||
|
||||
def test_search_by_title_applies_default_search_filter_rule_groups(monkeypatch):
|
||||
"""
|
||||
标题搜索应在组装上下文前应用默认搜索过滤规则。
|
||||
"""
|
||||
chain = _make_chain()
|
||||
keep = TorrentInfo(title="Movie 2026 1080p WEB-DL", description="")
|
||||
drop = TorrentInfo(title="Movie 2026 2160p REMUX", description="")
|
||||
filter_calls = []
|
||||
|
||||
chain._SearchChain__search_all_sites = lambda **_kwargs: [keep, drop]
|
||||
|
||||
def filter_torrents(**kwargs):
|
||||
"""
|
||||
记录过滤参数并模拟排除 REMUX 资源。
|
||||
"""
|
||||
filter_calls.append(kwargs)
|
||||
return [keep]
|
||||
|
||||
chain.filter_torrents = filter_torrents
|
||||
_patch_search_filter_rule_groups(monkeypatch, ["exclude-remux"])
|
||||
|
||||
contexts = chain.search_by_title(title="Movie")
|
||||
|
||||
assert [context.torrent_info for context in contexts] == [keep]
|
||||
assert len(filter_calls) == 1
|
||||
assert filter_calls[0]["rule_groups"] == ["exclude-remux"]
|
||||
assert filter_calls[0]["torrent_list"] == [keep, drop]
|
||||
assert filter_calls[0]["mediainfo"] is None
|
||||
|
||||
|
||||
def test_async_search_by_title_stream_filters_batches_before_yield(monkeypatch):
|
||||
"""
|
||||
标题搜索流应只向前端输出过滤后的批次和最终结果。
|
||||
"""
|
||||
chain = _make_chain()
|
||||
keep = TorrentInfo(title="Movie 2026 1080p WEB-DL", description="")
|
||||
drop = TorrentInfo(title="Movie 2026 2160p REMUX", description="")
|
||||
filter_calls = []
|
||||
|
||||
async def search_stream(**_kwargs):
|
||||
"""
|
||||
模拟站点页完成后返回一批混合资源。
|
||||
"""
|
||||
yield {
|
||||
"type": "append",
|
||||
"stage": "searching",
|
||||
"value": 100,
|
||||
"text": "done",
|
||||
"items": [keep, drop],
|
||||
"site": "测试站点",
|
||||
"site_id": 1,
|
||||
"page": 0,
|
||||
"finished": 1,
|
||||
"total": 1,
|
||||
"total_items": 2,
|
||||
}
|
||||
|
||||
def filter_torrents(**kwargs):
|
||||
"""
|
||||
记录过滤参数并模拟排除 REMUX 资源。
|
||||
"""
|
||||
filter_calls.append(kwargs)
|
||||
return [keep]
|
||||
|
||||
async def collect_events():
|
||||
"""
|
||||
收集标题搜索流全部事件。
|
||||
"""
|
||||
return [
|
||||
event
|
||||
async for event in chain.async_search_by_title_stream(title="Movie")
|
||||
]
|
||||
|
||||
chain._SearchChain__async_search_all_sites_stream = search_stream
|
||||
chain.filter_torrents = filter_torrents
|
||||
_patch_search_filter_rule_groups(monkeypatch, ["exclude-remux"])
|
||||
|
||||
events = asyncio.run(collect_events())
|
||||
|
||||
append_event = events[0]
|
||||
done_event = events[-1]
|
||||
assert append_event["type"] == "append"
|
||||
assert append_event["total_items"] == 1
|
||||
assert [item["torrent_info"]["title"] for item in append_event["items"]] == [keep.title]
|
||||
assert done_event["type"] == "done"
|
||||
assert done_event["total_items"] == 1
|
||||
assert [item["torrent_info"]["title"] for item in done_event["items"]] == [keep.title]
|
||||
assert len(filter_calls) == 1
|
||||
assert filter_calls[0]["rule_groups"] == ["exclude-remux"]
|
||||
assert filter_calls[0]["torrent_list"] == [keep, drop]
|
||||
assert filter_calls[0]["mediainfo"] is None
|
||||
Reference in New Issue
Block a user