From 60dc01694f50ca67b5e4c5be904278542cf81755 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 10 Aug 2026 21:41:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(search):=20=E6=90=9C=E7=B4=A2=E6=B5=81?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E6=90=BA=E5=B8=A6=E8=BF=87=E6=BB=A4=E5=89=8D?= =?UTF-8?q?=E5=80=99=E9=80=89=E8=B5=84=E6=BA=90=E6=95=B0=20candidate=5Fite?= =?UTF-8?q?ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 标题搜索流逐批过滤导致候选数丢失,媒体搜索流最终事件补充候选数,供前端在候选全部被过滤规则淘汰时给出友好提示。Closes jxxghp/MoviePilot-Frontend#646 --- app/chain/search.py | 10 ++++++-- tests/test_search_title_filter.py | 41 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/app/chain/search.py b/app/chain/search.py index b9de50115..30e82028a 100644 --- a/app/chain/search.py +++ b/app/chain/search.py @@ -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 } diff --git a/tests/test_search_title_filter.py b/tests/test_search_title_filter.py index bbbe8c9a0..1119bef62 100644 --- a/tests/test_search_title_filter.py +++ b/tests/test_search_title_filter.py @@ -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"] == []