diff --git a/app/modules/zspace/zspace.py b/app/modules/zspace/zspace.py index ce8f9649..bf82c521 100644 --- a/app/modules/zspace/zspace.py +++ b/app/modules/zspace/zspace.py @@ -848,7 +848,14 @@ class ZSpace: result = res.json() or {} items = result.get("Items") or [] for item in items: - if not item or item.get("Type") not in ["Movie", "Series"]: + if not item: + continue + if item.get("Type") == "BoxSet" and item.get("Id"): + for sub_item in self.get_items(parent=item.get("Id")): + if sub_item: + yield sub_item + continue + if item.get("Type") not in ["Movie", "Series"]: continue provider_ids = item.get("ProviderIds") or {} needs_detail = ( diff --git a/tests/test_zspace_sync_items.py b/tests/test_zspace_sync_items.py index f72baabd..996c8fbe 100644 --- a/tests/test_zspace_sync_items.py +++ b/tests/test_zspace_sync_items.py @@ -25,7 +25,7 @@ def _build_client() -> ZSpace: def test_get_items_fetches_all_recursive_pages() -> None: - """全量同步应递归查询并持续翻页,且忽略合集外壳。""" + """全量同步应递归查询并持续翻页,且忽略非媒体条目。""" client = _build_client() responses = [ _FakeResponse({ @@ -39,9 +39,9 @@ def test_get_items_fetches_all_recursive_pages() -> None: "Path": "/media/movie-1.mkv", }, { - "Id": "collection-1", - "Type": "BoxSet", - "Name": "电影合集", + "Id": "audio-1", + "Type": "Audio", + "Name": "音频一", }, ], "TotalRecordCount": 3, @@ -76,6 +76,104 @@ def test_get_items_fetches_all_recursive_pages() -> None: assert calls[1].kwargs["params"]["Limit"] == 2 +def test_get_items_expands_boxset_movies() -> None: + """电影合集应向下查询并返回全部子电影。""" + client = _build_client() + responses = [ + _FakeResponse({ + "Items": [{ + "Id": "collection-1", + "Type": "BoxSet", + "Name": "疯狂动物城(系列)", + }], + "TotalRecordCount": 1, + }), + _FakeResponse({ + "Items": [ + { + "Id": "movie-1", + "Type": "Movie", + "Name": "疯狂动物城", + "ProductionYear": None, + "ProviderIds": {}, + "Path": "", + }, + { + "Id": "movie-2", + "Type": "Movie", + "Name": "疯狂动物城2", + "ProductionYear": 2025, + "ProviderIds": {"Tmdb": "1084242"}, + "Path": "/media/疯狂动物城2.mkv", + }, + ], + "TotalRecordCount": 2, + }), + _FakeResponse({ + "Id": "movie-1", + "ParentId": "collection-1", + "Type": "Movie", + "Name": "疯狂动物城", + "ProductionYear": 2016, + "ProviderIds": {"Tmdb": "269149"}, + "Path": "/media/疯狂动物城.mkv", + }), + ] + + with patch("app.modules.zspace.zspace.RequestUtils") as request_utils_cls: + request_utils_cls.return_value.get_res.side_effect = responses + items = list(client.get_items(parent="library-id")) + + assert [item.item_id for item in items] == ["movie-1", "movie-2"] + assert [item.tmdbid for item in items] == [269149, 1084242] + calls = request_utils_cls.return_value.get_res.call_args_list + assert calls[0].kwargs["params"]["Recursive"] == "true" + assert calls[1].kwargs["params"]["ParentId"] == "collection-1" + + +def test_get_items_expands_boxset_series() -> None: + """电视剧合集应向下展开并返回全部子 Series。""" + client = _build_client() + responses = [ + _FakeResponse({ + "Items": [{ + "Id": "collection-1", + "Type": "BoxSet", + "Name": "绝命毒师", + }], + "TotalRecordCount": 1, + }), + _FakeResponse({ + "Items": [ + { + "Id": "series-1", + "Type": "Series", + "Name": "绝命毒师 第 1 季", + "ProductionYear": 2008, + "ProviderIds": {"Tmdb": "1396"}, + "Path": "/media/绝命毒师/Season 1", + }, + { + "Id": "series-2", + "Type": "Series", + "Name": "绝命毒师 第 2 季", + "ProductionYear": 2009, + "ProviderIds": {"Tmdb": "1396"}, + "Path": "/media/绝命毒师/Season 2", + }, + ], + "TotalRecordCount": 2, + }), + ] + + with patch("app.modules.zspace.zspace.RequestUtils") as request_utils_cls: + request_utils_cls.return_value.get_res.side_effect = responses + items = list(client.get_items(parent="library-id")) + + assert [item.item_id for item in items] == ["series-1", "series-2"] + assert [item.tmdbid for item in items] == [1396, 1396] + + def test_get_items_loads_detail_when_list_metadata_is_incomplete() -> None: """列表项缺少关键元数据时应使用详情接口补全。""" client = _build_client()