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)