mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07: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.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 = []
|
||||||
|
|||||||
@@ -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