def _mastertoken_checks(self): """Perform checks to the MasterToken and executes a new key handshake when necessary""" is_handshake_required = False if self.crypto.mastertoken: if self.crypto.is_current_mastertoken_expired(): common.debug( 'Stored MSL MasterToken is expired, a new key handshake will be performed' ) is_handshake_required = True else: # Check if the current ESN is same of ESN bound to MasterToken if G.get_esn() != self.crypto.bound_esn: common.debug( 'Stored MSL MasterToken is bound to a different ESN, ' 'a new key handshake will be performed') is_handshake_required = True else: common.debug( 'MSL MasterToken is not available, a new key handshake will be performed' ) is_handshake_required = True if is_handshake_required: if self.perform_key_handshake(): msl_data = json.loads(common.load_file_def(MSL_DATA_FILENAME)) self.crypto.load_msl_data(msl_data) self.crypto.load_crypto_session(msl_data)
def release_license(self, data=None): # pylint: disable=unused-argument """Release the server license""" try: # When you try to play a video while another one is currently in playing, # a new license to be released will be queued, so the oldest license must be released url = self.licenses_release_url.pop() sid = self.licenses_session_id.pop() xid = self.licenses_xid.pop() common.debug('Requesting releasing license') params = [{ 'url': url, 'params': { 'drmSessionId': sid, 'xid': xid }, 'echo': 'drmSessionId' }] response = self.msl_requests.chunked_request( ENDPOINTS['license'], self.msl_requests.build_request_data('/bundle', params), G.get_esn()) common.debug('License release response: {}', response) except IndexError: # Example the supplemental media type have no license common.debug('No license to release')
def bind_events(self): """Bind events""" # I don't know the real purpose of its use, it seems to be requested after the license and before starting # playback, and only the first time after a switch, # in the response you can also understand if the msl switch has worked common.debug('Requesting bind events') response = self.msl_requests.chunked_request( ENDPOINTS['events'], self.msl_requests.build_request_data('/bind', {}), G.get_esn(), disable_msl_switch=False) common.debug('Bind events response: {}', response)
def _show_only_forced_subtitle(self): # Forced stream not found, then fix Kodi bug if user chose to apply the workaround # Kodi bug???: # If the kodi player is set with "forced only" subtitle setting, Kodi use this behavior: # 1) try to select forced subtitle that matches audio language # 2) if missing the forced subtitle in language, then # Kodi try to select: The first "forced" subtitle or the first "regular" subtitle # that can respect the chosen language or not, depends on the available streams # So can cause a wrong subtitle language or in a permanent display of subtitles! # This does not reflect the setting chosen in the Kodi player and is very annoying! # There is no other solution than to disable the subtitles manually. if self.legacy_kodi_version: # --- ONLY FOR KODI VERSION 18 --- # NOTE: With Kodi 18 it is not possible to read the properties of the streams # so the only possible way is to read the data from the manifest file audio_language = common.get_kodi_audio_language() cache_identifier = G.get_esn() + '_' + self.videoid.value manifest_data = G.CACHE.get(CACHE_MANIFESTS, cache_identifier) common.fix_locale_languages(manifest_data['timedtexttracks']) if not any( text_track.get('isForcedNarrative', False) is True and text_track['language'] == audio_language for text_track in manifest_data['timedtexttracks']): self.sc_settings.update({'subtitleenabled': False}) else: # --- ONLY FOR KODI VERSION 19 --- audio_language = common.get_kodi_audio_language( iso_format=xbmc.ISO_639_2, use_fallback=False) if audio_language == 'mediadefault': # Find the language of the default audio track audio_list = self.player_state.get(STREAMS['audio']['list']) for audio_track in audio_list: if audio_track['isdefault']: audio_language = audio_track['language'] break player_stream = self.player_state.get( STREAMS['subtitle']['current']) if player_stream is None: return if audio_language == 'original': # Do nothing is_language_appropriate = True else: is_language_appropriate = player_stream[ 'language'] == audio_language # Check if the current stream is forced and with an appropriate subtitle language if not player_stream['isforced'] or not is_language_appropriate: self.sc_settings.update({'subtitleenabled': False})
def perform_key_handshake(self, data=None): # pylint: disable=unused-argument """Perform a key handshake and initialize crypto keys""" esn = G.get_esn() if not esn: common.warn('Cannot perform key handshake, missing ESN') return False common.info('Performing key handshake with ESN: {}', common.censure(esn) if G.ADDON.getSetting('esn') else esn) response = _process_json_response( self._post(ENDPOINTS['manifest'], self.handshake_request(esn))) header_data = self.decrypt_header_data(response['headerdata'], False) self.crypto.parse_key_response(header_data, esn, True) # Delete all the user id tokens (are correlated to the previous mastertoken) self.crypto.clear_user_id_tokens() common.debug('Key handshake successful') return True
def load_manifest(self, viewable_id): """ Loads the manifests 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 """ try: manifest = self._load_manifest(viewable_id, G.get_esn()) except MSLError as exc: if 'Email or password is incorrect' in G.py2_decode(str(exc)): # Known cases when MSL error "Email or password is incorrect." can happen: # - If user change the password when the nf session was still active # - Netflix has reset the password for suspicious activity when the nf session was still active # Then clear the credentials and also user tokens. common.purge_credentials() self.msl_requests.crypto.clear_user_id_tokens() raise return self.__tranform_to_dash(manifest)
def _get_owner_user_id_token(self): """A way to get the user token id of owner profile""" # In order to get a user id token of another (non-owner) profile you must make a request with SWITCH_PROFILE # authentication scheme (a custom authentication for netflix), and this request can be directly included # in the MSL manifest request. # But in order to execute this switch profile, you need to have the user id token of the main (owner) profile. # The only way (found to now) to get it immediately, is send a logblob event request, and save the # user id token obtained in the response. common.debug('Requesting logblog') params = {'reqAttempt': 1, 'reqPriority': 0, 'reqName': EVENT_BIND} url = ENDPOINTS['logblobs'] + '?' + urlencode(params).replace( '%2F', '/') response = self.chunked_request(url, self.build_request_data( '/logblob', generate_logblobs_params()), G.get_esn(), force_auth_credential=True) common.debug('Response of logblob request: {}', response)
def _process_event_request(self, event): """Do the event post request""" event.status = Event.STATUS_REQUESTED # Request attempts can be made up to a maximum of 3 times per event while event.is_attempts_granted(): common.info('EVENT [{}] - Executing request (attempt {})', event, event.req_attempt) params = { 'reqAttempt': event.req_attempt, 'reqPriority': 20 if event.event_type == EVENT_START else 0, 'reqName': 'events/{}'.format(event) } url = ENDPOINTS['events'] + '?' + urlencode(params).replace( '%2F', '/') try: response = self.chunked_request(url, event.request_data, G.get_esn(), disable_msl_switch=False) event.set_response(response) break except Exception as exc: # pylint: disable=broad-except common.error('EVENT [{}] - The request has failed: {}', event, exc) if event.event_type == EVENT_STOP: self.clear_queue() if event.event_data['allow_request_update_loco']: # Calls to nfsession common.make_http_call('update_loco_context', {'context_name': 'continueWatching'}) common.make_http_call('update_videoid_bookmark', {'video_id': event.get_video_id()}) # Below commented lines: let future requests continue to be sent, unstable connections like wi-fi cause problems # if not event.is_response_success(): # The event request is unsuccessful then there is some problem, # no longer make any future requests from this event id # return False return True
def get_license(self, challenge, sid): """ Requests and returns a license for the given challenge and sid :param challenge: The base64 encoded challenge :param sid: The sid paired to the challenge :return: Base64 representation of the license key or False unsuccessful """ common.debug('Requesting license') timestamp = int(time.time() * 10000) xid = str(timestamp + 1610) params = [{ 'drmSessionId': sid, 'clientTime': int(timestamp / 10000), 'challengeBase64': challenge, 'xid': xid }] self.manifest_challenge = challenge endpoint_url = ENDPOINTS[ 'license'] + '?reqAttempt=1&reqPriority=0&reqName=prefetch/license' response = self.msl_requests.chunked_request( endpoint_url, self.msl_requests.build_request_data(self.last_license_url, params, 'drmSessionId'), G.get_esn()) # This xid must be used also for each future Event request, until playback stops G.LOCAL_DB.set_value('xid', xid, TABLE_SESSION) self.licenses_xid.insert(0, xid) self.licenses_session_id.insert(0, sid) self.licenses_release_url.insert( 0, response[0]['links']['releaseLicense']['href']) if self.msl_requests.msl_switch_requested: self.msl_requests.msl_switch_requested = False self.bind_events() return response[0]['licenseResponseBase64']
def view_esn(self, pathitems=None): # pylint: disable=unused-argument """Show the ESN in use""" ui.show_ok_dialog(common.get_local_string(30016), G.get_esn())
def get_manifest(videoid): """Get the manifest from cache""" cache_identifier = G.get_esn() + '_' + videoid.value return G.CACHE.get(CACHE_MANIFESTS, cache_identifier)
def load_msl_data(self, msl_data=None): self._msl_data = msl_data if msl_data else {} if msl_data: self.set_mastertoken(msl_data['tokens']['mastertoken']) self.bound_esn = msl_data.get('bound_esn', G.get_esn())
def _verify_esn_existence(): return bool(G.get_esn())