2018-09-06 08:36:32 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2020-01-04 02:32:30 +00:00
|
|
|
from __future__ import division, absolute_import, print_function, unicode_literals
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
#################################################################################################
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
2019-01-30 12:42:06 +00:00
|
|
|
import requests
|
2020-01-04 02:32:30 +00:00
|
|
|
|
2021-10-10 18:38:25 +00:00
|
|
|
from ..helper.utils import JsonDebugPrinter
|
|
|
|
from ..helper import LazyLogger
|
|
|
|
from ..helper.exceptions import HTTPException
|
|
|
|
from .utils import clean_none_dict_values
|
2020-04-19 10:07:55 +00:00
|
|
|
|
2018-09-06 08:36:32 +00:00
|
|
|
#################################################################################################
|
|
|
|
|
2020-04-19 01:05:59 +00:00
|
|
|
LOG = LazyLogger(__name__)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
#################################################################################################
|
|
|
|
|
2019-07-09 20:05:28 +00:00
|
|
|
|
2018-09-06 08:36:32 +00:00
|
|
|
class HTTP(object):
|
|
|
|
|
|
|
|
session = None
|
|
|
|
keep_alive = False
|
|
|
|
|
|
|
|
def __init__(self, client):
|
|
|
|
|
|
|
|
self.client = client
|
2019-09-09 00:20:58 +00:00
|
|
|
self.config = client.config
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
def start_session(self):
|
2019-07-09 20:05:28 +00:00
|
|
|
|
2018-09-06 08:36:32 +00:00
|
|
|
self.session = requests.Session()
|
|
|
|
|
2019-09-09 00:20:58 +00:00
|
|
|
max_retries = self.config.data["http.max_retries"]
|
2018-09-06 08:36:32 +00:00
|
|
|
self.session.mount(
|
|
|
|
"http://", requests.adapters.HTTPAdapter(max_retries=max_retries)
|
2024-06-10 09:19:47 +00:00
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
self.session.mount(
|
|
|
|
"https://", requests.adapters.HTTPAdapter(max_retries=max_retries)
|
2024-06-10 09:19:47 +00:00
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
def stop_session(self):
|
2019-07-09 20:05:28 +00:00
|
|
|
|
2018-09-06 08:36:32 +00:00
|
|
|
if self.session is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
2019-10-02 00:59:25 +00:00
|
|
|
LOG.info("--<[ session/%s ]", id(self.session))
|
2018-09-06 08:36:32 +00:00
|
|
|
self.session.close()
|
|
|
|
except Exception as error:
|
2019-10-02 00:59:25 +00:00
|
|
|
LOG.warning("The requests session could not be terminated: %s", error)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
def _replace_user_info(self, string):
|
|
|
|
|
2018-09-15 08:16:37 +00:00
|
|
|
if "{server}" in string:
|
2020-01-05 23:41:26 +00:00
|
|
|
if self.config.data.get("auth.server", None):
|
2020-01-04 02:32:30 +00:00
|
|
|
string = string.replace("{server}", self.config.data["auth.server"])
|
2018-09-15 08:16:37 +00:00
|
|
|
else:
|
2020-01-05 23:41:26 +00:00
|
|
|
LOG.debug("Server address not set")
|
2018-09-15 08:16:37 +00:00
|
|
|
|
2020-08-21 12:56:15 +00:00
|
|
|
if "{UserId}" in string:
|
2020-01-05 23:41:26 +00:00
|
|
|
if self.config.data.get("auth.user_id", None):
|
2020-01-04 02:32:30 +00:00
|
|
|
string = string.replace("{UserId}", self.config.data["auth.user_id"])
|
2018-09-15 08:16:37 +00:00
|
|
|
else:
|
2020-01-05 23:41:26 +00:00
|
|
|
LOG.debug("UserId is not set.")
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
return string
|
|
|
|
|
|
|
|
def request(self, data, session=None):
|
2019-02-02 13:10:33 +00:00
|
|
|
"""Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back
|
2018-09-06 08:36:32 +00:00
|
|
|
data dictionary can contain:
|
|
|
|
type: GET, POST, etc.
|
|
|
|
url: (optional)
|
|
|
|
handler: not considered when url is provided (optional)
|
|
|
|
params: request parameters (optional)
|
|
|
|
json: request body (optional)
|
|
|
|
headers: (optional),
|
|
|
|
verify: ssl certificate, True (verify using device built-in library) or False
|
2024-06-10 09:19:47 +00:00
|
|
|
"""
|
2018-09-06 08:36:32 +00:00
|
|
|
if not data:
|
|
|
|
raise AttributeError("Request cannot be empty")
|
|
|
|
|
|
|
|
data = self._request(data)
|
2020-02-22 15:04:28 +00:00
|
|
|
LOG.debug("--->[ http ] %s", JsonDebugPrinter(data))
|
2018-09-06 08:36:32 +00:00
|
|
|
retry = data.pop("retry", 5)
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
try:
|
|
|
|
r = self._requests(
|
|
|
|
session or self.session or requests, data.pop("type", "GET"), **data
|
|
|
|
)
|
2019-07-09 20:05:28 +00:00
|
|
|
r.content # release the connection
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
if not self.keep_alive and self.session is not None:
|
|
|
|
self.stop_session()
|
|
|
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
|
|
|
except requests.exceptions.ConnectionError as error:
|
|
|
|
if retry:
|
|
|
|
|
|
|
|
retry -= 1
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
LOG.error(error)
|
2019-09-09 00:20:58 +00:00
|
|
|
self.client.callback(
|
|
|
|
"ServerUnreachable",
|
|
|
|
{"ServerId": self.config.data["auth.server-id"]},
|
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
raise HTTPException("ServerUnreachable", error)
|
|
|
|
|
|
|
|
except requests.exceptions.ReadTimeout as error:
|
|
|
|
if retry:
|
|
|
|
|
|
|
|
retry -= 1
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
LOG.error(error)
|
|
|
|
|
|
|
|
raise HTTPException("ReadTimeout", error)
|
|
|
|
|
|
|
|
except requests.exceptions.HTTPError as error:
|
|
|
|
LOG.error(error)
|
|
|
|
|
|
|
|
if r.status_code == 401:
|
|
|
|
|
|
|
|
if "X-Application-Error-Code" in r.headers:
|
2019-09-09 00:20:58 +00:00
|
|
|
self.client.callback(
|
|
|
|
"AccessRestricted",
|
|
|
|
{"ServerId": self.config.data["auth.server-id"]},
|
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
raise HTTPException("AccessRestricted", error)
|
|
|
|
else:
|
2019-09-09 00:20:58 +00:00
|
|
|
self.client.callback(
|
|
|
|
"Unauthorized",
|
|
|
|
{"ServerId": self.config.data["auth.server-id"]},
|
|
|
|
)
|
2019-10-03 02:14:54 +00:00
|
|
|
self.client.auth.revoke_token()
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
raise HTTPException("Unauthorized", error)
|
|
|
|
|
2020-11-23 02:21:01 +00:00
|
|
|
elif r.status_code == 400:
|
|
|
|
LOG.warning(error)
|
|
|
|
LOG.warning(data)
|
|
|
|
try:
|
|
|
|
LOG.warning(r.json())
|
|
|
|
except Exception:
|
2020-11-25 10:54:22 +00:00
|
|
|
LOG.warning(r.text)
|
2020-11-23 02:21:01 +00:00
|
|
|
|
2019-07-09 20:05:28 +00:00
|
|
|
elif r.status_code == 500: # log and ignore.
|
2018-09-06 08:36:32 +00:00
|
|
|
LOG.error("--[ 500 response ] %s", error)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
elif r.status_code == 502:
|
|
|
|
if retry:
|
|
|
|
|
|
|
|
retry -= 1
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
raise HTTPException(r.status_code, error)
|
|
|
|
|
|
|
|
except requests.exceptions.MissingSchema as error:
|
2020-01-05 23:41:26 +00:00
|
|
|
LOG.error("Request missing Schema. " + str(error))
|
|
|
|
raise HTTPException(
|
|
|
|
"MissingSchema", {"Id": self.config.data.get("auth.server", "None")}
|
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
try:
|
2024-09-19 01:03:29 +00:00
|
|
|
# Prefer custom Server-Time header in ISO 8601 format
|
|
|
|
# TODO: Clean up once the probability of most users having
|
|
|
|
# the updated server-side plugin is high.
|
|
|
|
self.config.data["server-time"] = r.headers.get(
|
|
|
|
"Server-Time", r.headers.get("Date")
|
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
elapsed = int(r.elapsed.total_seconds() * 1000)
|
|
|
|
response = r.json()
|
|
|
|
LOG.debug("---<[ http ][%s ms]", elapsed)
|
2020-02-22 15:04:28 +00:00
|
|
|
LOG.debug(JsonDebugPrinter(response))
|
2018-09-06 08:36:32 +00:00
|
|
|
|
2020-08-21 13:09:34 +00:00
|
|
|
return clean_none_dict_values(response)
|
2018-09-06 08:36:32 +00:00
|
|
|
except ValueError:
|
|
|
|
return
|
2021-05-11 23:55:08 +00:00
|
|
|
except TypeError:
|
|
|
|
# Empty json
|
|
|
|
return
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
def _request(self, data):
|
|
|
|
|
|
|
|
if "url" not in data:
|
2020-01-05 23:41:26 +00:00
|
|
|
data["url"] = "%s/%s" % (
|
|
|
|
self.config.data.get("auth.server", ""),
|
|
|
|
data.pop("handler", ""),
|
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
self._get_header(data)
|
2019-09-09 00:20:58 +00:00
|
|
|
data["timeout"] = data.get("timeout") or self.config.data["http.timeout"]
|
2020-01-05 23:41:26 +00:00
|
|
|
data["verify"] = data.get("verify") or self.config.data.get("auth.ssl", False)
|
2018-09-06 08:36:32 +00:00
|
|
|
data["url"] = self._replace_user_info(data["url"])
|
|
|
|
self._process_params(data.get("params") or {})
|
|
|
|
self._process_params(data.get("json") or {})
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
def _process_params(self, params):
|
|
|
|
|
|
|
|
for key in params:
|
|
|
|
value = params[key]
|
|
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
self._process_params(value)
|
|
|
|
|
2024-06-11 04:46:20 +00:00
|
|
|
if isinstance(value, str):
|
2018-09-06 08:36:32 +00:00
|
|
|
params[key] = self._replace_user_info(value)
|
|
|
|
|
|
|
|
def _get_header(self, data):
|
|
|
|
|
|
|
|
data["headers"] = data.setdefault("headers", {})
|
2024-06-10 09:19:47 +00:00
|
|
|
|
2018-09-06 08:36:32 +00:00
|
|
|
if not data["headers"]:
|
|
|
|
data["headers"].update(
|
|
|
|
{
|
|
|
|
"Content-type": "application/json",
|
|
|
|
"Accept-Charset": "UTF-8,*",
|
|
|
|
"Accept-encoding": "gzip",
|
2020-01-05 23:41:26 +00:00
|
|
|
"User-Agent": self.config.data["http.user_agent"]
|
|
|
|
or "%s/%s"
|
|
|
|
% (
|
|
|
|
self.config.data.get("app.name", "Jellyfin for Kodi"),
|
|
|
|
self.config.data.get("app.version", "0.0.0"),
|
2024-06-10 09:19:47 +00:00
|
|
|
),
|
2018-09-06 08:36:32 +00:00
|
|
|
}
|
|
|
|
)
|
2024-06-10 09:19:47 +00:00
|
|
|
|
2019-08-30 13:45:37 +00:00
|
|
|
if "x-emby-authorization" not in data["headers"]:
|
2018-09-06 08:36:32 +00:00
|
|
|
self._authorization(data)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
def _authorization(self, data):
|
|
|
|
|
2019-07-09 20:05:28 +00:00
|
|
|
auth = "MediaBrowser "
|
2020-01-05 23:41:26 +00:00
|
|
|
auth += "Client=%s, " % self.config.data.get("app.name", "Jellyfin for Kodi")
|
|
|
|
auth += "Device=%s, " % self.config.data.get(
|
|
|
|
"app.device_name", "Unknown Device"
|
|
|
|
)
|
|
|
|
auth += "DeviceId=%s, " % self.config.data.get(
|
|
|
|
"app.device_id", "Unknown Device id"
|
2024-06-10 09:19:47 +00:00
|
|
|
)
|
2020-01-05 23:41:26 +00:00
|
|
|
auth += "Version=%s" % self.config.data.get("app.version", "0.0.0")
|
2024-06-10 09:19:47 +00:00
|
|
|
|
2024-06-11 04:46:20 +00:00
|
|
|
data["headers"].update({"x-emby-authorization": auth})
|
2024-06-10 09:19:47 +00:00
|
|
|
|
2019-09-09 00:20:58 +00:00
|
|
|
if self.config.data.get("auth.token") and self.config.data.get("auth.user_id"):
|
2024-06-10 09:19:47 +00:00
|
|
|
|
2020-01-05 23:41:26 +00:00
|
|
|
auth += ", UserId=%s" % self.config.data.get("auth.user_id")
|
2020-05-23 00:46:35 +00:00
|
|
|
data["headers"].update(
|
|
|
|
{
|
2024-06-11 04:46:20 +00:00
|
|
|
"x-emby-authorization": auth,
|
2020-05-23 00:46:35 +00:00
|
|
|
"X-MediaBrowser-Token": self.config.data.get("auth.token"),
|
2024-06-10 09:19:47 +00:00
|
|
|
}
|
2020-05-23 00:46:35 +00:00
|
|
|
)
|
2018-09-06 08:36:32 +00:00
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
def _requests(self, session, action, **kwargs):
|
|
|
|
|
|
|
|
if action == "GET":
|
|
|
|
return session.get(**kwargs)
|
|
|
|
elif action == "POST":
|
|
|
|
return session.post(**kwargs)
|
|
|
|
elif action == "HEAD":
|
|
|
|
return session.head(**kwargs)
|
|
|
|
elif action == "DELETE":
|
|
|
|
return session.delete(**kwargs)
|