mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-25 09:34:19 +08:00
fix(zspace): sync complete media library metadata (#5953)
This commit is contained in:
@@ -15,6 +15,9 @@ from app.utils.http import RequestUtils
|
||||
from app.utils.url import UrlUtils
|
||||
|
||||
|
||||
DEFAULT_ITEMS_PAGE_SIZE = 100
|
||||
|
||||
|
||||
class ZSpace:
|
||||
_host: Optional[str] = None
|
||||
_playhost: Optional[str] = None
|
||||
@@ -826,30 +829,53 @@ class ZSpace:
|
||||
if not parent or not self._host or not self._apikey or not self.user:
|
||||
return None
|
||||
url = f"{self._host}emby/Users/{self.user}/Items"
|
||||
params = {
|
||||
"ParentId": parent,
|
||||
"Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId"
|
||||
}
|
||||
if limit is not None and limit != -1:
|
||||
params.update({
|
||||
"StartIndex": start_index,
|
||||
"Limit": limit
|
||||
})
|
||||
try:
|
||||
res = self.__request_utils().get_res(url, params=params)
|
||||
if not res or res.status_code != 200:
|
||||
return None
|
||||
items = res.json().get("Items") or []
|
||||
for item in items:
|
||||
if not item:
|
||||
continue
|
||||
if "Folder" in item.get("Type"):
|
||||
for sub_item in self.get_items(parent=item.get('Id')) or []:
|
||||
yield sub_item
|
||||
elif item.get("Type") in ["Movie", "Series"]:
|
||||
fetch_all = limit is None or limit == -1
|
||||
page_size = DEFAULT_ITEMS_PAGE_SIZE if fetch_all else limit
|
||||
current_start_index = max(start_index or 0, 0)
|
||||
while True:
|
||||
params = {
|
||||
"ParentId": parent,
|
||||
"Recursive": "true",
|
||||
"StartIndex": current_start_index,
|
||||
"Limit": page_size,
|
||||
"Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,"
|
||||
"UserDataPlayCount,UserDataLastPlayedDate,ParentId"
|
||||
}
|
||||
try:
|
||||
res = self.__request_utils().get_res(url, params=params)
|
||||
if not res or res.status_code != 200:
|
||||
return None
|
||||
result = res.json() or {}
|
||||
items = result.get("Items") or []
|
||||
for item in items:
|
||||
if not item or item.get("Type") not in ["Movie", "Series"]:
|
||||
continue
|
||||
provider_ids = item.get("ProviderIds") or {}
|
||||
needs_detail = (
|
||||
not provider_ids.get("Tmdb")
|
||||
or not item.get("ProductionYear")
|
||||
or not item.get("Path")
|
||||
)
|
||||
if needs_detail and item.get("Id"):
|
||||
detail_item = self.get_iteminfo(item.get("Id"))
|
||||
if detail_item:
|
||||
yield detail_item
|
||||
continue
|
||||
yield self.__format_item_info(item)
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:{e}")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:{e}")
|
||||
return None
|
||||
|
||||
if not fetch_all:
|
||||
break
|
||||
current_start_index += len(items)
|
||||
total_count = result.get("TotalRecordCount")
|
||||
if not items or (
|
||||
total_count is not None and current_start_index >= total_count
|
||||
) or (
|
||||
total_count is None and len(items) < page_size
|
||||
):
|
||||
break
|
||||
return None
|
||||
|
||||
def get_webhook_message(self, form: Any, args: dict) -> Optional[schemas.WebhookEventInfo]:
|
||||
|
||||
167
tests/test_zspace_sync_items.py
Normal file
167
tests/test_zspace_sync_items.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.modules.zspace.zspace import ZSpace
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
"""模拟极影视 HTTP 响应。"""
|
||||
|
||||
def __init__(self, payload: dict, status_code: int = 200):
|
||||
self._payload = payload
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self) -> dict:
|
||||
"""返回模拟的 JSON 数据。"""
|
||||
return self._payload
|
||||
|
||||
|
||||
def _build_client() -> ZSpace:
|
||||
"""构造不触发登录流程的极影视客户端。"""
|
||||
client = ZSpace.__new__(ZSpace)
|
||||
client._host = "http://zspace.local/"
|
||||
client._apikey = "zspace-token"
|
||||
client.user = "user-id"
|
||||
return client
|
||||
|
||||
|
||||
def test_get_items_fetches_all_recursive_pages() -> None:
|
||||
"""全量同步应递归查询并持续翻页,且忽略合集外壳。"""
|
||||
client = _build_client()
|
||||
responses = [
|
||||
_FakeResponse({
|
||||
"Items": [
|
||||
{
|
||||
"Id": "movie-1",
|
||||
"Type": "Movie",
|
||||
"Name": "电影一",
|
||||
"ProductionYear": 2025,
|
||||
"ProviderIds": {"Tmdb": "101"},
|
||||
"Path": "/media/movie-1.mkv",
|
||||
},
|
||||
{
|
||||
"Id": "collection-1",
|
||||
"Type": "BoxSet",
|
||||
"Name": "电影合集",
|
||||
},
|
||||
],
|
||||
"TotalRecordCount": 3,
|
||||
}),
|
||||
_FakeResponse({
|
||||
"Items": [
|
||||
{
|
||||
"Id": "series-1",
|
||||
"Type": "Series",
|
||||
"Name": "剧集一",
|
||||
"ProductionYear": 2024,
|
||||
"ProviderIds": {"Tmdb": "202"},
|
||||
"Path": "/media/series-1",
|
||||
},
|
||||
],
|
||||
"TotalRecordCount": 3,
|
||||
}),
|
||||
]
|
||||
|
||||
with patch("app.modules.zspace.zspace.DEFAULT_ITEMS_PAGE_SIZE", 2), 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", "series-1"]
|
||||
calls = request_utils_cls.return_value.get_res.call_args_list
|
||||
assert calls[0].kwargs["params"]["Recursive"] == "true"
|
||||
assert calls[0].kwargs["params"]["StartIndex"] == 0
|
||||
assert calls[0].kwargs["params"]["Limit"] == 2
|
||||
assert calls[1].kwargs["params"]["StartIndex"] == 2
|
||||
assert calls[1].kwargs["params"]["Limit"] == 2
|
||||
|
||||
|
||||
def test_get_items_loads_detail_when_list_metadata_is_incomplete() -> None:
|
||||
"""列表项缺少关键元数据时应使用详情接口补全。"""
|
||||
client = _build_client()
|
||||
responses = [
|
||||
_FakeResponse({
|
||||
"Items": [
|
||||
{
|
||||
"Id": "movie-1",
|
||||
"Type": "Movie",
|
||||
"Name": "疯狂动物城",
|
||||
"ProductionYear": None,
|
||||
"ProviderIds": {},
|
||||
"Path": "",
|
||||
}
|
||||
],
|
||||
"TotalRecordCount": 1,
|
||||
}),
|
||||
_FakeResponse({
|
||||
"Id": "movie-1",
|
||||
"ParentId": "library-id",
|
||||
"Type": "Movie",
|
||||
"Name": "疯狂动物城",
|
||||
"OriginalTitle": "Zootopia",
|
||||
"ProductionYear": 2016,
|
||||
"ProviderIds": {
|
||||
"Tmdb": "269149",
|
||||
"Imdb": "tt2948356",
|
||||
},
|
||||
"Path": "/media/疯狂动物城 (2016)/疯狂动物城.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 len(items) == 1
|
||||
assert items[0].item_id == "movie-1"
|
||||
assert items[0].tmdbid == 269149
|
||||
assert items[0].imdbid == "tt2948356"
|
||||
assert items[0].year == 2016
|
||||
assert items[0].path.endswith("疯狂动物城.mkv")
|
||||
assert request_utils_cls.return_value.get_res.call_args_list[1].args[0] == (
|
||||
"http://zspace.local/emby/Users/user-id/Items/movie-1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_items_uses_total_count_when_server_returns_short_pages() -> None:
|
||||
"""服务端单页少于请求数量时应以总记录数决定是否继续翻页。"""
|
||||
client = _build_client()
|
||||
responses = [
|
||||
_FakeResponse({
|
||||
"Items": [
|
||||
{
|
||||
"Id": "movie-1",
|
||||
"Type": "Movie",
|
||||
"Name": "电影一",
|
||||
"ProductionYear": 2025,
|
||||
"ProviderIds": {"Tmdb": "101"},
|
||||
"Path": "/media/movie-1.mkv",
|
||||
}
|
||||
],
|
||||
"TotalRecordCount": 2,
|
||||
}),
|
||||
_FakeResponse({
|
||||
"Items": [
|
||||
{
|
||||
"Id": "movie-2",
|
||||
"Type": "Movie",
|
||||
"Name": "电影二",
|
||||
"ProductionYear": 2024,
|
||||
"ProviderIds": {"Tmdb": "102"},
|
||||
"Path": "/media/movie-2.mkv",
|
||||
}
|
||||
],
|
||||
"TotalRecordCount": 2,
|
||||
}),
|
||||
]
|
||||
|
||||
with patch("app.modules.zspace.zspace.DEFAULT_ITEMS_PAGE_SIZE", 100), 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"]
|
||||
calls = request_utils_cls.return_value.get_res.call_args_list
|
||||
assert calls[0].kwargs["params"]["StartIndex"] == 0
|
||||
assert calls[1].kwargs["params"]["StartIndex"] == 1
|
||||
Reference in New Issue
Block a user