Merge pull request #29 from joaosagrath/patch-1
[plugin.video.netflix.git] / resources / lib / MSL.py
index 83ae2e7abfe2b34cfff46f67f3fc4bc0ff4ff196..ef7e93be5a6c20ba6ab7ce85881eb09bfa009dc3 100644 (file)
@@ -1,3 +1,8 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Module: MSL
+# Created on: 26.01.2017
+
 import base64
 import gzip
 import json
 import base64
 import gzip
 import json
@@ -5,21 +10,19 @@ import os
 import pprint
 import random
 from StringIO import StringIO
 import pprint
 import random
 from StringIO import StringIO
-from hmac import HMAC
-import hashlib
+
+from datetime import datetime
 import requests
 import zlib
 
 import time
 import requests
 import zlib
 
 import time
-from Crypto.PublicKey import RSA
-from Crypto.Cipher import PKCS1_OAEP
-from Crypto.Cipher import AES
-from Crypto.Random import get_random_bytes
-# from Crypto.Hash import HMAC, SHA256
-from Crypto.Util import Padding
+from Cryptodome.PublicKey import RSA
+from Cryptodome.Cipher import PKCS1_OAEP
+from Cryptodome.Cipher import AES
+from Cryptodome.Random import get_random_bytes
+from Cryptodome.Hash import HMAC, SHA256
+from Cryptodome.Util import Padding
 import xml.etree.ElementTree as ET
 import xml.etree.ElementTree as ET
-from common import log
-from common import ADDONUSERDATA
 
 pp = pprint.PrettyPrinter(indent=4)
 
 
 pp = pprint.PrettyPrinter(indent=4)
 
@@ -40,6 +43,7 @@ class MSL:
     last_playback_context = ''
     #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
     esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
     last_playback_context = ''
     #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
     esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
+    #esn = "NFCDIE-02-DCH84Q2EK3N6VFVQJ0NLRQ27498N0F"
     current_message_id = 0
     session = requests.session()
     rndm = random.SystemRandom()
     current_message_id = 0
     session = requests.session()
     rndm = random.SystemRandom()
@@ -49,44 +53,114 @@ class MSL:
         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
     }
 
         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
     }
 
-    def __init__(self, email, password):
+    def __init__(self, kodi_helper):
         """
         The Constructor checks for already existing crypto Keys.
         If they exist it will load the existing keys
         """
         """
         The Constructor checks for already existing crypto Keys.
         If they exist it will load the existing keys
         """
-        self.email = email
-        self.password = password
+        self.kodi_helper = kodi_helper
         try:
         try:
-            os.mkdir(ADDONUSERDATA)
+            os.mkdir(self.kodi_helper.msl_data_path)
         except OSError:
             pass
 
         except OSError:
             pass
 
-        if self.file_exists('msl_data.json'):
+        if self.file_exists(self.kodi_helper.msl_data_path, 'msl_data.json'):
+            self.kodi_helper.log(msg='MSL Data exists. Use old Tokens.')
             self.__load_msl_data()
             self.handshake_performed = True
             self.__load_msl_data()
             self.handshake_performed = True
-        elif self.file_exists('rsa_key.bin'):
-            log('RSA Keys do already exist load old ones')
+        elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
+            self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
             self.__load_rsa_keys()
             self.__perform_key_handshake()
         else:
             self.__load_rsa_keys()
             self.__perform_key_handshake()
         else:
-            log('Create new RSA Keys')
+            self.kodi_helper.log(msg='Create new RSA Keys')
             # Create new Key Pair and save
             self.rsa_key = RSA.generate(2048)
             self.__save_rsa_keys()
             self.__perform_key_handshake()
 
     def load_manifest(self, viewable_id):
             # Create new Key Pair and save
             self.rsa_key = RSA.generate(2048)
             self.__save_rsa_keys()
             self.__perform_key_handshake()
 
     def load_manifest(self, viewable_id):
