fix: correct Plex notification image lookup

Closes #5700
This commit is contained in:
jxxghp
2026-04-28 09:19:18 +08:00
parent 5d588ee127
commit 483fe55372
2 changed files with 118 additions and 25 deletions
+59 -25
View File
@@ -3,7 +3,6 @@ from pathlib import Path
from typing import List, Optional, Dict, Tuple, Generator, Any, Union from typing import List, Optional, Dict, Tuple, Generator, Any, Union
from urllib.parse import quote_plus from urllib.parse import quote_plus
from plexapi import media
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer from plexapi.server import PlexServer
from requests import Response, Session from requests import Response, Session
@@ -315,51 +314,86 @@ class Plex:
item = self._plex.fetchItem(ekey=ekey) item = self._plex.fetchItem(ekey=ekey)
if not item: if not item:
return None return None
# 直接使用当前条目的 art/thumb 作为 Plex 本地图片兜底。
# 这样既能兼容 webhook 返回带 /children 的 key,也能避免图片列表为空时彻底没有封面。
local_image_url = self.__build_local_image_url(item=item,
image_type=image_type,
plex_url=plex_url)
# 如果配置了外网播放地址以及Token,则默认从Plex媒体服务器获取图片,否则返回有外网地址的图片资源 # 如果配置了外网播放地址以及Token,则默认从Plex媒体服务器获取图片,否则返回有外网地址的图片资源
# Plex外网播放地址这个框里目前可以填两种地址 # Plex外网播放地址这个框里目前可以填两种地址
# 1. Plex的官方转发地址https://app.plex.tv, 2. 自己处理的端口转发地址 # 1. Plex的官方转发地址https://app.plex.tv, 2. 自己处理的端口转发地址
# 如果使用的是1的官方转发地址,那么就不能走这个逻辑,因为官方转发地址无法获取到图片 # 如果使用的是1的官方转发地址,那么就不能走这个逻辑,因为官方转发地址无法获取到图片
if (self._playhost and "app.plex.tv" not in self._playhost if (self._playhost and "app.plex.tv" not in self._playhost
and self._token and plex_url): and self._token and plex_url):
query = {"X-Plex-Token": self._token} image_url = local_image_url
if image_type == "Poster":
if item.thumb:
image_url = UrlUtils.combine_url(host=self._playhost, path=item.thumb, query=query)
else:
# 默认使用art也就是Backdrop进行处理
if item.art:
image_url = UrlUtils.combine_url(host=self._playhost, path=item.art, query=query)
# 这里对episode进行特殊处理,实际上episode的Backdrop是Poster
# 也有个别情况,比如机智的凡人小子episode就是Poster,因此这里把episode的优先级降低,默认还是取art
if not image_url and item.TYPE == "episode" and item.thumb:
image_url = UrlUtils.combine_url(host=self._playhost, path=item.thumb, query=query)
else: else:
if image_type == "Poster": if image_type == "Poster":
images = self._plex.fetchItems(ekey=f"{ekey}/posters", # 这里必须通过 item.posters() 走 Plex 的规范 metadata endpoint
cls=media.Poster) # 不能直接拼原始 item_id,否则 show webhook 返回 /children 时会变成 /children/posters。
images = item.posters() if hasattr(item, "posters") else []
images = images or []
else: else:
# 默认使用art也就是Backdrop进行处理 # 默认使用art也就是Backdrop进行处理
images = self._plex.fetchItems(ekey=f"{ekey}/arts", # 同上,统一通过 item.arts() 取图片列表,避免 /children/arts 这类错误路径。
cls=media.Art) images = item.arts() if hasattr(item, "arts") else []
images = images or []
# 这里对episode进行特殊处理,实际上episode的Backdrop是Poster # 这里对episode进行特殊处理,实际上episode的Backdrop是Poster
# 也有个别情况,比如机智的凡人小子episode就是Poster,因此这里把episode的优先级降低,默认还是取art # 也有个别情况,比如机智的凡人小子episode就是Poster,因此这里把episode的优先级降低,默认还是取art
if not images and item.TYPE == "episode": if not images and item.TYPE == "episode":
images = self._plex.fetchItems(ekey=f"{ekey}/posters", images = item.posters() if hasattr(item, "posters") else []
cls=media.Poster) images = images or []
for image in images: for image in images:
if hasattr(image, "key") and image.key.startswith("http"): image_key = getattr(image, "key", None)
image_url = image.key if image_key and image_key.startswith("http"):
image_url = image_key
break break
# 如果最后还是找不到,则递归父级进行查找 # 某些 Plex 条目没有可直接外链的图片列表,此时退回到当前条目的实际图片地址。
if not image_url and hasattr(item, "parentKey"): if not image_url and local_image_url:
return self.get_remote_image_by_id(item_id=item.parentKey, image_url = local_image_url
# 如果最后还是找不到,则递归父级进行查找
parent_key = getattr(item, "parentKey", None)
if not image_url and parent_key:
return self.get_remote_image_by_id(item_id=parent_key,
image_type=image_type, image_type=image_type,
depth=depth + 1) depth=depth + 1,
plex_url=plex_url)
return image_url return image_url
except Exception as e: except Exception as e:
logger.error(f"获取封面出错:" + str(e)) logger.error(f"获取封面出错:" + str(e))
return None return None
def __build_local_image_url(
self,
item: Any,
image_type: str,
plex_url: Optional[bool] = True
) -> Optional[str]:
"""
构造 Plex 本地图片地址,作为图片列表查询失败时的兜底结果。
"""
if not item or not plex_url or not self._token:
return None
# app.plex.tv 无法直接用于图片获取,因此回退到实际的 Plex 服务地址。
image_host = self._playhost if self._playhost and "app.plex.tv" not in self._playhost else self._host
if not image_host:
return None
query = {"X-Plex-Token": self._token}
item_type = getattr(item, "TYPE", None)
if image_type == "Poster":
image_path = getattr(item, "thumb", None)
else:
image_path = getattr(item, "art", None)
# episode 的背景图经常落在 thumb 上,这里沿用原有特殊处理逻辑。
if not image_path and item_type == "episode":
image_path = getattr(item, "thumb", None)
if not image_path:
return None
return UrlUtils.combine_url(host=image_host, path=image_path, query=query)
def refresh_root_library(self) -> bool: def refresh_root_library(self) -> bool:
""" """
通知Plex刷新整个媒体库 通知Plex刷新整个媒体库
+59
View File
@@ -0,0 +1,59 @@
import unittest
from unittest.mock import Mock
from app.modules.plex.plex import Plex
class PlexImageLookupTest(unittest.TestCase):
def test_get_remote_image_by_id_uses_item_arts_for_children_key(self):
plex = Plex.__new__(Plex)
plex._host = "http://192.168.8.254:32400/"
plex._playhost = None
plex._token = "plex-token"
plex._plex = Mock()
plex._plex.fetchItems.side_effect = AssertionError("should not use raw fetchItems with /children key")
item = Mock()
item.TYPE = "show"
item.art = "/library/metadata/29242/art/1"
item.parentKey = None
item.arts.return_value = [Mock(key="https://image.tmdb.org/t/p/original/test.jpg")]
plex._plex.fetchItem.return_value = item
image_url = plex.get_remote_image_by_id(
item_id="/library/metadata/29242/children",
image_type="Backdrop",
plex_url=False,
)
self.assertEqual(image_url, "https://image.tmdb.org/t/p/original/test.jpg")
item.arts.assert_called_once_with()
plex._plex.fetchItem.assert_called_once_with(ekey="/library/metadata/29242/children")
def test_get_remote_image_by_id_falls_back_to_local_art_url(self):
plex = Plex.__new__(Plex)
plex._host = "http://192.168.8.254:32400/"
plex._playhost = None
plex._token = "plex-token"
plex._plex = Mock()
item = Mock()
item.TYPE = "show"
item.art = "/library/metadata/29242/art/1"
item.parentKey = None
item.arts.return_value = []
plex._plex.fetchItem.return_value = item
image_url = plex.get_remote_image_by_id(
item_id="/library/metadata/29242/children",
image_type="Backdrop",
)
self.assertEqual(
image_url,
"http://192.168.8.254:32400/library/metadata/29242/art/1?X-Plex-Token=plex-token",
)
if __name__ == "__main__":
unittest.main()