def convert_to_dash(manifest): """Convert a Netflix style manifest to MPEG-DASH manifest""" from xbmcaddon import Addon isa_version = Addon('inputstream.adaptive').getAddonInfo('version') has_drm_streams = manifest['hasDrmStreams'] protection_info = _get_protection_info(manifest) if has_drm_streams else None seconds = int(manifest['duration'] / 1000) init_length = int(seconds / 2 * 12 + 20 * 1000) duration = "PT" + str(seconds) + ".00S" root = _mpd_manifest_root(duration) period = ET.SubElement(root, 'Period', start='PT0S', duration=duration) for video_track in manifest['video_tracks']: _convert_video_track(video_track, period, init_length, protection_info, has_drm_streams) common.fix_locale_languages(manifest['audio_tracks']) common.fix_locale_languages(manifest['timedtexttracks']) default_audio_language_index = _get_default_audio_language(manifest) for index, audio_track in enumerate(manifest['audio_tracks']): _convert_audio_track(audio_track, period, init_length, (index == default_audio_language_index), has_drm_streams) default_subtitle_language_index = _get_default_subtitle_language(manifest) for index, text_track in enumerate(manifest['timedtexttracks']): if text_track['isNoneTrack']: continue _convert_text_track(text_track, period, (index == default_subtitle_language_index), isa_version) xml = ET.tostring(root, encoding='utf-8', method='xml') if common.is_debug_verbose(): common.save_file('manifest.mpd', xml) return xml.decode('utf-8').replace('\n', '').replace('\r', '').encode('utf-8')
def update_lolomo_context(context_name): """Update the lolomo list by context""" # 01/06/2020: refreshListByContext often return HTTP error 500, currently i have seen that in the website is # performed only when the video played is not added to "my list", but with a strange mixed data: # context_id: the id of continueWatching # context_index: that seem to point to "My list" id context index # This api is no more needed to update the continueWatching lolomo list lolomo_root = g.LOCAL_DB.get_value('lolomo_root_id', '', TABLE_SESSION) context_index = g.LOCAL_DB.get_value( 'lolomo_{}_index'.format(context_name.lower()), '', TABLE_SESSION) context_id = g.LOCAL_DB.get_value( 'lolomo_{}_id'.format(context_name.lower()), '', TABLE_SESSION) if not context_index: common.warn( 'Update lolomo context {} skipped due to missing lolomo index', context_name) return path = [['lolomos', lolomo_root, 'refreshListByContext']] # The fourth parameter is like a request-id, but it doesn't seem to match to # serverDefs/date/requestId of reactContext (g.LOCAL_DB.get_value('request_id', table=TABLE_SESSION)) # nor to request_id of the video event request # has a kind of relationship with renoMessageId suspect with the logblob but i'm not sure because my debug crashed, # and i am no longer able to trace the source. # I noticed also that this request can also be made with the fourth parameter empty, # but it still doesn't update the continueWatching list of lolomo, that is strange because of no error params = [ common.enclose_quotes(context_id), context_index, common.enclose_quotes(context_name), '' ] # path_suffixs = [ # [['trackIds', 'context', 'length', 'genreId', 'videoId', 'displayName', 'isTallRow', 'isShowAsARow', # 'impressionToken', 'showAsARow', 'id', 'requestId']], # [{'from': 0, 'to': 100}, 'reference', 'summary'], # [{'from': 0, 'to': 100}, 'reference', 'title'], # [{'from': 0, 'to': 100}, 'reference', 'titleMaturity'], # [{'from': 0, 'to': 100}, 'reference', 'userRating'], # [{'from': 0, 'to': 100}, 'reference', 'userRatingRequestId'], # [{'from': 0, 'to': 100}, 'reference', 'boxarts', '_342x192', 'jpg'], # [{'from': 0, 'to': 100}, 'reference', 'promoVideo'] # ] callargs = { 'callpaths': path, 'params': params, # 'path_suffixs': path_suffixs } try: response = common.make_http_call('callpath_request', callargs) common.debug('refreshListByContext response: {}', response) # The call response return the new context id of the previous invalidated lolomo context_id # and if path_suffixs is added return also the new video list data except Exception: # pylint: disable=broad-except if not common.is_debug_verbose(): return ui.show_notification( title=common.get_local_string(30105), msg='An error prevented the update the lolomo context on netflix', time=10000)
def _on_playback_started(self): player_id = _get_player_id() self._notify_all(PlaybackActionManager.on_playback_started, self._get_player_state(player_id)) if common.is_debug_verbose() and g.ADDON.getSettingBool( 'show_codec_info'): common.json_rpc('Input.ExecuteAction', {'action': 'codecinfo'}) self.active_player_id = player_id
def log_cookie(cookie_jar): """Print cookie info to the log""" if not common.is_debug_verbose(): return debug_output = 'Cookies currently loaded:\n' for cookie in cookie_jar: remaining_ttl = int((cookie.expires or 0) - time()) if cookie.expires else None debug_output += '{} (expires ts {} - remaining TTL {} sec)\n'.format( cookie.name, cookie.expires, remaining_ttl) common.debug(debug_output)
def _on_playback_started(self, data): # When UpNext addon play a video while we are inside Netflix addon and # not externally like Kodi library, the playerid become -1 this id does not exist player_id = data['player'][ 'playerid'] if data['player']['playerid'] > -1 else 1 self.active_player_id = player_id self._notify_all(PlaybackActionManager.on_playback_started, self._get_player_state()) if common.is_debug_verbose() and g.ADDON.getSettingBool( 'show_codec_info'): common.json_rpc('Input.ExecuteAction', {'action': 'codecinfo'})
def convert_to_dash(manifest): """Convert a Netflix style manifest to MPEG-DASH manifest""" from xbmcaddon import Addon isa_version = g.remove_ver_suffix( g.py2_decode(Addon('inputstream.adaptive').getAddonInfo('version'))) # If a CDN server has stability problems it may cause errors with streaming, # we allow users to select a different CDN server # (should be managed by ISA but is currently is not implemented) cdn_index = int(g.ADDON.getSettingString('cdn_server')[-1]) - 1 seconds = manifest['duration'] / 1000 init_length = int(seconds / 2 * 12 + 20 * 1000) duration = "PT" + str(int(seconds)) + ".00S" root = _mpd_manifest_root(duration) period = ET.SubElement(root, 'Period', start='PT0S', duration=duration) has_video_drm_streams = manifest['video_tracks'][0].get( 'hasDrmStreams', False) video_protection_info = _get_protection_info( manifest['video_tracks'][0]) if has_video_drm_streams else None for video_track in manifest['video_tracks']: _convert_video_track(video_track, period, init_length, video_protection_info, has_video_drm_streams, cdn_index) common.fix_locale_languages(manifest['audio_tracks']) common.fix_locale_languages(manifest['timedtexttracks']) has_audio_drm_streams = manifest['audio_tracks'][0].get( 'hasDrmStreams', False) default_audio_language_index = _get_default_audio_language(manifest) for index, audio_track in enumerate(manifest['audio_tracks']): _convert_audio_track(audio_track, period, init_length, (index == default_audio_language_index), has_audio_drm_streams, cdn_index) default_subtitle_language_index = _get_default_subtitle_language(manifest) for index, text_track in enumerate(manifest['timedtexttracks']): if text_track['isNoneTrack']: continue _convert_text_track(text_track, period, (index == default_subtitle_language_index), cdn_index, isa_version) xml = ET.tostring(root, encoding='utf-8', method='xml') if common.is_debug_verbose(): common.save_file('manifest.mpd', xml) return xml.decode('utf-8').replace('\n', '').replace('\r', '').encode('utf-8')
def update_lolomo_context(context_name): """Update the lolomo list by context""" lolomo_root = g.LOCAL_DB.get_value('lolomo_root_id', '', TABLE_SESSION) context_index = g.LOCAL_DB.get_value( 'lolomo_{}_index'.format(context_name.lower()), '', TABLE_SESSION) context_id = g.LOCAL_DB.get_value( 'lolomo_{}_id'.format(context_name.lower()), '', TABLE_SESSION) if not context_index: return path = [['lolomos', lolomo_root, 'refreshListByContext']] # The fourth parameter is like a request-id, but it doesn't seem to match to # serverDefs/date/requestId of reactContext (g.LOCAL_DB.get_value('request_id', table=TABLE_SESSION)) # nor to request_id of the video event request # has a kind of relationship with renoMessageId suspect with the logblob but i'm not sure because my debug crashed, # and i am no longer able to trace the source. # I noticed also that this request can also be made with the fourth parameter empty, # but it still doesn't update the continueWatching list of lolomo, that is strange because of no error params = [ common.enclose_quotes(context_id), context_index, common.enclose_quotes(context_name), '' ] # path_suffixs = [ # [['trackIds', 'context', 'length', 'genreId', 'videoId', 'displayName', 'isTallRow', 'isShowAsARow', # 'impressionToken', 'showAsARow', 'id', 'requestId']], # [{'from': 0, 'to': 100}, 'reference', 'summary'], # [{'from': 0, 'to': 100}, 'reference', 'title'], # [{'from': 0, 'to': 100}, 'reference', 'titleMaturity'], # [{'from': 0, 'to': 100}, 'reference', 'userRating'], # [{'from': 0, 'to': 100}, 'reference', 'userRatingRequestId'], # [{'from': 0, 'to': 100}, 'reference', 'boxarts', '_342x192', 'jpg'], # [{'from': 0, 'to': 100}, 'reference', 'promoVideo'] # ] callargs = { 'callpaths': path, 'params': params, # 'path_suffixs': path_suffixs } try: response = common.make_http_call('callpath_request', callargs) common.debug('refreshListByContext response: {}', response) except Exception: # pylint: disable=broad-except # I do not know the reason yet, but sometimes continues to return error 401, # making it impossible to update the bookmark position if not common.is_debug_verbose(): return ui.show_notification( title=common.get_local_string(30105), msg='An error prevented the update the lolomo context on netflix', time=10000)
def update_loco_context(context_name): """Update a loco list by context""" # This api seem no more needed to update the continueWatching loco list loco_root = g.LOCAL_DB.get_value('loco_root_id', '', TABLE_SESSION) context_index = g.LOCAL_DB.get_value( 'loco_{}_index'.format(context_name.lower()), '', TABLE_SESSION) context_id = g.LOCAL_DB.get_value( 'loco_{}_id'.format(context_name.lower()), '', TABLE_SESSION) if not context_index: common.warn('Update loco context {} skipped due to missing loco index', context_name) return path = [['locos', loco_root, 'refreshListByContext']] # After the introduction of LoCo, the following notes are to be reviewed (refers to old LoLoMo): # The fourth parameter is like a request-id, but it doesn't seem to match to # serverDefs/date/requestId of reactContext (g.LOCAL_DB.get_value('request_id', table=TABLE_SESSION)) # nor to request_id of the video event request, # has a kind of relationship with renoMessageId suspect with the logblob but i'm not sure because my debug crashed # and i am no longer able to trace the source. # I noticed also that this request can also be made with the fourth parameter empty. params = [ common.enclose_quotes(context_id), context_index, common.enclose_quotes(context_name), '' ] # path_suffixs = [ # [{'from': 0, 'to': 100}, 'itemSummary'], # [['componentSummary']] # ] callargs = { 'callpaths': path, 'params': params, # 'path_suffixs': path_suffixs } try: response = common.make_http_call('callpath_request', callargs) common.debug('refreshListByContext response: {}', response) # The call response return the new context id of the previous invalidated loco context_id # and if path_suffixs is added return also the new video list data except Exception: # pylint: disable=broad-except if not common.is_debug_verbose(): return ui.show_notification( title=common.get_local_string(30105), msg='An error prevented the update the loco context on netflix', time=10000)
def update_loco_context(self, context_name): """Update a loco list by context""" # Call this api seem no more needed to update the continueWatching loco list # Get current loco root data loco_data = self.path_request( [['loco', [context_name], ['context', 'id', 'index']]]) loco_root = loco_data['loco'][1] if 'continueWatching' in loco_data['locos'][loco_root]: context_index = loco_data['locos'][loco_root]['continueWatching'][ 2] context_id = loco_data['locos'][loco_root][context_index][1] else: # In the new profiles, there is no 'continueWatching' list and no list is returned common.warn( 'update_loco_context: Update skipped due to missing context {}', context_name) return path = [['locos', loco_root, 'refreshListByContext']] # After the introduction of LoCo, the following notes are to be reviewed (refers to old LoLoMo): # The fourth parameter is like a request-id, but it does not seem to match to # serverDefs/date/requestId of reactContext nor to request_id of the video event request, # seem to have some kind of relationship with renoMessageId suspect with the logblob but i am not sure. # I noticed also that this request can also be made with the fourth parameter empty. params = [ common.enclose_quotes(context_id), context_index, common.enclose_quotes(context_name), '' ] # path_suffixs = [ # [{'from': 0, 'to': 100}, 'itemSummary'], # [['componentSummary']] # ] try: response = self.callpath_request(path, params) common.debug('refreshListByContext response: {}', response) # The call response return the new context id of the previous invalidated loco context_id # and if path_suffixs is added return also the new video list data except Exception as exc: # pylint: disable=broad-except common.warn('refreshListByContext failed: {}', exc) if not common.is_debug_verbose(): return ui.show_notification( title=common.get_local_string(30105), msg='An error prevented the update the loco context on Netflix', time=10000)
def parse_profiles(data): """Parse profile information from Netflix response""" profiles_list = jgraph_get_list('profilesList', data) try: if not profiles_list: raise InvalidProfilesError( 'It has not been possible to obtain the list of profiles.') sort_order = 0 current_guids = [] for index, profile_data in iteritems(profiles_list): # pylint: disable=unused-variable summary = jgraph_get('summary', profile_data) guid = summary['guid'] current_guids.append(guid) common.debug('Parsing profile {}', summary['guid']) avatar_url = _get_avatar(profile_data, data, guid) is_active = summary.pop('isActive') g.LOCAL_DB.set_profile(guid, is_active, sort_order) g.SHARED_DB.set_profile(guid, sort_order) # Add profile language description translated from locale summary['language_desc'] = g.py2_decode( xbmc.convertLanguage(summary['language'][:2], xbmc.ENGLISH_NAME)) for key, value in iteritems(summary): if common.is_debug_verbose() and key in PROFILE_DEBUG_INFO: common.debug('Profile info {}', {key: value}) if key == 'profileName': # The profile name is coded as HTML value = parse_html(value) g.LOCAL_DB.set_profile_config(key, value, guid) g.LOCAL_DB.set_profile_config('avatar', avatar_url, guid) sort_order += 1 _delete_non_existing_profiles(current_guids) except Exception: import traceback common.error(g.py2_decode(traceback.format_exc(), 'latin-1')) common.error('Profile list data: {}', profiles_list) raise InvalidProfilesError
def _load_manifest(self, viewable_id, esn): cache_identifier = esn + '_' + unicode(viewable_id) try: # The manifest must be requested once and maintained for its entire duration manifest = g.CACHE.get(CACHE_MANIFESTS, cache_identifier) expiration = int(manifest['expiration'] / 1000) if (expiration - time.time()) < 14400: # Some devices remain active even longer than 48 hours, if the manifest is at the limit of the deadline # when requested by stream_continuity.py / events_handler.py will cause problems # if it is already expired, so we guarantee a minimum of safety ttl of 4h (14400s = 4 hours) raise CacheMiss() if common.is_debug_verbose(): common.debug('Manifest for {} obtained from the cache', viewable_id) # Save the manifest to disk as reference common.save_file('manifest.json', json.dumps(manifest).encode('utf-8')) return manifest except CacheMiss: pass isa_addon = xbmcaddon.Addon('inputstream.adaptive') hdcp_override = isa_addon is not None and isa_addon.getSettingBool( 'HDCPOVERRIDE') hdcp_4k_capable = common.is_device_4k_capable( ) or g.ADDON.getSettingBool('enable_force_hdcp') hdcp_version = [] if not hdcp_4k_capable and hdcp_override: hdcp_version = ['1.4'] if hdcp_4k_capable and hdcp_override: hdcp_version = ['2.2'] common.info('Requesting manifest for {} with ESN {} and HDCP {}', viewable_id, common.censure(esn) if g.ADDON.getSetting('esn') else esn, hdcp_version) profiles = enabled_profiles() from pprint import pformat common.info('Requested profiles:\n{}', pformat(profiles, indent=2)) params = { 'type': 'standard', 'viewableId': [viewable_id], 'profiles': profiles, 'flavor': 'PRE_FETCH', 'drmType': 'widevine', 'drmVersion': 25, 'usePsshBox': True, 'isBranching': False, 'isNonMember': False, 'isUIAutoPlay': False, 'useHttpsStreams': True, 'imageSubtitleHeight': 1080, 'uiVersion': 'shakti-v93016808', 'uiPlatform': 'SHAKTI', 'clientVersion': '6.0016.426.011', 'desiredVmaf': 'plus_lts', # phone_plus_exp can be used to mobile, not tested 'supportsPreReleasePin': True, 'supportsWatermark': True, 'supportsUnequalizedDownloadables': True, 'showAllSubDubTracks': False, 'titleSpecificData': { viewable_id: { 'unletterboxed': True } }, 'videoOutputInfo': [{ 'type': 'DigitalVideoOutputDescriptor', 'outputType': 'unknown', 'supportedHdcpVersions': hdcp_version, 'isHdcpEngaged': hdcp_override }], 'preferAssistiveAudio': False } manifest = self.msl_requests.chunked_request( ENDPOINTS['manifest'], self.msl_requests.build_request_data('/manifest', params), esn, disable_msl_switch=False) if common.is_debug_verbose(): # Save the manifest to disk as reference common.save_file('manifest.json', json.dumps(manifest).encode('utf-8')) # Save the manifest to the cache to retrieve it during its validity expiration = int(manifest['expiration'] / 1000) g.CACHE.add(CACHE_MANIFESTS, cache_identifier, manifest, expires=expiration) if 'result' in manifest: return manifest['result'] return manifest
def _load_manifest(self, viewable_id, esn): cache_identifier = esn + '_' + unicode(viewable_id) try: # The manifest must be requested once and maintained for its entire duration manifest = G.CACHE.get(CACHE_MANIFESTS, cache_identifier) expiration = int(manifest['expiration'] / 1000) if (expiration - time.time()) < 14400: # Some devices remain active even longer than 48 hours, if the manifest is at the limit of the deadline # when requested by am_stream_continuity.py / events_handler.py will cause problems # if it is already expired, so we guarantee a minimum of safety ttl of 4h (14400s = 4 hours) raise CacheMiss() if common.is_debug_verbose(): common.debug('Manifest for {} obtained from the cache', viewable_id) # Save the manifest to disk as reference common.save_file_def('manifest.json', json.dumps(manifest).encode('utf-8')) return manifest except CacheMiss: pass isa_addon = xbmcaddon.Addon('inputstream.adaptive') hdcp_override = isa_addon.getSettingBool('HDCPOVERRIDE') hdcp_4k_capable = common.is_device_4k_capable( ) or G.ADDON.getSettingBool('enable_force_hdcp') hdcp_version = [] if not hdcp_4k_capable and hdcp_override: hdcp_version = ['1.4'] if hdcp_4k_capable and hdcp_override: hdcp_version = ['2.2'] common.info('Requesting manifest for {} with ESN {} and HDCP {}', viewable_id, common.censure(esn) if G.ADDON.getSetting('esn') else esn, hdcp_version) profiles = enabled_profiles() from pprint import pformat common.info('Requested profiles:\n{}', pformat(profiles, indent=2)) params = { 'type': 'standard', 'viewableId': [viewable_id], 'profiles': profiles, 'flavor': 'PRE_FETCH', 'drmType': 'widevine', 'drmVersion': 25, 'usePsshBox': True, 'isBranching': False, 'isNonMember': False, 'isUIAutoPlay': False, 'useHttpsStreams': True, 'imageSubtitleHeight': 1080, 'uiVersion': G.LOCAL_DB.get_value('ui_version', '', table=TABLE_SESSION), 'uiPlatform': 'SHAKTI', 'clientVersion': G.LOCAL_DB.get_value('client_version', '', table=TABLE_SESSION), 'desiredVmaf': 'plus_lts', # phone_plus_exp can be used to mobile, not tested 'supportsPreReleasePin': True, 'supportsWatermark': True, 'supportsUnequalizedDownloadables': True, 'showAllSubDubTracks': False, 'titleSpecificData': { unicode(viewable_id): { 'unletterboxed': True } }, 'videoOutputInfo': [{ 'type': 'DigitalVideoOutputDescriptor', 'outputType': 'unknown', 'supportedHdcpVersions': hdcp_version, 'isHdcpEngaged': hdcp_override }], 'preferAssistiveAudio': False } if 'linux' in common.get_system_platform( ) and 'arm' in common.get_machine(): # 24/06/2020 To get until to 1080P resolutions under arm devices (ChromeOS), android excluded, # is mandatory to add the widevine challenge data (key request) to the manifest request. # Is not possible get the key request from the default_crypto, is needed to implement # the wv crypto (used for android) but currently InputStreamAdaptive support this interface only # under android OS. # As workaround: Initially we pass an hardcoded challenge data needed to play the first video, # then when ISA perform the license callback we replace it with the fresh license challenge data. params['challenge'] = self.manifest_challenge endpoint_url = ENDPOINTS[ 'manifest'] + '?reqAttempt=1&reqPriority=0&reqName=prefetch/manifest' manifest = self.msl_requests.chunked_request( endpoint_url, self.msl_requests.build_request_data('/manifest', params), esn, disable_msl_switch=False) if common.is_debug_verbose(): # Save the manifest to disk as reference common.save_file_def('manifest.json', json.dumps(manifest).encode('utf-8')) # Save the manifest to the cache to retrieve it during its validity expiration = int(manifest['expiration'] / 1000) G.CACHE.add(CACHE_MANIFESTS, cache_identifier, manifest, expires=expiration) return manifest
def extract_session_data(content, validate=False, update_profiles=False): """ Call all the parsers we need to extract all the session relevant data from the HTML page """ common.debug('Extracting session data...') react_context = extract_json(content, 'reactContext') if validate: validate_login(react_context) user_data = extract_userdata(react_context) if user_data.get('membershipStatus') == 'ANONYMOUS': # Possible known causes: # -Login password has been changed # -In the login request, 'Content-Type' specified is not compliant with data passed or no more supported # -Expired profiles cookies!? (not verified) # In these cases it is mandatory to login again raise InvalidMembershipStatusAnonymous if user_data.get('membershipStatus') != 'CURRENT_MEMBER': # When NEVER_MEMBER it is possible that the account has not been confirmed or renewed common.error('Can not login, the Membership status is {}', user_data.get('membershipStatus')) raise InvalidMembershipStatusError(user_data.get('membershipStatus')) api_data = extract_api_data(react_context) # Note: Falcor cache does not exist if membershipStatus is not CURRENT_MEMBER falcor_cache = extract_json(content, 'falcorCache') if update_profiles: parse_profiles(falcor_cache) if common.is_debug_verbose(): # Only for debug purpose not sure if can be useful try: common.debug( 'ReactContext profileGateState {} ({})', PROFILE_GATE_STATES[ react_context['models']['profileGateState']['data']], react_context['models']['profileGateState']['data']) except KeyError: common.error('ReactContext unknown profileGateState {}', react_context['models']['profileGateState']['data']) # Profile idle timeout (not sure if will be useful, to now for documentation purpose) # NOTE: On the website this value is used to update the profilesNewSession cookie expiration after a profile switch # and also to update the expiration of this cookie on each website interaction. # When the session is expired the 'profileGateState' will be 0 and the website return auto. to profiles page # g.LOCAL_DB.set_value('profile_gate_idle_timer', user_data.get('idle_timer', 30), TABLE_SESSION) # 21/05/2020 - Netflix has introduced a new paging type called "loco" similar to the old "lolomo" # Extract loco root id loco_root = falcor_cache['loco']['value'][1] g.LOCAL_DB.set_value('loco_root_id', loco_root, TABLE_SESSION) # Check if the profile session is still active # (when a session expire in the website, the screen return automatically to the profiles page) is_profile_session_active = 'componentSummary' in falcor_cache['locos'][ loco_root] # Extract loco root request id if is_profile_session_active: component_summary = falcor_cache['locos'][loco_root][ 'componentSummary']['value'] # Note: 18/06/2020 now the request id is the equal to reactContext models/serverDefs/data/requestId g.LOCAL_DB.set_value('loco_root_requestid', component_summary['requestId'], TABLE_SESSION) else: g.LOCAL_DB.set_value('loco_root_requestid', '', TABLE_SESSION) # Extract loco continueWatching id and index # The following commented code was needed for update_loco_context in api_requests.py, but currently # seem not more required to update the continueWatching list then we keep this in case of future nf changes # -- INIT -- # cw_list_data = jgraph_get('continueWatching', falcor_cache['locos'][loco_root], falcor_cache) # if cw_list_data: # context_index = falcor_cache['locos'][loco_root]['continueWatching']['value'][2] # g.LOCAL_DB.set_value('loco_continuewatching_index', context_index, TABLE_SESSION) # g.LOCAL_DB.set_value('loco_continuewatching_id', # jgraph_get('componentSummary', cw_list_data)['id'], TABLE_SESSION) # elif is_profile_session_active: # # Todo: In the new profiles, there is no 'continueWatching' context # # How get or generate the continueWatching context? # # NOTE: it was needed for update_loco_context in api_requests.py # cur_profile = jgraph_get_path(['profilesList', 'current'], falcor_cache) # common.warn('Context continueWatching not found in locos for profile guid {}.', # jgraph_get('summary', cur_profile)['guid']) # g.LOCAL_DB.set_value('loco_continuewatching_index', '', TABLE_SESSION) # g.LOCAL_DB.set_value('loco_continuewatching_id', '', TABLE_SESSION) # else: # common.warn('Is not possible to find the context continueWatching, the profile session is no more active') # g.LOCAL_DB.set_value('loco_continuewatching_index', '', TABLE_SESSION) # g.LOCAL_DB.set_value('loco_continuewatching_id', '', TABLE_SESSION) # -- END -- # Save only some info of the current profile from user data g.LOCAL_DB.set_value('build_identifier', user_data.get('BUILD_IDENTIFIER'), TABLE_SESSION) if not g.LOCAL_DB.get_value('esn', table=TABLE_SESSION): g.LOCAL_DB.set_value('esn', common.generate_android_esn() or user_data['esn'], TABLE_SESSION) g.LOCAL_DB.set_value('locale_id', user_data.get('preferredLocale').get('id', 'en-US')) # Save api urls for key, path in list(api_data.items()): g.LOCAL_DB.set_value(key, path, TABLE_SESSION) api_data['is_profile_session_active'] = is_profile_session_active return api_data
def _load_manifest(self, viewable_id, esn): cache_identifier = esn + '_' + unicode(viewable_id) try: # The manifest must be requested once and maintained for its entire duration manifest = g.CACHE.get(cache.CACHE_MANIFESTS, cache_identifier, False) common.debug('Manifest for {} with ESN {} obtained from the cache', viewable_id, esn) if common.is_debug_verbose(): # Save the manifest to disk as reference common.save_file('manifest.json', json.dumps(manifest).encode('utf-8')) return manifest except cache.CacheMiss: pass common.debug('Requesting manifest for {} with ESN {}', viewable_id, esn) profiles = enabled_profiles() import pprint common.info('Requested profiles:\n{}', pprint.pformat(profiles, indent=2)) ia_addon = xbmcaddon.Addon('inputstream.adaptive') hdcp = ia_addon is not None and ia_addon.getSetting( 'HDCPOVERRIDE') == 'true' # TODO: Future implementation when available, # request the HDCP version from Kodi through a function # in CryptoSession currently not implemented # so there will be no more need to use the HDCPOVERRIDE = true hdcp_version = [] if not g.ADDON.getSettingBool('enable_force_hdcp') and hdcp: hdcp_version = ['1.4'] if g.ADDON.getSettingBool('enable_force_hdcp') and hdcp: hdcp_version = ['2.2'] timestamp = int(time.time() * 10000) manifest_request_data = { 'version': 2, 'url': '/manifest', 'id': timestamp, 'languages': [g.LOCAL_DB.get_value('locale_id')], 'params': { 'type': 'standard', 'viewableId': [viewable_id], 'profiles': profiles, 'flavor': 'PRE_FETCH', 'drmType': 'widevine', 'drmVersion': 25, 'usePsshBox': True, 'isBranching': False, 'isNonMember': False, 'isUIAutoPlay': False, 'useHttpsStreams': True, 'imageSubtitleHeight': 1080, 'uiVersion': 'shakti-v93016808', 'uiPlatform': 'SHAKTI', 'clientVersion': '6.0016.426.011', 'desiredVmaf': 'plus_lts', # phone_plus_exp can be used to mobile, not tested 'supportsPreReleasePin': True, 'supportsWatermark': True, 'supportsUnequalizedDownloadables': True, 'showAllSubDubTracks': False, 'titleSpecificData': { viewable_id: { 'unletterboxed': True } }, 'videoOutputInfo': [{ 'type': 'DigitalVideoOutputDescriptor', 'outputType': 'unknown', 'supportedHdcpVersions': hdcp_version, 'isHdcpEngaged': hdcp }], 'preferAssistiveAudio': False }, 'echo': '' } # Get and check mastertoken validity mt_validity = self.check_mastertoken_validity() manifest = self._chunked_request(ENDPOINTS['manifest'], manifest_request_data, esn, mt_validity) if common.is_debug_verbose(): # Save the manifest to disk as reference common.save_file('manifest.json', json.dumps(manifest).encode('utf-8')) # Save the manifest to the cache to retrieve it during its validity expiration = int(manifest['expiration'] / 1000) g.CACHE.add(cache.CACHE_MANIFESTS, cache_identifier, manifest, eol=expiration) if 'result' in manifest: return manifest['result'] return manifest