+        """
+        Loads the manifets for the given viewable_id and returns a mpd-XML-Manifest
+        :param viewable_id: The id of of the viewable
+        :return: MPD XML Manifest or False if no success
+        """
         manifest_request_data = {
             'method': 'manifest',
             'lookupType': 'PREPARE',
             'viewableIds': [viewable_id],
             'profiles': [
         manifest_request_data = {
             'method': 'manifest',
             'lookupType': 'PREPARE',
             'viewableIds': [viewable_id],
             'profiles': [
-                'playready-h264mpl30-dash',
-                'playready-h264mpl31-dash',
+                "playready-h264bpl30-dash",
+                "playready-h264mpl30-dash",
+                "playready-h264mpl31-dash",
+                "playready-h264mpl40-dash",
+                # "hevc-main-L30-dash-cenc",
+                # "hevc-main-L31-dash-cenc",
+                # "hevc-main-L40-dash-cenc",
+                # "hevc-main-L41-dash-cenc",
+                # "hevc-main-L50-dash-cenc",
+                # "hevc-main-L51-dash-cenc",
+                # "hevc-main10-L30-dash-cenc",
+                # "hevc-main10-L31-dash-cenc",
+                # "hevc-main10-L40-dash-cenc",
+                # "hevc-main10-L41-dash-cenc",
+                # "hevc-main10-L50-dash-cenc",
+                # "hevc-main10-L51-dash-cenc",
+                # "hevc-main10-L30-dash-cenc-prk",
+                # "hevc-main10-L31-dash-cenc-prk",
+                # "hevc-main10-L40-dash-cenc-prk",
+                # "hevc-main10-L41-dash-cenc-prk",
+                # "hevc-main-L30-L31-dash-cenc-tl",
+                # "hevc-main-L31-L40-dash-cenc-tl",
+                # "hevc-main-L40-L41-dash-cenc-tl",
+                # "hevc-main-L50-L51-dash-cenc-tl",
+                # "hevc-main10-L30-L31-dash-cenc-tl",
+                # "hevc-main10-L31-L40-dash-cenc-tl",
+                # "hevc-main10-L40-L41-dash-cenc-tl",
+                # "hevc-main10-L50-L51-dash-cenc-tl",
+                # "hevc-dv-main10-L30-dash-cenc",
+                # "hevc-dv-main10-L31-dash-cenc",
+                # "hevc-dv-main10-L40-dash-cenc",
+                # "hevc-dv-main10-L41-dash-cenc",
+                # "hevc-dv-main10-L50-dash-cenc",
+                # "hevc-dv-main10-L51-dash-cenc",
+                # "hevc-dv5-main10-L30-dash-cenc-prk",
+                # "hevc-dv5-main10-L31-dash-cenc-prk",
+                # "hevc-dv5-main10-L40-dash-cenc-prk",
+                # "hevc-dv5-main10-L41-dash-cenc-prk",
+                # "hevc-dv5-main10-L50-dash-cenc-prk",
+                # "hevc-dv5-main10-L51-dash-cenc-prk",
+                # "hevc-hdr-main10-L30-dash-cenc",
+                # "hevc-hdr-main10-L31-dash-cenc",
+                # "hevc-hdr-main10-L40-dash-cenc",
+                # "hevc-hdr-main10-L41-dash-cenc",
+                # "hevc-hdr-main10-L50-dash-cenc",
+                # "hevc-hdr-main10-L51-dash-cenc",
+                # "hevc-hdr-main10-L30-dash-cenc-prk",
+                # "hevc-hdr-main10-L31-dash-cenc-prk",
+                # "hevc-hdr-main10-L40-dash-cenc-prk",
+                # "hevc-hdr-main10-L41-dash-cenc-prk",
+                # "hevc-hdr-main10-L50-dash-cenc-prk",
+                # "hevc-hdr-main10-L51-dash-cenc-prk"
+
+               # 'playready-h264mpl30-dash',
+                #'playready-h264mpl31-dash',
+                #'playready-h264mpl40-dash',
+                #'hevc-main10-L41-dash-cenc',
+                #'hevc-main10-L50-dash-cenc',
+                #'hevc-main10-L51-dash-cenc',
+
+
+
+                # Audio
                 'heaac-2-dash',
                 'heaac-2-dash',
+
+                #subtiltes
                 'dfxp-ls-sdh',
                 'dfxp-ls-sdh',
-                'simplesdh',
-                'nflx-cmisc',
+                #'simplesdh',
+                #'nflx-cmisc',
+
+                #unkown
                 'BIF240',
                 'BIF320'
             ],
                 'BIF240',
                 'BIF320'
             ],
@@ -106,39 +180,35 @@ class MSL:
             'clientVersion': '4.0004.899.011',
             'uiVersion': 'akira'
         }
             'clientVersion': '4.0004.899.011',
             'uiVersion': 'akira'
         }
