# -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals ################################################################################################# import time import requests from ..helper.utils import JsonDebugPrinter from ..helper import LazyLogger from ..helper.exceptions import HTTPException from .utils import clean_none_dict_values ################################################################################################# LOG = LazyLogger(__name__) ################################################################################################# class HTTP(object): session = None keep_alive = False def __init__(self, client): self.client = client self.config = client.config def start_session(self): self.session = requests.Session() max_retries = self.config.data["http.max_retries"] self.session.mount( "http://", requests.adapters.HTTPAdapter(max_retries=max_retries) ) self.session.mount( "https://", requests.adapters.HTTPAdapter(max_retries=max_retries) ) def stop_session(self): if self.session is None: return try: LOG.info("--<[ session/%s ]", id(self.session)) self.session.close() except Exception as error: LOG.warning("The requests session could not be terminated: %s", error) def _replace_user_info(self, string): if "{server}" in string: if self.config.data.get("auth.server", None): string = string.replace("{server}", self.config.data["auth.server"]) else: LOG.debug("Server address not set") if "{UserId}" in string: if self.config.data.get("auth.user_id", None): string = string.replace("{UserId}", self.config.data["auth.user_id"]) else: LOG.debug("UserId is not set.") return string def request(self, data, session=None): """Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back 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 """ if not data: raise AttributeError("Request cannot be empty") data = self._request(data) LOG.debug("--->[ http ] %s", JsonDebugPrinter(data)) retry = data.pop("retry", 5) while True: try: r = self._requests( session or self.session or requests, data.pop("type", "GET"), **data ) r.content # release the connection 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) self.client.callback( "ServerUnreachable", {"ServerId": self.config.data["auth.server-id"]}, ) 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: self.client.callback( "AccessRestricted", {"ServerId": self.config.data["auth.server-id"]}, ) raise HTTPException("AccessRestricted", error) else: self.client.callback( "Unauthorized", {"ServerId": self.config.data["auth.server-id"]}, ) self.client.auth.revoke_token() raise HTTPException("Unauthorized", error) elif r.status_code == 400: LOG.warning(error) LOG.warning(data) try: LOG.warning(r.json()) except Exception: LOG.warning(r.text) elif r.status_code == 500: # log and ignore. 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: LOG.error("Request missing Schema. " + str(error)) raise HTTPException( "MissingSchema", {"Id": self.config.data.get("auth.server", "None")} ) else: try: # 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") ) elapsed = int(r.elapsed.total_seconds() * 1000) response = r.json() LOG.debug("---<[ http ][%s ms]", elapsed) LOG.debug(JsonDebugPrinter(response)) return clean_none_dict_values(response) except ValueError: return except TypeError: # Empty json return def _request(self, data): if "url" not in data: data["url"] = "%s/%s" % ( self.config.data.get("auth.server", ""), data.pop("handler", ""), ) self._get_header(data) data["timeout"] = data.get("timeout") or self.config.data["http.timeout"] data["verify"] = data.get("verify") or self.config.data.get("auth.ssl", False) 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) if isinstance(value, str): params[key] = self._replace_user_info(value) def _get_header(self, data): data["headers"] = data.setdefault("headers", {}) if not data["headers"]: data["headers"].update( { "Content-type": "application/json", "Accept-Charset": "UTF-8,*", "Accept-encoding": "gzip", "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"), ), } ) if "x-emby-authorization" not in data["headers"]: self._authorization(data) return data def _authorization(self, data): auth = "MediaBrowser " 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" ) auth += "Version=%s" % self.config.data.get("app.version", "0.0.0") data["headers"].update({"x-emby-authorization": auth}) if self.config.data.get("auth.token") and self.config.data.get("auth.user_id"): auth += ", UserId=%s" % self.config.data.get("auth.user_id") data["headers"].update( { "x-emby-authorization": auth, "X-MediaBrowser-Token": self.config.data.get("auth.token"), } ) 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)