fix(emby): include image tags in primary image URLs

This commit is contained in:
jxxghp
2026-10-17 09:36:25 +08:00
parent b26772b147
commit 35199dbba0
2 changed files with 130 additions and 10 deletions

View File

@@ -22,6 +22,8 @@ from app.schemas.types import MediaSource, MediaType
class Emby:
"""Emby媒体服务器客户端。"""
_host: Optional[str] = None
_playhost: Optional[str] = None
_apikey: Optional[str] = None
@@ -175,7 +177,10 @@ class Emby:
library_type = MediaType.TV.value
else:
library_type = MediaType.UNKNOWN.value
image = self.__get_local_image_by_id(library.get("Id"))
image = self.__get_local_image_by_id(
library.get("Id"),
image_tag=(library.get("ImageTags") or {}).get("Primary"),
)
server_id = library.get("ServerId") or self.serverid
server_query = f"serverId={server_id}&" if server_id else ""
libraries.append(
@@ -1251,16 +1256,16 @@ class Emby:
return f"{host_url}Items/{item_id}/" \
f"Images/Backdrop?tag={image_tag}&api_key={self._apikey}"
def __get_local_image_by_id(self, item_id: str) -> str:
def __get_local_image_by_id(self, item_id: str, image_tag: Optional[str] = None) -> str:
"""
根据ItemId从媒体服务器查询本地图片地址
:param: item_id: 在Emby中的ID
:param: remote 是否远程使用,TG微信等客户端调用应为True
:param: inner 是否NT内部调用,为True是会使用NT中转
:param: image_tag: Emby图片版本标签,用于使图片缓存随封面更新失效
"""
if not self._host or not self._apikey:
return ""
return "%sItems/%s/Images/Primary" % (self._host, item_id)
image_url = f"{self._host}Items/{item_id}/Images/Primary"
return f"{image_url}?tag={image_tag}" if image_tag else image_url
def get_resume(self, num: Optional[int] = 12, username: Optional[str] = None) -> Optional[
List[_SchemaMediaServerPlayItem]]:
@@ -1279,7 +1284,7 @@ class Emby:
params = {
"Limit": 100,
"MediaTypes": "Video",
"Fields": "ProductionYear,Path",
"Fields": "ProductionYear,Path,ImageTags",
"api_key": self._apikey,
}
try:
@@ -1312,12 +1317,18 @@ class Emby:
image = self.get_backdrop_url(item_id=item.get("Id"),
image_tag=item.get("BackdropImageTags")[0])
else:
image = self.__get_local_image_by_id(item.get("Id"))
image = self.__get_local_image_by_id(
item.get("Id"),
image_tag=(item.get("ImageTags") or {}).get("Primary"),
)
else:
image = self.get_backdrop_url(item_id=item.get("SeriesId"),
image_tag=item.get("SeriesPrimaryImageTag"))
if not image:
image = self.__get_local_image_by_id(item.get("SeriesId"))
image = self.__get_local_image_by_id(
item.get("SeriesId"),
image_tag=item.get("SeriesPrimaryImageTag"),
)
ret_resume.append(_SchemaMediaServerPlayItem(
id=item.get("Id"),
item_id=item.get("Id"),
@@ -1354,7 +1365,7 @@ class Emby:
params = {
"Limit": 100,
"MediaTypes": "Video",
"Fields": "ProductionYear,Path,BackdropImageTags",
"Fields": "ProductionYear,Path,BackdropImageTags,ImageTags",
"api_key": self._apikey
}
try:
@@ -1376,7 +1387,10 @@ class Emby:
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value
server_id = item.get("ServerId") or self.serverid
link = self.get_play_url(item.get("Id"), server_id=server_id)
image = self.__get_local_image_by_id(item_id=item.get("Id"))
image = self.__get_local_image_by_id(
item_id=item.get("Id"),
image_tag=(item.get("ImageTags") or {}).get("Primary"),
)
ret_latest.append(_SchemaMediaServerPlayItem(
id=item.get("Id"),
item_id=item.get("Id"),

View File

@@ -0,0 +1,106 @@
"""验证 Emby 图片版本标签参与图片 URL 构造。"""
from unittest.mock import Mock, patch
from app.modules.emby.emby import Emby
class _FakeResponse:
"""提供 Emby 接口响应的最小 JSON 封装。"""
def __init__(self, payload):
"""保存测试响应体。"""
self._payload = payload
def json(self):
"""返回测试响应体。"""
return self._payload
def _client() -> Emby:
"""构造跳过远端初始化的 Emby 客户端。"""
client = Emby.__new__(Emby)
client._host = "http://emby.local/"
client._playhost = None
client._apikey = "api-key"
client._sync_libraries = []
client.user = "user-id"
client.serverid = "server-id"
return client
def test_local_primary_image_url_includes_image_tag() -> None:
"""封面标签应成为 URL 的版本参数,使缓存键随原地换图变化。"""
client = _client()
assert client._Emby__get_local_image_by_id("item-id", "primary-tag") == (
"http://emby.local/Items/item-id/Images/Primary?tag=primary-tag"
)
assert client._Emby__get_local_image_by_id("item-id") == (
"http://emby.local/Items/item-id/Images/Primary"
)
def test_get_librarys_uses_primary_image_tag() -> None:
"""媒体库封面应使用 Views 返回的 Primary 标签。"""
client = _client()
client.get_items_count = Mock(return_value=0)
with patch.object(client, "_Emby__get_emby_librarys") as librarys:
librarys.return_value = [{
"Id": "library-id",
"Name": "电影库",
"CollectionType": "movies",
"ImageTags": {"Primary": "library-tag"},
}]
result = client.get_librarys()
assert result[0].image == (
"http://emby.local/Items/library-id/Images/Primary?tag=library-tag"
)
def test_get_resume_uses_primary_image_tag_for_movie_fallback() -> None:
"""继续观看电影没有背景图时应使用电影主图标签。"""
client = _client()
client.get_user_library_folders = Mock(return_value=[])
with patch("app.modules.emby.emby.RequestUtils") as request_utils_cls:
request_utils_cls.return_value.get_res.return_value = _FakeResponse({
"Items": [{
"Id": "movie-id",
"Type": "Movie",
"Name": "测试电影",
"ImageTags": {"Primary": "movie-tag"},
"BackdropImageTags": [],
}]
})
result = client.get_resume()
assert result[0].image == "http://emby.local/Items/movie-id/Images/Primary?tag=movie-tag"
assert request_utils_cls.return_value.get_res.call_args.args[1]["Fields"] == (
"ProductionYear,Path,ImageTags"
)
def test_get_latest_uses_primary_image_tag() -> None:
"""最近入库条目的主图 URL 应携带图片标签。"""
client = _client()
client.get_user_library_folders = Mock(return_value=[])
with patch("app.modules.emby.emby.RequestUtils") as request_utils_cls:
request_utils_cls.return_value.get_res.return_value = _FakeResponse([{
"Id": "movie-id",
"Type": "Movie",
"Name": "测试电影",
"ImageTags": {"Primary": "movie-tag"},
}])
result = client.get_latest()
assert result[0].image == "http://emby.local/Items/movie-id/Images/Primary?tag=movie-tag"
assert request_utils_cls.return_value.get_res.call_args.args[1]["Fields"] == (
"ProductionYear,Path,BackdropImageTags,ImageTags"
)