mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 19:14:01 +08:00
fix(jellyfin): support multiple authentication versions
This commit is contained in:
@@ -52,6 +52,17 @@ class Jellyfin:
|
||||
self.user = self.get_user()
|
||||
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]:
|
||||
"""
|
||||
获取Jellyfin媒体库路径列表
|
||||
@@ -63,7 +74,7 @@ class Jellyfin:
|
||||
'api_key': self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
return res.json()
|
||||
else:
|
||||
@@ -85,7 +96,7 @@ class Jellyfin:
|
||||
'api_key': self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
library_items = res.json()
|
||||
librarys = []
|
||||
@@ -132,7 +143,7 @@ class Jellyfin:
|
||||
return None
|
||||
params = {"api_key": self._apikey}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
items = res.json().get("Items")
|
||||
return items if isinstance(items, list) else None
|
||||
@@ -199,7 +210,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
return len(res.json())
|
||||
else:
|
||||
@@ -220,7 +231,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
users = res.json()
|
||||
# 先查询是否有与当前用户名称匹配的
|
||||
@@ -276,7 +287,7 @@ class Jellyfin:
|
||||
return None
|
||||
url = f"{self._host}Users/authenticatebyname"
|
||||
try:
|
||||
res = RequestUtils(headers={
|
||||
res = self._request(headers={
|
||||
'X-Emby-Authorization': f'MediaBrowser Client="MoviePilot", '
|
||||
f'Device="requests", '
|
||||
f'DeviceId="1", '
|
||||
@@ -313,7 +324,7 @@ class Jellyfin:
|
||||
'api_key': self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
return res.json().get("Id")
|
||||
else:
|
||||
@@ -342,7 +353,7 @@ class Jellyfin:
|
||||
'api_key': self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
result = res.json()
|
||||
return schemas.Statistic(
|
||||
@@ -398,7 +409,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
res_items = res.json().get("Items")
|
||||
if res_items:
|
||||
@@ -435,7 +446,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
res_items = res.json().get("Items")
|
||||
if res_items:
|
||||
@@ -507,7 +518,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res_json = RequestUtils().get_res(url, params)
|
||||
res_json = self._request().get_res(url, params)
|
||||
if res_json:
|
||||
tv_info = res_json.json()
|
||||
res_items = tv_info.get("Items")
|
||||
@@ -548,7 +559,7 @@ class Jellyfin:
|
||||
"isMissing": "false",
|
||||
"api_key": self._apikey
|
||||
}
|
||||
res_json = RequestUtils().get_res(url, params)
|
||||
res_json = self._request().get_res(url, params)
|
||||
if not res_json:
|
||||
return {}
|
||||
episode_ids: Dict[int, str] = {}
|
||||
@@ -575,7 +586,7 @@ class Jellyfin:
|
||||
url = f"{self._host}Items/{item_id}/RemoteImages"
|
||||
params = {"api_key": self._apikey}
|
||||
try:
|
||||
res = RequestUtils(timeout=10).get_res(url, params)
|
||||
res = self._request(timeout=10).get_res(url, params)
|
||||
if res:
|
||||
images = res.json().get("Images") or []
|
||||
for image in images:
|
||||
@@ -600,7 +611,7 @@ class Jellyfin:
|
||||
url = f"{self._host}Items/{item_id}/PlaybackInfo"
|
||||
params = {"api_key": self._apikey}
|
||||
try:
|
||||
res = RequestUtils(timeout=10).get_res(url, params)
|
||||
res = self._request(timeout=10).get_res(url, params)
|
||||
if res:
|
||||
media_sources = res.json().get("MediaSources")
|
||||
if media_sources:
|
||||
@@ -634,7 +645,7 @@ class Jellyfin:
|
||||
_host = self._playhost
|
||||
url = f"{_host}Items/{item_id}/Images/{image_type}"
|
||||
try:
|
||||
res = RequestUtils().get_res(url)
|
||||
res = self._request().get_res(url)
|
||||
if res and res.status_code != 404:
|
||||
logger.info(f"影片图片链接:{res.url}")
|
||||
return res.url
|
||||
@@ -658,7 +669,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
return res.json()[index].get(key)
|
||||
else:
|
||||
@@ -679,7 +690,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().post_res(url, params=params)
|
||||
res = self._request().post_res(url, params=params)
|
||||
if res:
|
||||
return True
|
||||
else:
|
||||
@@ -876,7 +887,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res and res.status_code == 200:
|
||||
return self.__format_item_info(res.json())
|
||||
except Exception as e:
|
||||
@@ -903,7 +914,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey,
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if not res or res.status_code != 200:
|
||||
return None
|
||||
total_count = res.json().get("TotalRecordCount")
|
||||
@@ -937,7 +948,7 @@ class Jellyfin:
|
||||
"Limit": limit
|
||||
})
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if not res or res.status_code != 200:
|
||||
return None
|
||||
items = res.json().get("Items") or []
|
||||
@@ -963,7 +974,7 @@ class Jellyfin:
|
||||
.replace("[APIKEY]", self._apikey or '') \
|
||||
.replace("[USER]", self.user or '')
|
||||
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:
|
||||
logger.error(f"连接Jellyfin出错:" + str(e))
|
||||
return None
|
||||
@@ -981,7 +992,7 @@ class Jellyfin:
|
||||
.replace("[APIKEY]", self._apikey or '') \
|
||||
.replace("[USER]", self.user or '')
|
||||
try:
|
||||
return RequestUtils(
|
||||
return self._request(
|
||||
headers=headers
|
||||
).post_res(url=url, data=data)
|
||||
except Exception as e:
|
||||
@@ -1046,7 +1057,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey,
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
result = res.json().get("Items") or []
|
||||
ret_resume = []
|
||||
@@ -1069,7 +1080,7 @@ class Jellyfin:
|
||||
else:
|
||||
image = self.__get_local_image_by_id(item.get("Id"))
|
||||
# 小部分剧集无[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:
|
||||
image = self.generate_image_link(item.get("Id"), "Backdrop", False)
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
@@ -1115,7 +1126,7 @@ class Jellyfin:
|
||||
"api_key": self._apikey,
|
||||
}
|
||||
try:
|
||||
res = RequestUtils().get_res(url, params)
|
||||
res = self._request().get_res(url, params)
|
||||
if res:
|
||||
result = res.json() or []
|
||||
ret_latest = []
|
||||
|
||||
82
tests/test_jellyfin_auth_compat.py
Normal file
82
tests/test_jellyfin_auth_compat.py
Normal 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",
|
||||
}
|
||||
Reference in New Issue
Block a user