fix(mediaserver): 翻页中途失败不再被当成查询结束

新增的关键字翻页逻辑只在第一页失败时返回 None,后续页面请求失败会让生成器
正常结束:get_movies 返回残缺列表、按标题定位剧集返回空串,等于把「连不上」
误报成「确定不在库中」,可能触发重复下载。

改为中途失败抛出私有异常并由调用方转成 None,恢复「连接失败返回 None、
未找到返回空结果」的区分;补 2 条回归,均已确认在修正前失败。
This commit is contained in:
thsrite
2026-09-10 17:35:27 +08:00
parent 429b0020be
commit 963a80788e
2 changed files with 54 additions and 5 deletions

View File

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

View File

@@ -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)