-        request_data = self.__generate_msl_request_data(manifest_request_data)
 
 
-        resp = self.session.post(self.endpoints['manifest'], request_data)
+        # Check if dolby sound is enabled and add to profles
+        if self.kodi_helper.get_dolby_setting():
+            manifest_request_data['profiles'].append('ddplus-2.0-dash')
+            manifest_request_data['profiles'].append('ddplus-5.1-dash')
 
 
+        request_data = self.__generate_msl_request_data(manifest_request_data)
+        resp = self.session.post(self.endpoints['manifest'], request_data)
 
         try:
 
         try:
+            # if the json() does not fail we have an error because the manifest response is a chuncked json response
             resp.json()
             resp.json()
-            log('MANIFEST RESPONE JSON: '+resp.text)
+            self.kodi_helper.log(msg='Error getting Manifest: '+resp.text)
+            return False
         except ValueError:
         except ValueError:
-            # Maybe we have a CHUNKED response
+            # json() failed so parse the chunked response
+            self.kodi_helper.log(msg='Got chunked Manifest Response: ' + resp.text)
             resp = self.__parse_chunked_msl_response(resp.text)
             resp = self.__parse_chunked_msl_response(resp.text)
+            self.kodi_helper.log(msg='Parsed chunked Response: ' + json.dumps(resp))
             data = self.__decrypt_payload_chunk(resp['payloads'][0])
             data = self.__decrypt_payload_chunk(resp['payloads'][0])
-            # pprint.pprint(data)
             return self.__tranform_to_dash(data)
 
             return self.__tranform_to_dash(data)
 
-
     def get_license(self, challenge, sid):
     def get_license(self, challenge, sid):
-
         """
         """
-            std::time_t t = std::time(0);  // t is an integer type
-    licenseRequestData["clientTime"] = (int)t;
-    //licenseRequestData["challengeBase64"] = challengeStr;
-    licenseRequestData["licenseType"] = "STANDARD";
-    licenseRequestData["playbackContextId"] = playbackContextId;//"E1-BQFRAAELEB32o6Se-GFvjwEIbvDydEtfj6zNzEC3qwfweEPAL3gTHHT2V8rS_u1Mc3mw5BWZrUlKYIu4aArdjN8z_Z8t62E5jRjLMdCKMsVhlSJpiQx0MNW4aGqkYz-1lPh85Quo4I_mxVBG5lgd166B5NDizA8.";
-    licenseRequestData["drmContextIds"] = Json::arrayValue;
-    licenseRequestData["drmContextIds"].append(drmContextId);
-
-        :param viewable_id:
-        :param challenge:
-        :param kid:
-        :return:
+        Requests and returns a license for the given challenge and sid
+        :param challenge: The base64 encoded challenge
+        :param sid: The sid paired to the challengew
+        :return: Base64 representation of the license key or False if no success
         """
         """
