diff --git a/app/modules/mediavault/mediavault.py b/app/modules/mediavault/mediavault.py index 9888593d4..199df08ca 100644 --- a/app/modules/mediavault/mediavault.py +++ b/app/modules/mediavault/mediavault.py @@ -244,10 +244,18 @@ class MediaVault: def rows() -> Generator[Dict[str, Any], Any, None]: result: Dict[str, Any] = first page = 1 + seen = 0 while True: batch = result.get("items") or [] + seen += len(batch) yield from batch - if len(batch) < self.PAGE_LIMIT: + total = result.get("total") + # 有可靠总数时以它为准:末页恰好满额也不再多发一次请求, + # 那次多余请求一旦失败会把完整结果误报成服务不可达 + if isinstance(total, int): + if seen >= total: + return + elif len(batch) < self.PAGE_LIMIT: return page += 1 following = self.__query_items( @@ -355,18 +363,19 @@ class MediaVault: if rows is None: return None try: - search_rows = list(rows) + # 逐行检查、命中即返回:预取全部页会让第一页已命中的目标 + # 因后续页请求失败被误报成服务不可达 + for row in rows: + item = self.__format_item_info(row) + if not item or item.title != title: + continue + if year and str(item.year) != str(year): + continue + if not MediaServerIdentityHelper.is_compatible(item, media_source, media_id): + continue + return str(item.item_id) except _SearchInterrupted: return None - for row in search_rows: - item = self.__format_item_info(row) - if not item or item.title != title: - continue - if year and str(item.year) != str(year): - continue - if not MediaServerIdentityHelper.is_compatible(item, media_source, media_id): - continue - return str(item.item_id) return "" def get_season_episode_ids(self, item_id: str, season: int) -> Dict[int, str]: diff --git a/tests/test_mediavault_module.py b/tests/test_mediavault_module.py index 1112e214a..232a63c3f 100644 --- a/tests/test_mediavault_module.py +++ b/tests/test_mediavault_module.py @@ -549,7 +549,8 @@ def _failing_second_page(total_rows: list): 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)] + # total 必须超过一页,否则第一页就判定取完,构造不出「中途失败」 + rows = [_item_row(i, title=f"沙丘{i}") for i in range(MediaVault.PAGE_LIMIT + 1)] client = _client({"/items": _failing_second_page(rows)}) assert client.get_movies(title="沙丘") is None @@ -557,7 +558,33 @@ def test_get_movies_reports_unreachable_when_a_later_page_fails(): 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)] + rows = [_item_row(i, kind="Series", title=f"剧{i}") for i in range(MediaVault.PAGE_LIMIT + 1)] client = _client({"/items": _failing_second_page(rows)}) assert client.get_tv_episodes(title="剧A") == (None, None) + + +def test_search_stops_at_reported_total_without_an_extra_request(): + """末页恰好满额时用 total 判定取完,不再多发一次可能失败的请求。""" + rows = [_item_row(i, title="沙丘", year=2021) for i in range(MediaVault.PAGE_LIMIT)] + client = _client({"/items": _paged_items(rows)}) + + matched = client.get_movies(title="沙丘") + + assert len(matched) == MediaVault.PAGE_LIMIT + assert [call["params"]["page"] for call in client._api.calls] == [1] + + +def test_find_series_returns_first_page_hit_even_if_a_later_page_fails(): + """第一页已命中就该直接返回,不因后续页失败被误报成服务不可达。""" + rows = [_item_row(0, kind="Series", title="剧A", year=2020)] + rows += [_item_row(i, kind="Series", title=f"其它{i}") for i in range(1, MediaVault.PAGE_LIMIT + 1)] + routes = { + "/items": _failing_second_page(rows), + "/items/id-0/episodes": Result(True, {"seasons": {"1": [1]}}), + } + + item_id, seasons = _client(routes).get_tv_episodes(title="剧A", year="2020") + + assert item_id == "id-0" + assert seasons == {1: [1]}