fix(jellyfin): support multiple authentication versions

This commit is contained in:
jxxghp
2026-08-03 11:59:58 +08:00
parent c6b94d4908
commit b1b6a81cef
2 changed files with 118 additions and 25 deletions
+36 -25
View File
@@ -52,6 +52,17 @@ class Jellyfin:
self.user = self.get_user() self.user = self.get_user()
self.serverid = self.get_server_id() self.serverid = self.get_server_id()
def _request(self, headers: Optional[dict] = None, **kwargs: Any) -> RequestUtils:
"""创建兼容不同 Jellyfin 版本鉴权方式的请求工具"""
request_headers = dict(headers or {})
if not any(str(name).lower() == "authorization" for name in request_headers):
request_headers["Authorization"] = f'MediaBrowser Token="{self._apikey}"'
if kwargs.get("accept_type") and "Accept" not in request_headers:
request_headers["Accept"] = kwargs["accept_type"]
if kwargs.get("content_type") and "Content-Type" not in request_headers:
request_headers["Content-Type"] = kwargs["content_type"]
return RequestUtils(headers=request_headers, **kwargs)
def get_jellyfin_folders(self) -> List[dict]: def get_jellyfin_folders(self) -> List[dict]:
""" """
获取Jellyfin媒体库路径列表 获取Jellyfin媒体库路径列表
@@ -63,7 +74,7 @@ class Jellyfin:
'api_key': self._apikey 'api_key': self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
return res.json() return res.json()
else: else:
@@ -85,7 +96,7 @@ class Jellyfin:
'api_key': self._apikey 'api_key': self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
library_items = res.json() library_items = res.json()
librarys = [] librarys = []
@@ -132,7 +143,7 @@ class Jellyfin:
return None return None
params = {"api_key": self._apikey} params = {"api_key": self._apikey}
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
items = res.json().get("Items") items = res.json().get("Items")
return items if isinstance(items, list) else None return items if isinstance(items, list) else None
@@ -199,7 +210,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
return len(res.json()) return len(res.json())
else: else:
@@ -220,7 +231,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
users = res.json() users = res.json()
# 先查询是否有与当前用户名称匹配的 # 先查询是否有与当前用户名称匹配的
@@ -276,7 +287,7 @@ class Jellyfin:
return None return None
url = f"{self._host}Users/authenticatebyname" url = f"{self._host}Users/authenticatebyname"
try: try:
res = RequestUtils(headers={ res = self._request(headers={
'X-Emby-Authorization': f'MediaBrowser Client="MoviePilot", ' 'X-Emby-Authorization': f'MediaBrowser Client="MoviePilot", '
f'Device="requests", ' f'Device="requests", '
f'DeviceId="1", ' f'DeviceId="1", '
@@ -313,7 +324,7 @@ class Jellyfin:
'api_key': self._apikey 'api_key': self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
return res.json().get("Id") return res.json().get("Id")
else: else:
@@ -342,7 +353,7 @@ class Jellyfin:
'api_key': self._apikey 'api_key': self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
result = res.json() result = res.json()
return schemas.Statistic( return schemas.Statistic(
@@ -398,7 +409,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
res_items = res.json().get("Items") res_items = res.json().get("Items")
if res_items: if res_items:
@@ -435,7 +446,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
res_items = res.json().get("Items") res_items = res.json().get("Items")
if res_items: if res_items:
@@ -507,7 +518,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res_json = RequestUtils().get_res(url, params) res_json = self._request().get_res(url, params)
if res_json: if res_json:
tv_info = res_json.json() tv_info = res_json.json()
res_items = tv_info.get("Items") res_items = tv_info.get("Items")
@@ -548,7 +559,7 @@ class Jellyfin:
"isMissing": "false", "isMissing": "false",
"api_key": self._apikey "api_key": self._apikey
} }
res_json = RequestUtils().get_res(url, params) res_json = self._request().get_res(url, params)
if not res_json: if not res_json:
return {} return {}
episode_ids: Dict[int, str] = {} episode_ids: Dict[int, str] = {}
@@ -575,7 +586,7 @@ class Jellyfin:
url = f"{self._host}Items/{item_id}/RemoteImages" url = f"{self._host}Items/{item_id}/RemoteImages"
params = {"api_key": self._apikey} params = {"api_key": self._apikey}
try: try:
res = RequestUtils(timeout=10).get_res(url, params) res = self._request(timeout=10).get_res(url, params)
if res: if res:
images = res.json().get("Images") or [] images = res.json().get("Images") or []
for image in images: for image in images:
@@ -600,7 +611,7 @@ class Jellyfin:
url = f"{self._host}Items/{item_id}/PlaybackInfo" url = f"{self._host}Items/{item_id}/PlaybackInfo"
params = {"api_key": self._apikey} params = {"api_key": self._apikey}
try: try:
res = RequestUtils(timeout=10).get_res(url, params) res = self._request(timeout=10).get_res(url, params)
if res: if res:
media_sources = res.json().get("MediaSources") media_sources = res.json().get("MediaSources")
if media_sources: if media_sources:
@@ -634,7 +645,7 @@ class Jellyfin:
_host = self._playhost _host = self._playhost
url = f"{_host}Items/{item_id}/Images/{image_type}" url = f"{_host}Items/{item_id}/Images/{image_type}"
try: try:
res = RequestUtils().get_res(url) res = self._request().get_res(url)
if res and res.status_code != 404: if res and res.status_code != 404:
logger.info(f"影片图片链接:{res.url}") logger.info(f"影片图片链接:{res.url}")
return res.url return res.url
@@ -658,7 +669,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
return res.json()[index].get(key) return res.json()[index].get(key)
else: else:
@@ -679,7 +690,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().post_res(url, params=params) res = self._request().post_res(url, params=params)
if res: if res:
return True return True
else: else:
@@ -876,7 +887,7 @@ class Jellyfin:
"api_key": self._apikey "api_key": self._apikey
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res and res.status_code == 200: if res and res.status_code == 200:
return self.__format_item_info(res.json()) return self.__format_item_info(res.json())
except Exception as e: except Exception as e:
@@ -903,7 +914,7 @@ class Jellyfin:
"api_key": self._apikey, "api_key": self._apikey,
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if not res or res.status_code != 200: if not res or res.status_code != 200:
return None return None
total_count = res.json().get("TotalRecordCount") total_count = res.json().get("TotalRecordCount")
@@ -937,7 +948,7 @@ class Jellyfin:
"Limit": limit "Limit": limit
}) })
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if not res or res.status_code != 200: if not res or res.status_code != 200:
return None return None
items = res.json().get("Items") or [] items = res.json().get("Items") or []
@@ -963,7 +974,7 @@ class Jellyfin:
.replace("[APIKEY]", self._apikey or '') \ .replace("[APIKEY]", self._apikey or '') \
.replace("[USER]", self.user or '') .replace("[USER]", self.user or '')
try: try:
return RequestUtils(accept_type="application/json").get_res(url=url) return self._request(accept_type="application/json").get_res(url=url)
except Exception as e: except Exception as e:
logger.error(f"连接Jellyfin出错:" + str(e)) logger.error(f"连接Jellyfin出错:" + str(e))
return None return None
@@ -981,7 +992,7 @@ class Jellyfin:
.replace("[APIKEY]", self._apikey or '') \ .replace("[APIKEY]", self._apikey or '') \
.replace("[USER]", self.user or '') .replace("[USER]", self.user or '')
try: try:
return RequestUtils( return self._request(
headers=headers headers=headers
).post_res(url=url, data=data) ).post_res(url=url, data=data)
except Exception as e: except Exception as e:
@@ -1046,7 +1057,7 @@ class Jellyfin:
"api_key": self._apikey, "api_key": self._apikey,
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
result = res.json().get("Items") or [] result = res.json().get("Items") or []
ret_resume = [] ret_resume = []
@@ -1069,7 +1080,7 @@ class Jellyfin:
else: else:
image = self.__get_local_image_by_id(item.get("Id")) image = self.__get_local_image_by_id(item.get("Id"))
# 小部分剧集无[xxx-S01E01-thumb.jpg]图片 # 小部分剧集无[xxx-S01E01-thumb.jpg]图片
image_res = RequestUtils().get_res(image) image_res = self._request().get_res(image)
if not image_res or image_res.status_code == 404: if not image_res or image_res.status_code == 404:
image = self.generate_image_link(item.get("Id"), "Backdrop", False) image = self.generate_image_link(item.get("Id"), "Backdrop", False)
if item_type == MediaType.MOVIE.value: if item_type == MediaType.MOVIE.value:
@@ -1115,7 +1126,7 @@ class Jellyfin:
"api_key": self._apikey, "api_key": self._apikey,
} }
try: try:
res = RequestUtils().get_res(url, params) res = self._request().get_res(url, params)
if res: if res:
result = res.json() or [] result = res.json() or []
ret_latest = [] ret_latest = []
+82
View File
@@ -0,0 +1,82 @@
from unittest.mock import patch
from app.modules.jellyfin.jellyfin import Jellyfin
class _FakeResponse:
"""模拟 Jellyfin HTTP 响应。"""
def __init__(self, payload: object):
"""保存响应数据。"""
self._payload = payload
def json(self) -> object:
"""返回模拟的 JSON 数据。"""
return self._payload
def _make_client() -> Jellyfin:
"""构造跳过初始化的 Jellyfin 客户端。"""
client = Jellyfin.__new__(Jellyfin)
client._host = "http://jellyfin.local:8096/"
client._apikey = "api-key"
client._playhost = None
client._sync_libraries = []
client.user = "user-id"
return client
def test_get_user_supports_legacy_query_and_jellyfin_12_header():
"""用户查询应同时兼容旧版查询参数与 Jellyfin 12 请求头鉴权。"""
client = _make_client()
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
request_utils_cls.return_value.get_res.return_value = _FakeResponse(
[{"Id": "user-id", "Name": "admin"}]
)
user_id = client.get_user("admin")
assert user_id == "user-id"
assert request_utils_cls.call_args.kwargs["headers"] == {
"Authorization": 'MediaBrowser Token="api-key"'
}
request_utils_cls.return_value.get_res.assert_called_once_with(
"http://jellyfin.local:8096/Users",
{"api_key": "api-key"},
)
def test_authenticate_preserves_client_headers_and_adds_jellyfin_12_header():
"""用户认证应保留客户端声明并补充 Jellyfin 12 请求头鉴权。"""
client = _make_client()
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
request_utils_cls.return_value.post_res.return_value = _FakeResponse(
{"AccessToken": "user-token"}
)
token = client.authenticate("admin", "password")
assert token == "user-token"
headers = request_utils_cls.call_args.kwargs["headers"]
assert headers["Authorization"] == 'MediaBrowser Token="api-key"'
assert headers["X-Emby-Authorization"].endswith('Token="api-key"')
assert headers["Content-Type"] == "application/json"
assert headers["Accept"] == "application/json"
def test_post_data_preserves_explicit_authorization_header():
"""自定义请求显式提供 Authorization 时不应被服务器密钥覆盖。"""
client = _make_client()
with patch("app.modules.jellyfin.jellyfin.RequestUtils") as request_utils_cls:
client.post_data(
"[HOST]Sessions/Playing",
headers={"Authorization": "Custom token", "X-Test": "value"},
)
assert request_utils_cls.call_args.kwargs["headers"] == {
"Authorization": "Custom token",
"X-Test": "value",
}