-
         license_request_data = {
             'method': 'license',
             'licenseType': 'STANDARD',
         license_request_data = {
             'method': 'license',
             'licenseType': 'STANDARD',
@@ -160,18 +230,19 @@ class MSL:
         resp = self.session.post(self.endpoints['license'], request_data)
 
         try:
         resp = self.session.post(self.endpoints['license'], request_data)
 
         try:
+            # If is valid json the request for the licnese failed
             resp.json()
             resp.json()
-            log('LICENSE RESPONE JSON: '+resp.text)
+            self.kodi_helper.log(msg='Error getting license: '+resp.text)
+            return False
         except ValueError:
         except ValueError:
-            # Maybe we have a CHUNKED response
+            # json() failed so we have a chunked json response
             resp = self.__parse_chunked_msl_response(resp.text)
             data = self.__decrypt_payload_chunk(resp['payloads'][0])
             resp = self.__parse_chunked_msl_response(resp.text)
             data = self.__decrypt_payload_chunk(resp['payloads'][0])
-            # pprint.pprint(data)
             if data['success'] is True:
                 return data['result']['licenses'][0]['data']
             else:
             if data['success'] is True:
                 return data['result']['licenses'][0]['data']
             else:
-                return ''
-
+                self.kodi_helper.log(msg='Error getting license: ' + json.dumps(data))
+                return False
 
     def __decrypt_payload_chunk(self, payloadchunk):
         payloadchunk = json.JSONDecoder().decode(payloadchunk)
 
     def __decrypt_payload_chunk(self, payloadchunk):
         payloadchunk = json.JSONDecoder().decode(payloadchunk)
@@ -196,7 +267,7 @@ class MSL:
 
     def __tranform_to_dash(self, manifest):
 
 
     def __tranform_to_dash(self, manifest):
 
-        self.save_file('manifest.json', json.dumps(manifest))
+        self.save_file(self.kodi_helper.msl_data_path, 'manifest.json', json.dumps(manifest))
         manifest = manifest['result']['viewables'][0]
 
         self.last_playback_context = manifest['playbackContextId']
         manifest = manifest['result']['viewables'][0]
 
         self.last_playback_context = manifest['playbackContextId']
@@ -208,15 +279,14 @@ class MSL:
             if len(manifest['psshb64']) >= 1:
                 pssh = manifest['psshb64'][0]
 
             if len(manifest['psshb64']) >= 1:
                 pssh = manifest['psshb64'][0]
 
-
+        seconds = manifest['runtime']/1000
+        init_length = seconds / 2 * 12 + 20*1000
+        duration = "PT"+str(seconds)+".00S"
 
         root = ET.Element('MPD')
         root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
         root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
 
         root = ET.Element('MPD')
         root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
         root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
-
-
-        seconds = manifest['runtime']/1000
-        duration = "PT"+str(seconds)+".00S"
+        root.attrib['mediaPresentationDuration'] = duration
 
         period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
 
 
         period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
 
@@ -230,18 +300,30 @@ class MSL:
                 ET.SubElement(protection, 'cenc:pssh').text = pssh
 
             for downloadable in video_track['downloadables']:
                 ET.SubElement(protection, 'cenc:pssh').text = pssh
 
             for downloadable in video_track['downloadables']:
+
+                codec = 'h264'
+                if 'hevc' in downloadable['contentProfile']:
+                    codec = 'hevc'
+
+                hdcp_versions = '0.0'
+                for hdcp in downloadable['hdcpVersions']:
+                    if hdcp != 'none':
+                        hdcp_versions = hdcp
+
                 rep = ET.SubElement(video_adaption_set, 'Representation',
                                     width=str(downloadable['width']),
                                     height=str(downloadable['height']),
                 rep = ET.SubElement(video_adaption_set, 'Representation',
                                     width=str(downloadable['width']),
                                     height=str(downloadable['height']),
-                                    bandwidth=str(downloadable['bitrate']*8*1024),
-                                    codecs='h264',
+                                    bandwidth=str(downloadable['bitrate']*1024),
+                                    hdcp=hdcp_versions,
+                                    nflxContentProfile=str(downloadable['contentProfile']),
+                                    codecs=codec,
                                     mimeType='video/mp4')
 
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Init an Segment block
                                     mimeType='video/mp4')
 
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Init an Segment block
-                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
-                ET.SubElement(segment_base, 'Initialization', range='0-60000')
+                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
+                ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
 
 
 
 
 
 
@@ -252,9 +334,14 @@ class MSL:
                                                contentType='audio',
                                                mimeType='audio/mp4')
             for downloadable in audio_track['downloadables']:
                                                contentType='audio',
                                                mimeType='audio/mp4')
             for downloadable in audio_track['downloadables']:
+                codec = 'aac'
+                print downloadable
+                if downloadable['contentProfile'] == 'ddplus-2.0-dash' or downloadable['contentProfile'] == 'ddplus-5.1-dash':
+                    codec = 'ec-3'
+                print "codec is: " + codec
                 rep = ET.SubElement(audio_adaption_set, 'Representation',
                 rep = ET.SubElement(audio_adaption_set, 'Representation',
-                                    codecs='aac',
-                                    bandwidth=str(downloadable['bitrate'] * 8 * 1024),
+                                    codecs=codec,
+                                    bandwidth=str(downloadable['bitrate']*1024),
                                     mimeType='audio/mp4')
 
                 #AudioChannel Config
                                     mimeType='audio/mp4')
 
                 #AudioChannel Config
@@ -265,8 +352,23 @@ class MSL:
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Index range
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Index range
-                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
-                ET.SubElement(segment_base, 'Initialization', range='0-60000')
+                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
+                ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
+
+        # Multiple Adaption Sets for subtiles
+        for text_track in manifest['textTracks']:
+            if 'downloadables' not in text_track or text_track['downloadables'] is None:
+                continue
+            subtiles_adaption_set = ET.SubElement(period, 'AdaptationSet',
+                                                  lang=text_track['bcp47'],
+                                                  codecs='stpp',
+                                                  contentType='text',
+                                                  mimeType='application/ttml+xml')
+            for downloadable in text_track['downloadables']:
+                rep = ET.SubElement(subtiles_adaption_set, 'Representation',
+                                    nflxProfile=downloadable['contentProfile']
+                                    )
+                ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
 
 
         xml = ET.tostring(root, encoding='utf-8', method='xml')
 
 
         xml = ET.tostring(root, encoding='utf-8', method='xml')
