mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
@@ -317,10 +317,18 @@ class Jellyfin:
|
|||||||
def get_medias_count(self) -> schemas.Statistic:
|
def get_medias_count(self) -> schemas.Statistic:
|
||||||
"""
|
"""
|
||||||
获得电影、电视剧、动漫媒体数量
|
获得电影、电视剧、动漫媒体数量
|
||||||
:return: MovieCount SeriesCount SongCount
|
|
||||||
|
优先遍历用户媒体库视图逐库统计:全局 `Items/Counts` 按数据库原始条目
|
||||||
|
计数,同一影片在库内有多个版本/多个文件夹拷贝时会重复累计(#5915),
|
||||||
|
而用户级 `Users/{user}/Items` 查询会折叠版本,与 Jellyfin 页面显示一致。
|
||||||
|
仅在用户视图不可用时回退到 `Items/Counts`。
|
||||||
|
:return: MovieCount SeriesCount EpisodeCount
|
||||||
"""
|
"""
|
||||||
if not self._host or not self._apikey:
|
if not self._host or not self._apikey:
|
||||||
return schemas.Statistic()
|
return schemas.Statistic()
|
||||||
|
stat = self.__count_medias_by_librarys()
|
||||||
|
if stat is not None:
|
||||||
|
return stat
|
||||||
url = f"{self._host}Items/Counts"
|
url = f"{self._host}Items/Counts"
|
||||||
params = {
|
params = {
|
||||||
'api_key': self._apikey
|
'api_key': self._apikey
|
||||||
@@ -341,6 +349,32 @@ class Jellyfin:
|
|||||||
logger.error(f"连接Items/Counts出错:" + str(e))
|
logger.error(f"连接Items/Counts出错:" + str(e))
|
||||||
return schemas.Statistic()
|
return schemas.Statistic()
|
||||||
|
|
||||||
|
def __count_medias_by_librarys(self) -> Optional[schemas.Statistic]:
|
||||||
|
"""
|
||||||
|
遍历用户媒体库视图逐库统计媒体数量
|
||||||
|
|
||||||
|
`Users/{user}/Views` 每个媒体库仅返回一条记录(库包含多个文件夹时
|
||||||
|
也不会重复),按 `CollectionType` 分桶后用用户级条目查询累计。
|
||||||
|
:return: 统计结果,用户或媒体库视图不可用时返回None(由调用方回退)
|
||||||
|
"""
|
||||||
|
if not self.user:
|
||||||
|
return None
|
||||||
|
librarys = self.__get_jellyfin_librarys()
|
||||||
|
if not librarys:
|
||||||
|
return None
|
||||||
|
stat = schemas.Statistic()
|
||||||
|
for library in librarys:
|
||||||
|
library_id = library.get("Id")
|
||||||
|
if not library_id:
|
||||||
|
continue
|
||||||
|
collection_type = library.get("CollectionType")
|
||||||
|
if collection_type == "movies":
|
||||||
|
stat.movie_count += self.get_items_count(library_id, include_item_types="Movie") or 0
|
||||||
|
elif collection_type == "tvshows":
|
||||||
|
stat.tv_count += self.get_items_count(library_id, include_item_types="Series") or 0
|
||||||
|
stat.episode_count += self.get_items_count(library_id, include_item_types="Episode") or 0
|
||||||
|
return stat
|
||||||
|
|
||||||
def __get_jellyfin_series_id_by_name(self, name: str, year: str) -> Optional[str]:
|
def __get_jellyfin_series_id_by_name(self, name: str, year: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
根据名称查询Jellyfin中剧集的SeriesId
|
根据名称查询Jellyfin中剧集的SeriesId
|
||||||
@@ -809,11 +843,13 @@ class Jellyfin:
|
|||||||
logger.error(f"连接Users/{self.user}/Items/{itemid}:" + str(e))
|
logger.error(f"连接Users/{self.user}/Items/{itemid}:" + str(e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_items_count(self, parent: Union[str, int]) -> Optional[int]:
|
def get_items_count(self, parent: Union[str, int],
|
||||||
|
include_item_types: str = "Movie,Series") -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
获取指定媒体库可同步的电影和剧集总数
|
获取指定媒体库可同步的媒体条目总数
|
||||||
|
|
||||||
:param parent: 媒体库ID
|
:param parent: 媒体库ID
|
||||||
|
:param include_item_types: 统计的条目类型,默认电影和剧集
|
||||||
:return: 媒体条目总数,查询失败时返回None
|
:return: 媒体条目总数,查询失败时返回None
|
||||||
"""
|
"""
|
||||||
if not parent or not self._host or not self._apikey or not self.user:
|
if not parent or not self._host or not self._apikey or not self.user:
|
||||||
@@ -822,7 +858,7 @@ class Jellyfin:
|
|||||||
params = {
|
params = {
|
||||||
"ParentId": parent,
|
"ParentId": parent,
|
||||||
"Recursive": "true",
|
"Recursive": "true",
|
||||||
"IncludeItemTypes": "Movie,Series",
|
"IncludeItemTypes": include_item_types,
|
||||||
"Limit": 0,
|
"Limit": 0,
|
||||||
"api_key": self._apikey,
|
"api_key": self._apikey,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from app.modules.jellyfin.jellyfin import Jellyfin
|
||||||
|
|
||||||
|
|
||||||
|
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 _make_client(user: str = "user-id") -> Jellyfin:
|
||||||
|
"""构造跳过初始化的 Jellyfin 客户端。"""
|
||||||
|
client = Jellyfin.__new__(Jellyfin)
|
||||||
|
client._host = "http://media.local/"
|
||||||
|
client._apikey = "token"
|
||||||
|
client.user = user
|
||||||
|
client._sync_libraries = []
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _routed_get_res(views: dict, counts: dict, global_counts: dict = None):
|
||||||
|
"""按 URL 分发的 get_res 模拟:视图列表、单库统计与全局统计。"""
|
||||||
|
|
||||||
|
def _get_res(url, params=None, **_kwargs):
|
||||||
|
if url.endswith("/Views"):
|
||||||
|
return _FakeResponse(views)
|
||||||
|
if url.endswith("/Items") and params:
|
||||||
|
key = (params.get("ParentId"), params.get("IncludeItemTypes"))
|
||||||
|
return _FakeResponse({"TotalRecordCount": counts.get(key, 0)})
|
||||||
|
if url.endswith("Items/Counts"):
|
||||||
|
return _FakeResponse(global_counts or {})
|
||||||
|
raise AssertionError(f"意外的请求地址:{url}")
|
||||||
|
|
||||||
|
return _get_res
|
||||||
|
|
||||||
|
|
||||||
|
def test_medias_count_deduplicates_multi_folder_library():
|
||||||
|
"""多文件夹电影库应按用户视图统计,避免版本重复计数(#5915)。"""
|
||||||
|
client = _make_client()
|
||||||
|
views = {"Items": [{"Id": "lib-movie", "CollectionType": "movies"}]}
|
||||||
|
# 用户级查询会折叠同一影片的多个版本,返回 67 而非数据库原始行数 201
|
||||||
|
counts = {("lib-movie", "Movie"): 67}
|
||||||
|
|
||||||
|
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
|
||||||
|
request_utils_cls.return_value.get_res.side_effect = _routed_get_res(
|
||||||
|
views, counts, global_counts={"MovieCount": 201}
|
||||||
|
)
|
||||||
|
stat = client.get_medias_count()
|
||||||
|
|
||||||
|
assert stat.movie_count == 67
|
||||||
|
assert stat.tv_count == 0
|
||||||
|
assert stat.episode_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_medias_count_buckets_by_collection_type():
|
||||||
|
"""电影与剧集库应按视图类型分桶累计,未知类型库不参与统计。"""
|
||||||
|
client = _make_client()
|
||||||
|
views = {
|
||||||
|
"Items": [
|
||||||
|
{"Id": "lib-movie", "CollectionType": "movies"},
|
||||||
|
{"Id": "lib-tv", "CollectionType": "tvshows"},
|
||||||
|
{"Id": "lib-music", "CollectionType": "music"},
|
||||||
|
{"Id": None, "CollectionType": "movies"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
counts = {
|
||||||
|
("lib-movie", "Movie"): 12,
|
||||||
|
("lib-tv", "Series"): 3,
|
||||||
|
("lib-tv", "Episode"): 45,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
|
||||||
|
request_utils_cls.return_value.get_res.side_effect = _routed_get_res(
|
||||||
|
views, counts
|
||||||
|
)
|
||||||
|
stat = client.get_medias_count()
|
||||||
|
|
||||||
|
assert stat.movie_count == 12
|
||||||
|
assert stat.tv_count == 3
|
||||||
|
assert stat.episode_count == 45
|
||||||
|
|
||||||
|
|
||||||
|
def test_medias_count_falls_back_without_user():
|
||||||
|
"""无可用用户时应回退到全局 Items/Counts 统计。"""
|
||||||
|
client = _make_client(user=None)
|
||||||
|
|
||||||
|
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
|
||||||
|
request_utils_cls.return_value.get_res.return_value = _FakeResponse(
|
||||||
|
{"MovieCount": 5, "SeriesCount": 2, "EpisodeCount": 30}
|
||||||
|
)
|
||||||
|
stat = client.get_medias_count()
|
||||||
|
|
||||||
|
assert stat.movie_count == 5
|
||||||
|
assert stat.tv_count == 2
|
||||||
|
assert stat.episode_count == 30
|
||||||
|
args = request_utils_cls.return_value.get_res.call_args.args
|
||||||
|
assert args[0] == "http://media.local/Items/Counts"
|
||||||
|
|
||||||
|
|
||||||
|
def test_medias_count_falls_back_when_views_unavailable():
|
||||||
|
"""媒体库视图查询失败时应回退到全局 Items/Counts 统计。"""
|
||||||
|
client = _make_client()
|
||||||
|
|
||||||
|
def _get_res(url, params=None, **_kwargs):
|
||||||
|
if url.endswith("/Views"):
|
||||||
|
return None
|
||||||
|
if url.endswith("Items/Counts"):
|
||||||
|
return _FakeResponse({"MovieCount": 7, "SeriesCount": 1, "EpisodeCount": 9})
|
||||||
|
raise AssertionError(f"意外的请求地址:{url}")
|
||||||
|
|
||||||
|
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
|
||||||
|
request_utils_cls.return_value.get_res.side_effect = _get_res
|
||||||
|
stat = client.get_medias_count()
|
||||||
|
|
||||||
|
assert stat.movie_count == 7
|
||||||
|
assert stat.tv_count == 1
|
||||||
|
assert stat.episode_count == 9
|
||||||
Reference in New Issue
Block a user