feat(search): 搜索流事件携带过滤前候选资源数 candidate_items

标题搜索流逐批过滤导致候选数丢失,媒体搜索流最终事件补充候选数,供前端在候选全部被过滤规则淘汰时给出友好提示。Closes jxxghp/MoviePilot-Frontend#646
This commit is contained in:
jxxghp
2026-08-10 21:41:04 +08:00
parent a322c4c375
commit 60dc01694f
2 changed files with 49 additions and 2 deletions

View File

@@ -976,11 +976,14 @@ class SearchChain(ChainBase):
logger.info(f'开始渐进式浏览资源,站点:{sites} ...')
contexts: List[Context] = []
# 记录过滤前的候选资源数,供前端在全部被过滤时给出友好提示
candidate_count = 0
if rule_groups is None:
rule_groups = SystemConfigOper().get(SystemConfigKey.SearchFilterRuleGroups) or []
async for event in self.__async_search_all_sites_stream(
keyword=title, sites=sites, page=page, mtype=mtype):
result = event.pop("items", []) or []
candidate_count += len(result)
result = await run_in_threadpool(
self.__filter_title_search_torrents,
torrents=result,
@@ -1012,7 +1015,8 @@ class SearchChain(ChainBase):
"type": "done",
"text": f"搜索完成,共 {len(contexts)} 个资源",
"items": [context.to_dict() for context in contexts],
"total_items": len(contexts)
"total_items": len(contexts),
"candidate_items": candidate_count
}
@staticmethod
@@ -1792,7 +1796,8 @@ class SearchChain(ChainBase):
"value": 100,
"text": f"过滤匹配完成,共 {len(contexts)} 个资源",
"items": final_items,
"total_items": len(contexts)
"total_items": len(contexts),
"candidate_items": len(candidate_contexts)
}
yield {
"type": "done",
@@ -1800,6 +1805,7 @@ class SearchChain(ChainBase):
"text": f"搜索完成,共 {len(contexts)} 个资源",
"items": final_items,
"total_items": len(contexts),
"candidate_items": len(candidate_contexts),
"contexts": contexts
}

View File

@@ -123,8 +123,49 @@ def test_async_search_by_title_stream_filters_batches_before_yield(monkeypatch):
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 done_event["candidate_items"] == 2
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
def test_async_search_by_title_stream_reports_candidates_when_all_filtered(monkeypatch):
"""
标题搜索流在候选全部被过滤时,应在完成事件中报告过滤前的候选资源数。
"""
chain = _make_chain()
drop = TorrentInfo(title="Movie 2026 2160p REMUX", description="")
async def search_stream(**_kwargs):
"""
模拟站点页完成后返回一批候选资源。
"""
yield {
"type": "append",
"stage": "searching",
"items": [drop],
"total_items": 1,
}
chain._SearchChain__async_search_all_sites_stream = search_stream
chain.filter_torrents = lambda **_kwargs: []
_patch_search_filter_rule_groups(monkeypatch, ["exclude-remux"])
async def collect_events():
"""
收集标题搜索流全部事件。
"""
return [
event
async for event in chain.async_search_by_title_stream(title="Movie")
]
events = asyncio.run(collect_events())
done_event = events[-1]
assert done_event["type"] == "done"
assert done_event["total_items"] == 0
assert done_event["candidate_items"] == 1
assert done_event["items"] == []