fix: stop torrent search paging after short pages

This commit is contained in:
jxxghp
2026-05-19 17:54:39 +08:00
parent cdddd8e080
commit 7866aee1de
12 changed files with 497 additions and 82 deletions

View File

@@ -28,6 +28,7 @@ from app.agent.tools.factory import MoviePilotToolFactory
from app.agent import ReplyMode
from app.chain.search import SearchChain
from app.core.config import settings
from app.modules.indexer import IndexerModule
from app.schemas.types import MediaType
@@ -66,6 +67,7 @@ class SearchChainAIRecommendTest(unittest.IsolatedAsyncioTestCase):
chain.load_cache = lambda _filename: None
chain.save_cache = lambda _cache, _filename: None
chain.remove_cache = lambda _filename: None
chain.get_search_page_size = IndexerModule.get_search_page_size
return chain
async def test_start_recommend_task_restores_original_indices(self):
@@ -170,15 +172,29 @@ class SearchChainAIRecommendTest(unittest.IsolatedAsyncioTestCase):
with patch.object(settings, "SEARCH_RESOURCE_PAGES", "bad", create=True):
self.assertEqual([0], SearchChain._build_search_pages(page="bad"))
def test_search_all_sites_requests_configured_pages(self):
def test_search_all_sites_stops_after_short_page(self):
"""
验证普通站点默认按 100 条判断是否继续翻页。
"""
chain = self._make_chain()
requested_pages = []
chain.search_torrents = lambda **kwargs: requested_pages.append(kwargs["page"]) or [
SimpleNamespace(title=f"Result Page {kwargs['page']}", description="")
]
def search_torrents(**kwargs):
"""
模拟前两页满页、第三页不足 100 条,验证不会继续请求第四页。
"""
page = kwargs["page"]
requested_pages.append(page)
count = 100 if page in (0, 1) else 1
return [
SimpleNamespace(title=f"Result Page {page}-{index}", description="")
for index in range(count)
]
chain.search_torrents = search_torrents
with (
patch.object(settings, "SEARCH_RESOURCE_PAGES", 3, create=True),
patch.object(settings, "SEARCH_RESOURCE_PAGES", 4, create=True),
patch("app.chain.search.SystemConfigOper") as system_config_oper,
patch("app.chain.search.SitesHelper") as sites_helper,
patch("app.chain.search.ProgressHelper") as progress_helper,
@@ -200,7 +216,215 @@ class SearchChainAIRecommendTest(unittest.IsolatedAsyncioTestCase):
)
self.assertEqual([0, 1, 2], sorted(requested_pages))
self.assertEqual(3, len(results))
self.assertEqual(201, len(results))
def test_search_all_sites_uses_configured_result_num_for_common_site(self):
"""
验证普通配置站点按 result_num 判断是否继续翻页。
"""
chain = self._make_chain()
requested_pages = []
def search_torrents(**kwargs):
"""
模拟配置站点每页 50 条,第二页不足 50 条后停止。
"""
page = kwargs["page"]
requested_pages.append(page)
count = 50 if page == 0 else 49
return [
SimpleNamespace(title=f"Result Page {page}-{index}", description="")
for index in range(count)
]
chain.search_torrents = search_torrents
with (
patch.object(settings, "SEARCH_RESOURCE_PAGES", 3, create=True),
patch("app.chain.search.SystemConfigOper") as system_config_oper,
patch("app.chain.search.SitesHelper") as sites_helper,
patch("app.chain.search.ProgressHelper") as progress_helper,
):
system_config_oper.return_value.get.return_value = [1]
sites_helper.return_value.get_indexers.return_value = [
{"id": 1, "name": "测试站点", "result_num": 50}
]
progress_helper.return_value = SimpleNamespace(
start=lambda: None,
update=lambda **_kwargs: None,
end=lambda: None,
)
results = chain._SearchChain__search_all_sites(
keyword="keyword",
sites=None,
page=0,
)
self.assertEqual([0, 1], requested_pages)
self.assertEqual(99, len(results))
def test_search_all_sites_uses_parser_page_size_for_yema(self):
"""
验证专用解析器按自身页容量判断,避免 Yema 的 40 条分页被误停。
"""
chain = self._make_chain()
requested_pages = []
def search_torrents(**kwargs):
"""
模拟 Yema 第一页满 40 条,第二页不足 40 条后停止。
"""
page = kwargs["page"]
requested_pages.append(page)
count = 40 if page == 0 else 39
return [
SimpleNamespace(title=f"Result Page {page}-{index}", description="")
for index in range(count)
]
chain.search_torrents = search_torrents
with (
patch.object(settings, "SEARCH_RESOURCE_PAGES", 3, create=True),
patch("app.chain.search.SystemConfigOper") as system_config_oper,
patch("app.chain.search.SitesHelper") as sites_helper,
patch("app.chain.search.ProgressHelper") as progress_helper,
):
system_config_oper.return_value.get.return_value = [1]
sites_helper.return_value.get_indexers.return_value = [
{"id": 1, "name": "测试站点", "parser": "Yema"}
]
progress_helper.return_value = SimpleNamespace(
start=lambda: None,
update=lambda **_kwargs: None,
end=lambda: None,
)
results = chain._SearchChain__search_all_sites(
keyword="keyword",
sites=None,
page=0,
)
self.assertEqual([0, 1], requested_pages)
self.assertEqual(79, len(results))
def test_indexer_module_search_page_size_uses_spider_metadata(self):
"""
验证站点单页容量由索引器模块统一读取,避免搜索链写死 parser 容量。
"""
self.assertEqual(
40,
IndexerModule.get_search_page_size({"parser": "Yema"}, keyword="keyword")
)
self.assertEqual(
50,
IndexerModule.get_search_page_size({"result_num": 50}, keyword="keyword")
)
self.assertIsNone(
IndexerModule.get_search_page_size({"parser": "Haidan"}, keyword="keyword")
)
self.assertIsNone(
IndexerModule.get_search_page_size({"parser": "TorrentLeech"}, keyword="keyword")
)
async def test_async_search_all_sites_stops_after_empty_page(self):
"""
验证异步搜索遇到空页后停止后续翻页。
"""
chain = self._make_chain()
requested_pages = []
async def async_search_torrents(**kwargs):
"""
模拟第二页为空,验证异步搜索不会继续请求后续页。
"""
page = kwargs["page"]
requested_pages.append(page)
count = 100 if page == 0 else 0
return [
SimpleNamespace(title=f"Result Page {page}-{index}", description="")
for index in range(count)
]
chain.async_search_torrents = async_search_torrents
with (
patch.object(settings, "SEARCH_RESOURCE_PAGES", 4, create=True),
patch("app.chain.search.SystemConfigOper") as system_config_oper,
patch("app.chain.search.SitesHelper") as sites_helper,
patch("app.chain.search.ProgressHelper") as progress_helper,
):
system_config_oper.return_value.get.return_value = [1]
sites_helper.return_value.async_get_indexers = AsyncMock(
return_value=[{"id": 1, "name": "测试站点"}]
)
progress_helper.return_value = SimpleNamespace(
start=lambda: None,
update=lambda **_kwargs: None,
end=lambda: None,
)
results = await chain._SearchChain__async_search_all_sites(
keyword="keyword",
sites=None,
page=0,
)
self.assertEqual([0, 1], requested_pages)
self.assertEqual(100, len(results))
async def test_async_search_all_sites_stream_stops_after_short_page(self):
"""
验证渐进式搜索遇到非满页后停止后续翻页。
"""
chain = self._make_chain()
requested_pages = []
async def async_search_torrents(**kwargs):
"""
模拟渐进式搜索第二页不足 100 条,验证不会继续请求第三页。
"""
page = kwargs["page"]
requested_pages.append(page)
count = 100 if page == 0 else 99
return [
SimpleNamespace(title=f"Result Page {page}-{index}", description="")
for index in range(count)
]
chain.async_search_torrents = async_search_torrents
with (
patch.object(settings, "SEARCH_RESOURCE_PAGES", 3, create=True),
patch("app.chain.search.SystemConfigOper") as system_config_oper,
patch("app.chain.search.SitesHelper") as sites_helper,
patch("app.chain.search.ProgressHelper") as progress_helper,
):
system_config_oper.return_value.get.return_value = [1]
sites_helper.return_value.async_get_indexers = AsyncMock(
return_value=[{"id": 1, "name": "测试站点"}]
)
progress_helper.return_value = SimpleNamespace(
start=lambda: None,
update=lambda **_kwargs: None,
end=lambda: None,
)
events = [
event
async for event in chain._SearchChain__async_search_all_sites_stream(
keyword="keyword",
sites=None,
page=0,
)
]
append_events = [event for event in events if event.get("type") == "append"]
self.assertEqual([0, 1], requested_pages)
self.assertEqual([0, 1], [event["page"] for event in append_events])
self.assertEqual(199, append_events[-1]["total_items"])
def test_search_by_id_caches_replayable_search_params_when_caching(self):
chain = self._make_chain()