@@ -314,7 +416,7 @@ class MSL:
         # Serialize the given Data
         serialized_data = json.dumps(data)
         serialized_data = serialized_data.replace('"', '\\"')
         # Serialize the given Data
         serialized_data = json.dumps(data)
         serialized_data = serialized_data.replace('"', '\\"')
-        serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-11","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
+        serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
 
         compressed_data = self.__compress_data(serialized_data)
 
 
         compressed_data = self.__compress_data(serialized_data)
 
@@ -332,21 +434,7 @@ class MSL:
             'signature': self.__sign(first_payload_encryption_envelope),
         }
 
             'signature': self.__sign(first_payload_encryption_envelope),
         }
 
-
-        # Create Second Payload
-        second_payload = {
-            "messageid": self.current_message_id,
-            "data": "",
-            "endofmsg": True,
-            "sequencenumber": 2
-        }
-        second_payload_encryption_envelope = self.__encrypt(json.dumps(second_payload))
-        second_payload_chunk = {
-            'payload': base64.standard_b64encode(second_payload_encryption_envelope),
-            'signature': base64.standard_b64encode(self.__sign(second_payload_encryption_envelope)),
-        }
-
-        request_data = json.dumps(header) + json.dumps(first_payload_chunk) # + json.dumps(second_payload_chunk)
+        request_data = json.dumps(header) + json.dumps(first_payload_chunk)
         return request_data
 
 
         return request_data
 
 
@@ -399,12 +487,13 @@ class MSL:
             if 'usertoken' in self.tokens:
                 pass
             else:
             if 'usertoken' in self.tokens:
                 pass
             else:
+                account = self.kodi_helper.get_credentials()
                 # Auth via email and password
                 header_data['userauthdata'] = {
                     'scheme': 'EMAIL_PASSWORD',
                     'authdata': {
                 # Auth via email and password
                 header_data['userauthdata'] = {
                     'scheme': 'EMAIL_PASSWORD',
                     'authdata': {
-                        'email': self.email,
-                        'password': self.password
+                        'email': account['email'],
+                        'password': account['password']
                     }
                 }
 
                     }
                 }
 
@@ -433,18 +522,18 @@ class MSL:
         encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
         return json.dumps(encryption_envelope)
 
         encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
         return json.dumps(encryption_envelope)
 
-    def __sign(self, text):
-        #signature = hmac.new(self.sign_key, text, hashlib.sha256).digest()
-        signature = HMAC(self.sign_key, text, hashlib.sha256).digest()
-
 
 
-        # hmac = HMAC.new(self.sign_key, digestmod=SHA256)
-        # hmac.update(text)
+    def __sign(self, text):
+        """
+        Calculates the HMAC signature for the given text with the current sign key and SHA256
+        :param text:
+        :return: Base64 encoded signature
+        """
+        signature = HMAC.new(self.sign_key, text, SHA256).digest()
         return base64.standard_b64encode(signature)
 
 
     def __perform_key_handshake(self):
         return base64.standard_b64encode(signature)
 
 
     def __perform_key_handshake(self):
