Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit c89aeec
Author: Valentin271 <valentin.d77@free.fr>
Date:   Sun Jan 8 17:24:13 2023 +0100

    fixed authenticate user failed while requesting stream, fixes #23

commit 828e4b5
Author: Valentin271 <valentin.d77@free.fr>
Date:   Sun Jan 8 14:17:07 2023 +0100

    added types
  • Loading branch information
Valentin271 committed Jan 8, 2023
1 parent 2fb4fb1 commit 88a98dd
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 57 deletions.
9 changes: 6 additions & 3 deletions src/app/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
from app import routes
from app.http import Router, Api
from app.views.list_view import ListView
from lib.exceptions import ApiException, OAuthException
from lib.exceptions.credentials_exception import CredentialsException
from lib.exceptions import ApiException, OAuthException, EmptyCredentialsException
from lib.helpers.logger import Logger


Expand Down Expand Up @@ -49,13 +48,15 @@ def run(self):
items = []

try:
# Loads an api instance
Api.instance()
items = self.__router.route(self)
except OAuthException as e:
xbmcgui.Dialog().notification(e.header, "Refreshing token ...", "", 1000, False)
Api.clean_cache()
self.run()
return
except CredentialsException:
except EmptyCredentialsException:
self.__addon.openSettings()
self.run()
return
Expand All @@ -73,6 +74,8 @@ def run(self):
xbmcplugin.addDirectoryItems(self.__args.addon_handle, items, len(items))
xbmcplugin.endOfDirectory(self.__args.addon_handle)

Api.instance().save()

def sortable(self, *sorts):
"""
Enable the given sort methods. Order defines UI order
Expand Down
94 changes: 58 additions & 36 deletions src/app/http/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import xbmcvfs

from lib import Settings
from lib.exceptions import ApiExceptionFinder, CredentialsException
from lib.exceptions import ApiExceptionFinder, LoadedCredentialsException, EmptyCredentialsException
from lib.helpers.logger import Logger


Expand Down Expand Up @@ -46,6 +46,14 @@ def instance(cls):
except IOError:
Logger.debug("Api instance not saved, trying to get token ...")

cls.__INSTANCE = cls(
Settings.get('username'),
Settings.get('password')
)
except LoadedCredentialsException:
Logger.warn("Loaded bad API credentials, trying from settings values ...")
cls.clean_cache()

cls.__INSTANCE = cls(
Settings.get('username'),
Settings.get('password')
Expand All @@ -54,7 +62,7 @@ def instance(cls):
return cls.__INSTANCE

@classmethod
def clean_cache(cls):
def clean_cache(cls) -> None:
"""Cleans the API cache"""
Logger.debug("Cleaning API cache ...")
xbmcvfs.delete(Api.__CACHE_FILE)
Expand All @@ -66,19 +74,20 @@ def __init__(self, username: str, password: str):
:param str username: The user's name
:param str password: The user's password
:raise EmptyCredentialsException: If the credentials are empty
"""
Logger.debug("Creating new API connection ...")
self._username = username
self._password = None
self._access_token = None
self._password = ""
self.set_password(password)
self._obtain_access_token()
self._access_token = None

def __del__(self):
"""Saves the Api instance on disk on deletion"""
self.save()
if self.empty_credentials():
raise EmptyCredentialsException("Username and password are required!")

def set_password(self, password: str):
self._obtain_access_token()

def set_password(self, password: str) -> None:
"""
Save md5 of user password.
Expand All @@ -88,7 +97,7 @@ def set_password(self, password: str):
md5.update(password.encode("utf-8"))
self._password = md5.hexdigest()

def save(self):
def save(self) -> None:
"""
Save the connection to a file in kodi special temp folder.
"""
Expand All @@ -98,29 +107,42 @@ def save(self):
pickle.dump(self, file, pickle.HIGHEST_PROTOCOL)

@staticmethod
def load():
def load(cache_file: str = __CACHE_FILE):
"""
Load a connection from a file in kodi special temp folder.
Loads a connection from a file.
:return: an Api object
"""
Logger.debug("Getting Api from file {}".format(Api.__CACHE_FILE))
Logger.debug("Getting Api from file {}".format(cache_file))

with open(Api.__CACHE_FILE, 'rb') as file:
with open(cache_file, 'rb') as file:
cls = pickle.load(file)

if cls.empty_credentials(check_token=True):
raise LoadedCredentialsException("Loaded empty username or password")

return cls

def _obtain_access_token(self):
def empty_credentials(self, check_token: bool = False) -> bool:
"""
Obtain access token by pretending to be a smart tv.
Checks if credentials are empty.
:raise CredentialsException: In case the credentials are empty
:param check_token: Tells wether or not to check the access token
:return: True if any credential is empty, False if there are all filled
"""
Logger.debug("Connection: Getting access token from API ...")
empty_creds = self._username == "" or self._password == ""
empty_tok = False

if check_token:
empty_tok = self._access_token is None

return empty_creds or empty_tok

if self._username is None or self._password is None:
raise CredentialsException("Username and password are required!")
def _obtain_access_token(self) -> None:
"""
Obtain access token by pretending to be a smart tv.
"""
Logger.debug("Connection: Getting access token from API ...")

response = requests.get(self._API_AUTH_URL, params={
'login': self._username,
Expand All @@ -132,19 +154,6 @@ def _obtain_access_token(self):

self._access_token = response['access_token']

@staticmethod
def _merge_two_dicts(lhs: dict, rhs: dict):
"""
Given two dicts, merge them into a new dict as a shallow copy.
:param dict lhs: First dict
:param dict rhs: Second dict
:return: Merged dict
"""
res = lhs.copy()
res.update(rhs)
return res

def request(
self,
service: str,
Expand All @@ -158,7 +167,7 @@ def request(
the Deezer API documentation (https://developers.deezer.com/api).
:param str service: The service to request
:param str identifiant: Item's ID in the service
:param str identifiant: Item's ID in the service
:param str method: Service method
:param dict parameters: Additional parameters at the end
:return: JSON response as dict
Expand All @@ -171,7 +180,7 @@ def request(
parameters = {}

url = self._API_BASE_URL.format(service=service, id=identifiant, method=method)
response = requests.get(url, params=self._merge_two_dicts(
response = requests.get(url, params=_merge_two_dicts(
{'output': 'json', 'access_token': self._access_token},
parameters
)).json()
Expand All @@ -182,7 +191,7 @@ def request(

Api.check_error(response)

return Api.check_error(response)
return response

@staticmethod
def request_url(url: str):
Expand Down Expand Up @@ -243,3 +252,16 @@ def check_error(response):
ApiExceptionFinder.from_error(response['error'])

return response


def _merge_two_dicts(lhs: dict, rhs: dict) -> dict:
"""
Given two dicts, merge them into a new dict as a shallow copy.
:param dict lhs: First dict
:param dict rhs: Second dict
:return: Merged dict
"""
res = lhs.copy()
res.update(rhs)
return res
3 changes: 2 additions & 1 deletion src/lib/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""This package contains all custom DeezerKodi exceptions."""

