diff --git a/plugin.video.external.library/addon.xml b/plugin.video.external.library/addon.xml index c610ab4..2a4a6c4 100644 --- a/plugin.video.external.library/addon.xml +++ b/plugin.video.external.library/addon.xml @@ -5,6 +5,7 @@ provider-name="Roman V.M."> + video diff --git a/plugin.video.external.library/libs/json_rpc_api.py b/plugin.video.external.library/libs/json_rpc_api.py index da7c943..14e6f2b 100644 --- a/plugin.video.external.library/libs/json_rpc_api.py +++ b/plugin.video.external.library/libs/json_rpc_api.py @@ -18,7 +18,8 @@ from pprint import pformat from typing import List, Dict, Any, Optional -from libs import simple_requests as requests +import simple_requests as requests + from libs.exceptions import NoDataError, RemoteKodiError from libs.kodi_service import ADDON, get_remote_kodi_url diff --git a/plugin.video.external.library/libs/simple_requests.py b/plugin.video.external.library/libs/simple_requests.py deleted file mode 100644 index 9161b50..0000000 --- a/plugin.video.external.library/libs/simple_requests.py +++ /dev/null @@ -1,252 +0,0 @@ -# Copyright (c) 2021, Roman Miroshnychenko -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -""" -A simple library for making HTTP requests with API similar to the popular "requests" library - -It depends only on the Python standard library. - -Supported: -* HTTP methods: GET, POST -* HTTP and HTTPS. -* Disabling SSL certificates validation. -* Request payload as form data and JSON. -* Custom headers. -* Basic authentication. -* Gzipped response content. - -Not supported: -* Cookies. -* File upload. -""" -import gzip -import io -import json as _json -import ssl -from base64 import b64encode -from email.message import Message -from typing import Optional, Dict, Any, Tuple, Union, List, BinaryIO -from urllib import request as url_request -from urllib.error import HTTPError as _HTTPError -from urllib.parse import urlparse, urlencode - -__all__ = [ - 'RequestException', - 'ConnectionError', - 'HTTPError', - 'get', - 'post', -] - - -class RequestException(IOError): - - def __repr__(self) -> str: - return self.__str__() - - -class ConnectionError(RequestException): - - def __init__(self, message: str, url: str): - super().__init__(message) - self.message = message - self.url = url - - def __str__(self) -> str: - return f'ConnectionError for url {self.url}: {self.message}' - - -class HTTPError(RequestException): - - def __init__(self, response: 'Response'): - self.response = response - - def __str__(self) -> str: - return f'HTTPError: {self.response.status_code} for url: {self.response.url}' - - -class HTTPMessage(Message): - - def update(self, dct: Dict[str, str]) -> None: - for key, value in dct.items(): - self[key] = value - - -class Response: - NULL = object() - - def __init__(self): - self.encoding: str = 'utf-8' - self.status_code: int = -1 - self.headers: Dict[str, str] = {} - self.url: str = '' - self.content: bytes = b'' - self._text = None - self._json = self.NULL - - def __str__(self) -> str: - return f'' - - def __repr__(self) -> str: - return self.__str__() - - @property - def ok(self) -> bool: - return self.status_code < 400 - - @property - def text(self) -> str: - """ - :return: Response payload as decoded text - """ - if self._text is None: - self._text = self.content.decode(self.encoding) - return self._text - - def json(self) -> Optional[Union[Dict[str, Any], List[Any]]]: - try: - if self._json is self.NULL: - self._json = _json.loads(self.content) - return self._json - except ValueError as exc: - raise ValueError('Response content is not a valid JSON') from exc - - def raise_for_status(self) -> None: - if not self.ok: - raise HTTPError(self) - - -def _create_request(url_structure, params=None, data=None, headers=None, auth=None, json=None): - query = url_structure.query - if params is not None: - separator = '&' if query else '' - query += separator + urlencode(params, doseq=True) - full_url = url_structure.scheme + '://' + url_structure.netloc + url_structure.path - if query: - full_url += '?' + query - prepared_headers = HTTPMessage() - if headers is not None: - prepared_headers.update(headers) - body = None - if json is not None: - body = _json.dumps(json).encode('utf-8') - prepared_headers['Content-Type'] = 'application/json' - if body is None and isinstance(data, dict): - body = urlencode(data, doseq=True).encode('ascii') - prepared_headers['Content-Type'] = 'application/x-www-form-urlencoded' - if body is None and isinstance(data, bytes): - body = data - if body is None and isinstance(data, str): - body = data.encode('utf-8') - if body is None and hasattr(data, 'read'): - body = data.read() - if hasattr(data, 'close'): - data.close() - if body is not None and 'Content-Type' not in prepared_headers: - prepared_headers['Content-Type'] = 'application/octet-stream' - if auth is not None: - encoded_credentials = b64encode((auth[0] + ':' + auth[1]).encode('utf-8')).decode('ascii') - prepared_headers['Authorization'] = f'Basic {encoded_credentials}' - if 'Accept-Encoding' not in prepared_headers: - prepared_headers['Accept-Encoding'] = 'gzip' - return url_request.Request(full_url, body, prepared_headers) - - -def post(url: str, - params: Optional[Dict[str, Any]] = None, - data: Optional[Union[Dict[str, Any], str, bytes, BinaryIO]] = None, - headers: Optional[Dict[str, str]] = None, - auth: Optional[Tuple[str, str]] = None, - timeout: Optional[float] = None, - verify: bool = True, - json: Optional[Dict[str, Any]] = None) -> Response: - """ - POST request - - This function assumes that a request body should be encoded with UTF-8 - and by default sends Accept-Encoding: gzip header to receive response content compressed. - - :param url: URL - :param params: URL query params - :param data: request payload as dict, str, bytes or a binary file object. - If "data" or "json" is passed then a POST request is sent. - For str, bytes or file object it's caller's responsibility to provide a proper - 'Content-Type' header. - :param headers: additional headers - :param auth: a tuple of (login, password) for Basic authentication - :param timeout: request timeout in seconds - :param verify: verify SSL certificates - :param json: request payload as JSON. This parameter has precedence over "data", that is, - if it's present then "data" is ignored. - :return: Response object - """ - url_structure = urlparse(url) - request = _create_request(url_structure, params, data, headers, auth, json) - context = None - if url_structure.scheme == 'https': - context = ssl.SSLContext() - if not verify: - context.verify_mode = ssl.CERT_NONE - context.check_hostname = False - fp = None - try: - r = fp = url_request.urlopen(request, timeout=timeout, context=context) - content = fp.read() - except _HTTPError as exc: - r = exc - fp = exc.fp - content = fp.read() - except Exception as exc: - raise ConnectionError(str(exc), request.full_url) from exc - finally: - if fp is not None: - fp.close() - response = Response() - response.status_code = r.status if hasattr(r, 'status') else r.getstatus() - response.headers = r.headers - response.url = r.url if hasattr(r, 'url') else r.geturl() - if r.headers.get('Content-Encoding') == 'gzip': - temp_fo = io.BytesIO(content) - gzip_file = gzip.GzipFile(fileobj=temp_fo) - content = gzip_file.read() - response.content = content - return response - - -def get(url: str, - params: Optional[Dict[str, Any]] = None, - headers: Optional[Dict[str, str]] = None, - auth: Optional[Tuple[str, str]] = None, - timeout: Optional[float] = None, - verify: bool = True) -> Response: - """ - GET request - - This function by default sends Accept-Encoding: gzip header - to receive response content compressed. - - :param url: URL - :param params: URL query params - :param headers: additional headers - :param auth: a tuple of (login, password) for Basic authentication - :param timeout: request timeout in seconds - :param verify: verify SSL certificates - :return: Response object - """ - return post(url=url, params=params, headers=headers, auth=auth, timeout=timeout, verify=verify)