-
         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
         request = {
             'entityauthdata': {
         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
         request = {
             'entityauthdata': {
@@ -456,21 +545,20 @@ class MSL:
             'headerdata': base64.standard_b64encode(header),
             'signature': '',
         }
             'headerdata': base64.standard_b64encode(header),
             'signature': '',
         }
-        log('Key Handshake Request:')
-        log(json.dumps(request))
-
+        self.kodi_helper.log(msg='Key Handshake Request:')
+        self.kodi_helper.log(msg=json.dumps(request))
 
         resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
         if resp.status_code == 200:
             resp = resp.json()
             if 'errordata' in resp:
 
         resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
         if resp.status_code == 200:
             resp = resp.json()
             if 'errordata' in resp:
-                log('Key Exchange failed')
-                log(base64.standard_b64decode(resp['errordata']))
+                self.kodi_helper.log(msg='Key Exchange failed')
+                self.kodi_helper.log(msg=base64.standard_b64decode(resp['errordata']))
                 return False
             self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
         else:
                 return False
             self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
         else:
-            log('Key Exchange failed')
-            log(resp.text)
+            self.kodi_helper.log(msg='Key Exchange failed')
+            self.kodi_helper.log(msg=resp.text)
 
     def __parse_crypto_keys(self, headerdata):
         self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
 
     def __parse_crypto_keys(self, headerdata):
         self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
@@ -491,7 +579,19 @@ class MSL:
         self.handshake_performed = True
 
     def __load_msl_data(self):
         self.handshake_performed = True
 
     def __load_msl_data(self):
-        msl_data = json.JSONDecoder().decode(self.load_file('msl_data.json'))
+        msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
+        #Check expire date of the token
+        master_token = json.JSONDecoder().decode(base64.standard_b64decode(msl_data['tokens']['mastertoken']['tokendata']))
+        valid_until = datetime.utcfromtimestamp(int(master_token['expiration']))
+        present = datetime.now()
+        difference = valid_until - present
+        difference = difference.total_seconds() / 60 / 60
+        # If token expires in less then 10 hours or is expires renew it
+        if difference < 10:
+            self.__load_rsa_keys()
+            self.__perform_key_handshake()
+            return
+
         self.__set_master_token(msl_data['tokens']['mastertoken'])
         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
         self.__set_master_token(msl_data['tokens']['mastertoken'])
         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
@@ -509,50 +609,49 @@ class MSL:
             }
         }
         serialized_data = json.JSONEncoder().encode(data)
             }
         }
         serialized_data = json.JSONEncoder().encode(data)
-        self.save_file('msl_data.json', serialized_data)
+        self.save_file(self.kodi_helper.msl_data_path, 'msl_data.json', serialized_data)
 
     def __set_master_token(self, master_token):
         self.mastertoken = master_token
 
     def __set_master_token(self, master_token):
         self.mastertoken = master_token
-        self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))[
-            'sequencenumber']
+        self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))['sequencenumber']
 
     def __load_rsa_keys(self):
 
     def __load_rsa_keys(self):
-        loaded_key = self.load_file('rsa_key.bin')
+        loaded_key = self.load_file(self.kodi_helper.msl_data_path, 'rsa_key.bin')
         self.rsa_key = RSA.importKey(loaded_key)
 
     def __save_rsa_keys(self):
         self.rsa_key = RSA.importKey(loaded_key)
 
     def __save_rsa_keys(self):
-        log('Save RSA Keys')
+        self.kodi_helper.log(msg='Save RSA Keys')
         # Get the DER Base64 of the keys
         encrypted_key = self.rsa_key.exportKey()
         # Get the DER Base64 of the keys
         encrypted_key = self.rsa_key.exportKey()
-        self.save_file('rsa_key.bin', encrypted_key)
+        self.save_file(self.kodi_helper.msl_data_path, 'rsa_key.bin', encrypted_key)
 
     @staticmethod
 
     @staticmethod
-    def file_exists(filename):
+    def file_exists(msl_data_path, filename):
         """
         Checks if a given file exists
         :param filename: The filename
         :return: True if so
         """
         """
         Checks if a given file exists
         :param filename: The filename
         :return: True if so
         """
-        return os.path.isfile(ADDONUSERDATA + filename)
+        return os.path.isfile(msl_data_path + filename)
 
     @staticmethod
 
     @staticmethod
-    def save_file(filename, content):
+    def save_file(msl_data_path, filename, content):
         """
         Saves the given content under given filename
         :param filename: The filename
         :param content: The content of the file
         """
         """
         Saves the given content under given filename
         :param filename: The filename
         :param content: The content of the file
         """
-        with open(ADDONUSERDATA + filename, 'w') as file_:
+        with open(msl_data_path + filename, 'w') as file_:
             file_.write(content)
             file_.flush()
 
     @staticmethod
             file_.write(content)
             file_.flush()
 
     @staticmethod
-    def load_file(filename):
+    def load_file(msl_data_path, filename):
         """
         Loads the content of a given filename
         :param filename: The file to load
         :return: The content of the file
         """
         """
         Loads the content of a given filename
         :param filename: The file to load
         :return: The content of the file
         """
-        with open(ADDONUSERDATA + filename) as file_:
+        with open(msl_data_path + filename) as file_:
             file_content = file_.read()
         return file_content
             file_content = file_.read()
         return file_content