from .api_exception import ApiException
from .credentials_exception import CredentialsException
from .deezerkodi_exception import DeezerKodiException
from .empty_credentials_exception import EmptyCredentialsException
from .loaded_credentials_exception import LoadedCredentialsException
from .oauth_exception import OAuthException
from .quota_exception import QuotaException

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .deezerkodi_exception import DeezerKodiException


class CredentialsException(DeezerKodiException):
class EmptyCredentialsException(DeezerKodiException):
"""Exception thrown when user credentials are not present"""
5 changes: 5 additions & 0 deletions src/lib/exceptions/loaded_credentials_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .deezerkodi_exception import DeezerKodiException


class LoadedCredentialsException(DeezerKodiException):
"""Thrown when credentials loaded from file are faulty (i.e. empty)"""
24 changes: 12 additions & 12 deletions src/lib/helpers/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,43 @@ class Logger(object):
"""

@staticmethod
def log(msg, loglevel):
def log(msg: str, loglevel: int, *args, **kwargs):
"""
Basic log function, adds DeezerKodi prefix.
"""
xbmc.log('DeezerKodi: {}'.format(msg), loglevel)
xbmc.log('DeezerKodi: {}'.format(msg.format(*args, **kwargs)), loglevel)

@staticmethod
def debug(msg):
def debug(msg: str, *args, **kwargs):
"""
Logs a debug information.
"""
Logger.log(msg, xbmc.LOGDEBUG)
Logger.log(msg, xbmc.LOGDEBUG, *args, **kwargs)

@staticmethod
def info(msg):
def info(msg: str, *args, **kwargs):
"""
Logs an information.
"""
Logger.log(msg, xbmc.LOGINFO)
Logger.log(msg, xbmc.LOGINFO, *args, **kwargs)

@staticmethod
def warn(msg):
def warn(msg: str, *args, **kwargs):
"""
Logs a warning.
"""
Logger.log(msg, xbmc.LOGWARNING)
Logger.log(msg, xbmc.LOGWARNING, *args, **kwargs)

@staticmethod
def error(msg):
def error(msg: str, *args, **kwargs):
"""
Logs an error.
"""
Logger.log(msg, xbmc.LOGERROR)
Logger.log(msg, xbmc.LOGERROR, *args, **kwargs)

@staticmethod
def fatal(msg):
def fatal(msg: str, *args, **kwargs):
"""
Logs a fatal error.
"""
Logger.log(msg, xbmc.LOGFATAL)
Logger.log(msg, xbmc.LOGFATAL, *args, **kwargs)
8 changes: 4 additions & 4 deletions src/lib/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@ class Settings:
addon = xbmcaddon.Addon('plugin.audio.deezer')

@classmethod
def get(cls, identifiant):
def get(cls, identifiant: str) -> str:
"""
Get value associated with `identifiant`
:param str identifiant: Setting id
"""
return cls.addon.getSetting(identifiant)

@classmethod
def get_bool(cls, identifiant):
def get_bool(cls, identifiant: str) -> bool:
"""
Get value associated with `identifiant` as a boolean
:param str identifiant: Setting id
"""
return cls.addon.getSettingBool(identifiant)

@classmethod
def get_int(cls, identifiant):
def get_int(cls, identifiant: str) -> int:
"""
Get value associated with `identifiant` as an int
:param str identifiant: Setting id
"""
return cls.addon.getSettingInt(identifiant)

@classmethod
def get_float(cls, identifiant):
def get_float(cls, identifiant: str) -> float:
"""
Get value associated with `identifiant` as a float
:param str identifiant: Setting id
Expand Down

0 comments on commit 88a98dd

Please sign in to comment.