fix(mediaserver): 翻页按总数收敛,剧集定位命中即返回

按代码审查意见再修两处,都与上一轮引入的关键字翻页有关:

- 末页恰好装满时仍会多请求一页。响应里已带可靠总数,改为累计条数达到总数即
  结束,只有没有总数时才按页是否满额判断;那次多余请求一旦失败,会把完整结果
  误报成服务不可达
- 按标题定位剧集先把所有页读完再检查匹配,第一页已命中的目标会因后续页请求
  失败而返回不可达,比改动前更差;改为逐行检查、命中即返回,只有在命中前中断
  才报告不可达

补 2 条回归,均已确认在修正前失败。
This commit is contained in:
thsrite
2026-09-10 17:59:08 +08:00
parent 963a80788e
commit 2b4ba9c927
2 changed files with 49 additions and 13 deletions

View File

@@ -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]:

View File

@@ -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]}