From 963a80788ebcdbd7253cbfcc2f39a4c98f756a64 Mon Sep 17 00:00:00 2001 From: thsrite Date: Thu, 10 Sep 2026 17:35:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(mediaserver):=20=E7=BF=BB=E9=A1=B5=E4=B8=AD?= =?UTF-8?q?=E9=80=94=E5=A4=B1=E8=B4=A5=E4=B8=8D=E5=86=8D=E8=A2=AB=E5=BD=93?= =?UTF-8?q?=E6=88=90=E6=9F=A5=E8=AF=A2=E7=BB=93=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增的关键字翻页逻辑只在第一页失败时返回 None,后续页面请求失败会让生成器 正常结束:get_movies 返回残缺列表、按标题定位剧集返回空串,等于把「连不上」 误报成「确定不在库中」,可能触发重复下载。 改为中途失败抛出私有异常并由调用方转成 None,恢复「连接失败返回 None、 未找到返回空结果」的区分;补 2 条回归,均已确认在修正前失败。 --- app/modules/mediavault/mediavault.py | 29 ++++++++++++++++++++++----- tests/test_mediavault_module.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/app/modules/mediavault/mediavault.py b/app/modules/mediavault/mediavault.py index 290740e95..9888593d4 100644 --- a/app/modules/mediavault/mediavault.py +++ b/app/modules/mediavault/mediavault.py @@ -14,6 +14,14 @@ from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem from app.schemas.types import MediaSource, MediaType +class _SearchInterrupted(Exception): + """关键字翻页中途请求失败。 + + 与「查完了」区分开:调用方据此返回 None(服务不可达),而不是空结果 + (确定不在库中)——后者会让上层误判成需要重新下载。 + """ + + class MediaVault: """MediaVault 自建媒体库客户端。 @@ -234,17 +242,20 @@ class MediaVault: return None def rows() -> Generator[Dict[str, Any], Any, None]: - result: Optional[Dict[str, Any]] = first + result: Dict[str, Any] = first page = 1 - while result is not None: + while True: batch = result.get("items") or [] yield from batch if len(batch) < self.PAGE_LIMIT: return page += 1 - result = self.__query_items( + following = self.__query_items( keyword=keyword, kinds=kinds, page=page, page_size=self.PAGE_LIMIT ) + if following is None: + raise _SearchInterrupted + result = following return rows() @@ -273,7 +284,11 @@ class MediaVault: if rows is None: return None movies = [] - for row in rows: + try: + search_rows = list(rows) + except _SearchInterrupted: + return None + for row in search_rows: item = self.__format_item_info(row) if not item or item.title != title: continue @@ -339,7 +354,11 @@ class MediaVault: rows = self.__search_rows(keyword=title, kinds="Series") if rows is None: return None - for row in rows: + try: + search_rows = list(rows) + except _SearchInterrupted: + return None + for row in search_rows: item = self.__format_item_info(row) if not item or item.title != title: continue diff --git a/tests/test_mediavault_module.py b/tests/test_mediavault_module.py index 6c43c6863..1112e214a 100644 --- a/tests/test_mediavault_module.py +++ b/tests/test_mediavault_module.py @@ -531,3 +531,33 @@ def test_play_item_and_backdrop_images_carry_no_credentials(): assert urls assert all("api_key" not in url for url in urls) + + +def _failing_second_page(total_rows: list): + """第一页正常、后续页请求失败,模拟翻页中途断连。""" + calls = {"n": 0} + + def handler(params, _data): + calls["n"] += 1 + if calls["n"] > 1: + return None + size = int(params.get("page_size", 40)) + return Result(True, {"items": total_rows[:size], "total": len(total_rows)}) + + return handler + + +def test_get_movies_reports_unreachable_when_a_later_page_fails(): + """翻页中途断连必须返回 None,不能把残缺结果当成「不在库中」。""" + rows = [_item_row(i, title=f"沙丘{i}") for i in range(MediaVault.PAGE_LIMIT)] + client = _client({"/items": _failing_second_page(rows)}) + + assert client.get_movies(title="沙丘") is None + + +def test_find_series_reports_unreachable_when_a_later_page_fails(): + """同上:剧集定位不能把断连误判成整部剧未入库。""" + rows = [_item_row(i, kind="Series", title=f"剧{i}") for i in range(MediaVault.PAGE_LIMIT)] + client = _client({"/items": _failing_second_page(rows)}) + + assert client.get_tv_episodes(title="剧A") == (None, None)