From 222c0f031400ee224a1e74aa04e5cdbdb13a0110 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 27 Oct 2026 07:31:05 +0800 Subject: [PATCH] fix(ugreen): accept null poster wall lists --- app/modules/ugreen/ugreen.py | 4 +- tests/test_mediaserver_review_regressions.py | 60 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/modules/ugreen/ugreen.py b/app/modules/ugreen/ugreen.py index 1c7a0a710..8fea74f1b 100644 --- a/app/modules/ugreen/ugreen.py +++ b/app/modules/ugreen/ugreen.py @@ -1633,6 +1633,7 @@ class Ugreen: 广度优先分页遍历海报墙目录,最多访问 20000 个不同路径。 重复路径只访问一次。任意目录或分页失败都中止,不能将部分结果视为完整媒体库。 + 无视频或子目录时,接口列表字段可为 null,按空列表继续遍历。 :param root_path: 遍历起始目录 :param page_size: 每个目录的分页大小,默认 100 @@ -1663,7 +1664,8 @@ class Ugreen: ) if data is None: raise ConnectionError("读取绿联海报墙目录失败") - if any(field in data and not isinstance(data[field], list) for field in ("video_arr", "folder_arr")): + if any(data.get(field) is not None and not isinstance(data[field], list) + for field in ("video_arr", "folder_arr")): raise ConnectionError("绿联海报墙目录响应格式错误") if not data: break diff --git a/tests/test_mediaserver_review_regressions.py b/tests/test_mediaserver_review_regressions.py index f5a6697c7..b1f5183fb 100644 --- a/tests/test_mediaserver_review_regressions.py +++ b/tests/test_mediaserver_review_regressions.py @@ -400,6 +400,66 @@ def test_partial_library_failure_preserves_older_sqlite_rows(kind, failure, monk assert len(calls) == 2 +def test_ugreen_nullable_lists_preserve_directory_and_page_traversal(monkeypatch, sync_repository): + """ + 仅含子目录的根目录和无子目录的视频分页返回 null 时,仍须同步全部条目。 + """ + pages = iter([ + {"video_arr": None, "folder_arr": [{"path": "/media/subfolder"}], "is_last_page": True}, + {"video_arr": [{"ug_video_info_id": "first", "type": 1, "name": "第一页电影"}], + "folder_arr": None, "is_last_page": False}, + {"video_arr": [{"ug_video_info_id": "second", "type": 1, "name": "第二页电影"}], + "folder_arr": None, "is_last_page": True}, + ]) + + def respond(*_args, **_kwargs): + """ + 在真实协议解析之前按顺序回放三次成功的目录响应。 + """ + return _response({"code": 200, "data": next(pages)}) + + client, calls = _sync_client("ugreen-internal", monkeypatch, respond) + chain = MediaServerChain.__new__(MediaServerChain) + chain.items = lambda **kwargs: client.get_items(kwargs["library_id"]) + repository, factory = sync_repository + assert chain._sync_server_libraries( + "test", [MediaServerLibrary(id="library", name="媒体库")], {"library": None}, repository, + "2026-10-27 00:00:00", None, 1, 1, None, 0, + ) == (2, 2) + with factory() as session: + assert {row.item_id for row in session.query(CachedItem).all()} == {"first", "second"} + assert [(kwargs["params"]["path"], kwargs["params"]["page"]) for _, kwargs in calls] == [ + ("/media", 1), ("/media/subfolder", 1), ("/media/subfolder", 2), + ] + + +@pytest.mark.parametrize("video_arr", [None, []]) +@pytest.mark.parametrize("folder_arr", [None, []]) +def test_ugreen_nullable_empty_lists_finish_enumeration(video_arr, folder_arr, monkeypatch): + """ + 空目录的两个列表均可独立使用 null 或空数组表示,末页应正常结束。 + """ + client, calls = _sync_client("ugreen-internal", monkeypatch, _response({ + "code": 200, + "data": {"video_arr": video_arr, "folder_arr": folder_arr, "is_last_page": True}, + })) + assert list(client.get_items("library")) == [] + assert len(calls) == 1 + + +@pytest.mark.parametrize("field", ["video_arr", "folder_arr"]) +@pytest.mark.parametrize("invalid", [{}, "", 0, False]) +def test_ugreen_non_list_values_still_fail_enumeration(field, invalid, monkeypatch): + """ + 允许 null 不得放行字典、字符串、数字或布尔值,避免将异常响应当作空库。 + """ + data = {"video_arr": None, "folder_arr": None, "is_last_page": True, field: invalid} + client, calls = _sync_client("ugreen-internal", monkeypatch, _response({"code": 200, "data": data})) + with pytest.raises(ConnectionError, match="绿联海报墙目录响应格式错误"): + list(client.get_items("library")) + assert len(calls) == 1 + + @pytest.mark.parametrize("projected", [False, True]) def test_navidrome_partial_page_failure_is_not_successful_enumeration(projected, monkeypatch): """