예제 #1
0
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')
예제 #2
0
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)
예제 #3
0
 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
예제 #4
0
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)
예제 #5
0
 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'})
예제 #6
0
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')
예제 #7
0
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)
예제 #9
0
    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)
예제 #10
0
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
예제 #11
0
    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
예제 #12
0
    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
예제 #13
0
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
예제 #14
0
    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