Пример #1
0
 def perform_key_handshake(self):
     """Perform a key handshake and initialize crypto keys"""
     esn = get_esn()
     if not esn:
         LOG.error('Cannot perform key handshake, missing ESN')
         return False
     LOG.info('Performing key handshake with ESN: {}',
              common.censure(esn) if len(esn) > 50 else esn)
     try:
         header, _ = _process_json_response(
             self._post(ENDPOINTS['manifest'], self.handshake_request(esn)))
         header_data = self.decrypt_header_data(header['headerdata'], False)
         self.crypto.parse_key_response(header_data, esn, True)
     except MSLError as exc:
         if exc.err_number == 207006 and common.get_system_platform(
         ) == 'android':
             msg = (
                 'Request failed validation during key exchange\r\n'
                 'To try to solve this problem read the Wiki FAQ on add-on GitHub.'
             )
             raise MSLError(msg) from exc
         raise
     # Delete all the user id tokens (are correlated to the previous mastertoken)
     self.crypto.clear_user_id_tokens()
     LOG.debug('Key handshake successful')
     return True
Пример #2
0
def run_addon_configuration(show_end_msg=False):
    """
    Add-on configuration wizard,
    automatically configures profiles and add-ons dependencies, based on user-supplied data and device characteristics
    """
    system = get_system_platform()
    debug('Running add-on configuration wizard ({})', system)
    G.settings_monitor_suspend(True, False)
    is_4k_capable = is_device_4k_capable()

    _set_profiles(system, is_4k_capable)
    _set_kodi_settings(system)
    _set_isa_addon_settings(is_4k_capable, system == 'android')

    # This settings for now used only with android devices and it should remain disabled (keep it for test),
    # in the future it may be useful for other platforms or it may be removed
    G.ADDON.setSettingBool('enable_force_hdcp', False)

    # Enable UpNext if it is installed and enabled
    G.ADDON.setSettingBool(
        'UpNextNotifier_enabled',
        getCondVisibility('System.AddonIsEnabled(service.upnext)'))

    G.settings_monitor_suspend(False)
    if show_end_msg:
        show_ok_dialog(get_local_string(30154), get_local_string(30157))
 def __init__(self):
     self.current_message_id = None
     self.rndm = random.SystemRandom()
     # Set the Crypto handler
     if common.get_system_platform() == 'android':
         from .android_crypto import AndroidMSLCrypto as MSLCrypto
     else:
         from .default_crypto import DefaultMSLCrypto as MSLCrypto
     self.crypto = MSLCrypto()
Пример #4
0
    def initial_addon_configuration(self):
        """
        Initial addon configuration,
        helps users to automatically configure addon parameters for proper viewing of videos
        """
        run_initial_config = self.ADDON.getSettingBool('run_init_configuration')
        if run_initial_config:
            import resources.lib.common as common
            import resources.lib.kodi.ui as ui
            self.settings_monitor_suspended(True)

            system = common.get_system_platform()
            common.debug('Running initial addon configuration dialogs on system: {}'.format(system))
            if system in ['osx', 'ios', 'xbox']:
                self.ADDON.setSettingBool('enable_vp9_profiles', False)
                self.ADDON.setSettingBool('enable_hevc_profiles', True)
            elif system == 'windows':
                # Currently inputstream does not support hardware video acceleration on windows,
                # there is no guarantee that we will get 4K without video hardware acceleration,
                # so no 4K configuration
                self.ADDON.setSettingBool('enable_vp9_profiles', True)
                self.ADDON.setSettingBool('enable_hevc_profiles', False)
            elif system == 'android':
                ultrahd_capable_device = False
                premium_account = ui.ask_for_confirmation(common.get_local_string(30154),
                                                          common.get_local_string(30155))
                if premium_account:
                    ultrahd_capable_device = ui.ask_for_confirmation(common.get_local_string(30154),
                                                                     common.get_local_string(30156))
                if ultrahd_capable_device:
                    ui.show_ok_dialog(common.get_local_string(30154),
                                      common.get_local_string(30157))
                    ia_enabled = xbmc.getCondVisibility('System.HasAddon(inputstream.adaptive)')
                    if ia_enabled:
                        xbmc.executebuiltin('Addon.OpenSettings(inputstream.adaptive)')
                    else:
                        ui.show_ok_dialog(common.get_local_string(30154),
                                          common.get_local_string(30046))
                    self.ADDON.setSettingBool('enable_vp9_profiles', False)
                    self.ADDON.setSettingBool('enable_hevc_profiles', True)
                else:
                    # VP9 should have better performance since there is no need for 4k
                    self.ADDON.setSettingBool('enable_vp9_profiles', True)
                    self.ADDON.setSettingBool('enable_hevc_profiles', False)
                self.ADDON.setSettingBool('enable_force_hdcp', ultrahd_capable_device)
            elif system == 'linux':
                # Too many different linux systems, we can not predict all the behaviors
                # Some linux distributions have encountered problems with VP9,
                # OMSC users complain that hevc creates problems
                self.ADDON.setSettingBool('enable_vp9_profiles', False)
                self.ADDON.setSettingBool('enable_hevc_profiles', False)
            else:
                self.ADDON.setSettingBool('enable_vp9_profiles', False)
                self.ADDON.setSettingBool('enable_hevc_profiles', False)
            self.ADDON.setSettingBool('run_init_configuration', False)
            self.settings_monitor_suspended(False)
Пример #5
0
 def _get_new_esn(self):
     if common.get_system_platform() == 'android':
         return generate_android_esn()
     # In the all other systems, create a new ESN by using the existing ESN prefix
     current_esn = G.LOCAL_DB.get_value('esn', table=TABLE_SESSION)
     from re import search
     esn_prefix_match = search(r'.+-', current_esn)
     if not esn_prefix_match:
         raise Exception('It was not possible to generate a new ESN. Before try login.')
     return generate_esn(esn_prefix_match.group(0))
Пример #6
0
def _get_cdm_file_path():
    if common.get_system_platform() == 'linux':
        lib_filename = 'libwidevinecdm.so'
    elif common.get_system_platform() in ['windows', 'uwp']:
        lib_filename = 'widevinecdm.dll'
    elif common.get_system_platform() == 'osx':
        lib_filename = 'libwidevinecdm.dylib'
        # import ctypes.util
        # lib_filename = util.find_library('libwidevinecdm.dylib')
    else:
        lib_filename = None
    if not lib_filename:
        raise Exception(
            'Widevine library filename not mapped for this operative system')
    # Get the CDM path from inputstream.adaptive (such as: ../.kodi/cdm)
    from xbmcaddon import Addon
    addon = Addon('inputstream.adaptive')
    cdm_path = xbmcvfs.translatePath(addon.getSetting('DECRYPTERPATH'))
    if not common.folder_exists(cdm_path):
        raise Exception(f'The CDM path {cdm_path} not exists')
    return common.join_folders_paths(cdm_path, lib_filename)
Пример #7
0
def generate_esn(user_data):
    """Generate an ESN if on android or return the one from user_data"""
    if common.get_system_platform() == 'android':
        import subprocess
        try:
            manufacturer = subprocess.check_output(
                ['/system/bin/getprop',
                 'ro.product.manufacturer']).decode('utf-8').strip(' \t\n\r')
            if manufacturer:
                model = subprocess.check_output(
                    ['/system/bin/getprop',
                     'ro.product.model']).decode('utf-8').strip(' \t\n\r')

                # This product_characteristics check seem no longer used, some L1 devices not have the 'tv' value
                # like Xiaomi Mi Box 3 or SM-T590 devices and is cause of wrong esn generation
                # product_characteristics = subprocess.check_output(
                #     ['/system/bin/getprop',
                #      'ro.build.characteristics']).decode('utf-8').strip(' \t\n\r')
                # Property ro.build.characteristics may also contain more then one value
                # has_product_characteristics_tv = any(
                #     value.strip(' ') == 'tv' for value in product_characteristics.split(','))

                # Netflix Ready Device Platform (NRDP)
                nrdp_modelgroup = subprocess.check_output(
                    ['/system/bin/getprop',
                     'ro.nrdp.modelgroup']).decode('utf-8').strip(' \t\n\r')

                # if has_product_characteristics_tv and \
                #         g.LOCAL_DB.get_value('drm_security_level', '', table=TABLE_SESSION) == 'L1':
                if g.LOCAL_DB.get_value('drm_security_level',
                                        '',
                                        table=TABLE_SESSION) == 'L1':
                    esn = 'NFANDROID2-PRV-'
                    if nrdp_modelgroup:
                        esn += nrdp_modelgroup + '-'
                    else:
                        esn += model.replace(' ', '').upper() + '-'
                else:
                    esn = 'NFANDROID1-PRV-'
                    esn += 'T-L3-'

                esn += '{:=<5.5}'.format(manufacturer.upper())
                esn += model.replace(' ', '=').upper()
                esn = sub(r'[^A-Za-z0-9=-]', '=', esn)
                system_id = g.LOCAL_DB.get_value('drm_system_id',
                                                 table=TABLE_SESSION)
                if system_id:
                    esn += '-' + str(system_id) + '-'
                common.debug('Android generated ESN: {}', esn)
                return esn
        except OSError:
            pass
    return user_data.get('esn', '')
Пример #8
0
 def __init__(self, *args, **kwargs):  # pylint: disable=unused-argument
     self.changes_applied = False
     self.esn = get_esn()
     self.esn_new = None
     self.wv_force_sec_lev = G.LOCAL_DB.get_value(
         'widevine_force_seclev',
         WidevineForceSecLev.DISABLED,
         table=TABLE_SESSION)
     self.wv_sec_lev_new = None
     self.is_android = common.get_system_platform() == 'android'
     self.action_exit_keys_id = [
         ACTION_PREVIOUS_MENU, ACTION_PLAYER_STOP, ACTION_NAV_BACK
     ]
     super().__init__(*args)
Пример #9
0
    def get_license(self, license_data):
        """
        Requests and returns a license for the given challenge and sid

        :param license_data: The license data provided by isa
        :return: Base64 representation of the license key or False unsuccessful
        """
        LOG.debug('Requesting license')
        challenge, sid = license_data.decode('utf-8').split('!')
        sid = base64.standard_b64decode(sid).decode('utf-8')
        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'] + create_req_params(
            0, 'prefetch/license')
        try:
            response = self.msl_requests.chunked_request(
                endpoint_url,
                self.msl_requests.build_request_data(self.last_license_url,
                                                     params, 'drmSessionId'),
                get_esn())
        except MSLError as exc:
            if exc.err_number == '1044' and common.get_system_platform(
            ) == 'android':
                msg = (
                    'This title is not available to watch instantly. Please try another title.\r\n'
                    'To try to solve this problem you can force "Widevine L3" from the add-on Expert settings.\r\n'
                    'More info in the Wiki FAQ on add-on GitHub.')
                raise MSLError(msg) from exc
            raise
        # 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 base64.standard_b64decode(response[0]['licenseResponseBase64'])
Пример #10
0
def get_inputstream_listitem(videoid):
    """Return a listitem that has all inputstream relevant properties set for playback of the given video_id"""
    service_url = f'http://127.0.0.1:{G.LOCAL_DB.get_value("nf_server_service_port")}'
    manifest_path = MANIFEST_PATH_FORMAT.format(videoid.value)
    list_item = xbmcgui.ListItem(path=service_url + manifest_path,
                                 offscreen=True)
    list_item.setContentLookup(False)
    list_item.setMimeType('application/xml+dash')
    list_item.setProperty('IsPlayable', 'true')
    # Allows the add-on to always have play callbacks also when using the playlist (Kodi versions >= 20)
    list_item.setProperty('ForceResolvePlugin', 'true')
    try:
        import inputstreamhelper
        is_helper = inputstreamhelper.Helper('mpd', drm='widevine')
        inputstream_ready = is_helper.check_inputstream()
    except Exception as exc:  # pylint: disable=broad-except
        # Captures all types of ISH internal errors
        import traceback
        LOG.error(traceback.format_exc())
        raise InputStreamHelperError(str(exc)) from exc
    if not inputstream_ready:
        raise Exception(common.get_local_string(30046))
    list_item.setProperty(key='inputstream.adaptive.stream_headers',
                          value=f'user-agent={common.get_user_agent()}')
    list_item.setProperty(key='inputstream.adaptive.license_type',
                          value='com.widevine.alpha')
    list_item.setProperty(key='inputstream.adaptive.manifest_type',
                          value='mpd')
    list_item.setProperty(key='inputstream.adaptive.license_key',
                          value=service_url +
                          LICENSE_PATH_FORMAT.format(videoid.value) +
                          '||b{SSM}!b{SID}|')
    list_item.setProperty(key='inputstream.adaptive.server_certificate',
                          value=INPUTSTREAM_SERVER_CERTIFICATE)
    list_item.setProperty(key='inputstream', value='inputstream.adaptive')
    # Set PSSH/KID to pre-initialize the DRM to get challenge/session ID data in the ISA manifest proxy callback
    if common.get_system_platform() != 'android':
        list_item.setProperty(key='inputstream.adaptive.pre_init_data',
                              value=PSSH_KID)
    return list_item
Пример #11
0
    def _get_manifest(self, viewable_id, esn, challenge, sid):
        if common.get_system_platform() != 'android' and (not challenge or not sid):
            LOG.error('DRM session data not valid (Session ID: {}, Challenge: {})', challenge, sid)

        from pprint import pformat
        cache_identifier = f'{esn}_{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 LOG.is_enabled:
                LOG.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']

        manifest_ver = G.ADDON.getSettingString('msl_manifest_version')
        profiles = enabled_profiles()

        LOG.info('Requesting manifest (version {}) for\nVIDEO ID: {}\nESN: {}\nHDCP: {}\nPROFILES:\n{}',
                 manifest_ver,
                 viewable_id,
                 common.censure(esn) if len(esn) > 50 else esn,
                 hdcp_version,
                 pformat(profiles, indent=2))
        xid = None
        # On non-Android systems, we pre-initialize the DRM with default PSSH/KID, this allows to obtain Challenge/SID
        # to achieve 1080p resolution.
        # On Android, pre-initialize DRM is possible but cannot keep the same DRM session, will result in an error
        # because the manifest license data do not match the current DRM session, then we do not use it and
        # we still make the license requests.
        if manifest_ver == 'v1':
            endpoint_url, request_data = self._build_manifest_v1(viewable_id=viewable_id, hdcp_version=hdcp_version,
                                                                 hdcp_override=hdcp_override, profiles=profiles,
                                                                 challenge=challenge)
        else:  # Default - most recent version
            endpoint_url, request_data, xid = self._build_manifest_v2(viewable_id=viewable_id,
                                                                      hdcp_version=hdcp_version,
                                                                      hdcp_override=hdcp_override,
                                                                      profiles=profiles,
                                                                      challenge=challenge, sid=sid)
        manifest = self.msl_requests.chunked_request(endpoint_url, request_data, esn, disable_msl_switch=False)
        if manifest_ver == 'default' and 'license' in manifest['video_tracks'][0]:
            self.needs_license_request = False
            # 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, manifest['video_tracks'][0]['license']['drmSessionId'])
            self.licenses_release_url.insert(0,
                                             manifest['video_tracks'][0]['license']['links']['releaseLicense']['href'])
            self.licenses_response = manifest['video_tracks'][0]['license']['licenseResponseBase64']
        else:
            self.needs_license_request = True
        if LOG.is_enabled:
            # 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
Пример #12
0
def play(videoid):
    """Play an episode or movie as specified by the path"""
    is_upnext_enabled = g.ADDON.getSettingBool('UpNextNotifier_enabled')
    # For db settings 'upnext_play_callback_received' and 'upnext_play_callback_file_type' see action_controller.py
    is_upnext_callback_received = g.LOCAL_DB.get_value(
        'upnext_play_callback_received', False)
    is_upnext_callback_file_type_strm = g.LOCAL_DB.get_value(
        'upnext_play_callback_file_type', '') == 'strm'
    # This is the only way found to know if the played item come from the add-on itself or from Kodi library
    # also when Up Next Add-on is used
    is_played_from_addon = not g.IS_ADDON_EXTERNAL_CALL or (
        g.IS_ADDON_EXTERNAL_CALL and is_upnext_callback_received
        and not is_upnext_callback_file_type_strm)
    common.info('Playing {} from {} (Is Up Next Add-on call: {})', videoid,
                'add-on' if is_played_from_addon else 'external call',
                is_upnext_callback_received)

    # Profile switch when playing from Kodi library
    if not is_played_from_addon:
        if not _profile_switch():
            xbmcplugin.endOfDirectory(g.PLUGIN_HANDLE, succeeded=False)
            return

    # Get metadata of videoid
    try:
        metadata = api.get_metadata(videoid)
        common.debug('Metadata is {}', metadata)
    except MetadataNotAvailable:
        common.warn('Metadata not available for {}', videoid)
        metadata = [{}, {}]

    # Check parental control PIN
    pin_result = _verify_pin(metadata[0].get('requiresPin', False))
    if not pin_result:
        if pin_result is not None:
            ui.show_notification(common.get_local_string(30106), time=8000)
        xbmcplugin.endOfDirectory(g.PLUGIN_HANDLE, succeeded=False)
        return

    # Generate the xbmcgui.ListItem to be played
    list_item = get_inputstream_listitem(videoid)

    # STRM file resume workaround (Kodi library)
    resume_position = _strm_resume_workaroud(is_played_from_addon, videoid)
    if resume_position == '':
        xbmcplugin.setResolvedUrl(handle=g.PLUGIN_HANDLE,
                                  succeeded=False,
                                  listitem=list_item)
        return

    info_data = None
    event_data = {}
    videoid_next_episode = None

    # Get Infolabels and Arts for the videoid to be played, and for the next video if it is an episode (for UpNext)
    if not is_played_from_addon or is_upnext_enabled:
        if is_upnext_enabled and videoid.mediatype == common.VideoId.EPISODE:
            # When UpNext is enabled, get the next episode to play
            videoid_next_episode = _upnext_get_next_episode_videoid(
                videoid, metadata)
        info_data = infolabels.get_info_from_netflix(
            [videoid, videoid_next_episode]
            if videoid_next_episode else [videoid])
        info, arts = info_data[videoid.value]
        # When a item is played from Kodi library or Up Next add-on is needed set info and art to list_item
        list_item.setInfo('video', info)
        list_item.setArt(arts)

    # Get event data for videoid to be played (needed for sync of watched status with Netflix)
    if (g.ADDON.getSettingBool('ProgressManager_enabled') and videoid.mediatype
            in [common.VideoId.MOVIE, common.VideoId.EPISODE]
            and is_played_from_addon):
        # Enable the progress manager only when:
        # - It is not an add-on external call
        # - It is an external call, but the played item is not a STRM file
        # Todo:
        #  in theory to enable in Kodi library need implement the update watched status code for items of Kodi library
        #  by using JSON RPC Files.SetFileDetails https://github.com/xbmc/xbmc/pull/17202
        #  that can be used only on Kodi 19.x
        event_data = _get_event_data(videoid)
        event_data['videoid'] = videoid.to_dict()
        event_data['is_played_by_library'] = not is_played_from_addon

    if 'raspberrypi' in common.get_system_platform():
        _raspberry_disable_omxplayer()

    xbmcplugin.setResolvedUrl(handle=g.PLUGIN_HANDLE,
                              succeeded=True,
                              listitem=list_item)

    g.LOCAL_DB.set_value('last_videoid_played',
                         videoid.to_dict(),
                         table=TABLE_SESSION)

    # Start and initialize the action controller (see action_controller.py)
    common.debug('Sending initialization signal')
    common.send_signal(common.Signals.PLAYBACK_INITIATED, {
        'videoid':
        videoid.to_dict(),
        'videoid_next_episode':
        videoid_next_episode.to_dict() if videoid_next_episode else None,
        'metadata':
        metadata,
        'info_data':
        info_data,
        'is_played_from_addon':
        is_played_from_addon,
        'resume_position':
        resume_position,
        'event_data':
        event_data,
        'is_upnext_callback_received':
        is_upnext_callback_received
    },
                       non_blocking=True)
Пример #13
0
def _save_system_info():
    # Ask to save to a file
    filename = 'NFSystemInfo.txt'
    path = ui.show_browse_dialog(
        f'{common.get_local_string(30603)} - {filename}')
    if not path:
        return
    # This collect the main data to allow verification checks for problems
    data = f'Netflix add-on version: {G.VERSION}'
    data += f'\nDebug enabled: {LOG.is_enabled}'
    data += f'\nSystem platform: {common.get_system_platform()}'
    data += f'\nMachine architecture: {common.get_machine()}'
    data += f'\nUser agent string: {common.get_user_agent()}'
    data += '\n\n#### Widevine info ####\n'
    if common.get_system_platform() == 'android':
        data += f'\nSystem ID: {G.LOCAL_DB.get_value("drm_system_id", "--not obtained--", TABLE_SESSION)}'
        data += f'\nSecurity level: {G.LOCAL_DB.get_value("drm_security_level", "--not obtained--", TABLE_SESSION)}'
        data += f'\nHDCP level: {G.LOCAL_DB.get_value("drm_hdcp_level", "--not obtained--", TABLE_SESSION)}'
        wv_force_sec_lev = G.LOCAL_DB.get_value('widevine_force_seclev',
                                                WidevineForceSecLev.DISABLED,
                                                TABLE_SESSION)
        data += f'\nForced security level setting is: {wv_force_sec_lev}'
    else:
        try:
            from ctypes import (CDLL, c_char_p)
            cdm_lib_file_path = _get_cdm_file_path()
            try:
                lib = CDLL(cdm_lib_file_path)
                data += '\nLibrary status: Correctly loaded'
                try:
                    lib.GetCdmVersion.restype = c_char_p
                    data += f'\nVersion: {lib.GetCdmVersion().decode("utf-8")}'
                except Exception:  # pylint: disable=broad-except
                    # This can happen if the endpoint 'GetCdmVersion' is changed
                    data += '\nVersion: Reading error'
            except Exception as exc:  # pylint: disable=broad-except
                # This should not happen but currently InputStream Helper does not perform any verification checks on
                # downloaded and installed files, so if due to an problem it installs a CDM for a different architecture
                # or the files are corrupted, the user can no longer play videos and does not know what to do
                data += '\nLibrary status: Error loading failed'
                data += '\n>>> It is possible that is installed a CDM of a wrong architecture or is corrupted'
                data += '\n>>> Suggested solutions:'
                data += '\n>>> - Restore a previous version of Widevine library from InputStream Helper add-on settings'
                data += '\n>>> - Report the problem to the GitHub of InputStream Helper add-on'
                data += f'\n>>> Error details: {exc}'
        except Exception as exc:  # pylint: disable=broad-except
            data += f'\nThe data could not be obtained. Error details: {exc}'
    data += '\n\n#### ESN ####\n'
    esn = get_esn() or '--not obtained--'
    data += f'\nUsed ESN: {common.censure(esn) if len(esn) > 50 else esn}'
    data += f'\nWebsite ESN: {get_website_esn() or "--not obtained--"}'
    data += f'\nAndroid generated ESN: {(generate_android_esn() or "--not obtained--")}'
    if common.get_system_platform() == 'android':
        data += '\n\n#### Device system info ####\n'
        try:
            import subprocess
            info = subprocess.check_output(['/system/bin/getprop'
                                            ]).decode('utf-8')
            data += f'\n{info}'
        except Exception as exc:  # pylint: disable=broad-except
            data += f'\nThe data could not be obtained. Error: {exc}'
    data += '\n'
    try:
        common.save_file(common.join_folders_paths(path, filename),
                         data.encode('utf-8'))
        ui.show_notification(
            f'{xbmc.getLocalizedString(35259)}: {filename}')  # 35259=Saved
    except Exception as exc:  # pylint: disable=broad-except
        LOG.error('save_file error: {}', exc)
        ui.show_notification('Error! Try another path')
Пример #14
0
def play(videoid):
    """Play an episode or movie as specified by the path"""
    common.info('Playing {}', videoid)
    is_up_next_enabled = g.ADDON.getSettingBool('UpNextNotifier_enabled')
    metadata = [{}, {}]
    try:
        metadata = api.metadata(videoid)
        common.debug('Metadata is {}', metadata)
    except MetadataNotAvailable:
        common.warn('Metadata not available for {}', videoid)

    # Parental control PIN
    pin_result = _verify_pin(metadata[0].get('requiresPin', False))
    if not pin_result:
        if pin_result is not None:
            ui.show_notification(common.get_local_string(30106), time=10000)
        xbmcplugin.endOfDirectory(g.PLUGIN_HANDLE, succeeded=False)
        return

    list_item = get_inputstream_listitem(videoid)
    infos, art = infolabels.add_info_for_playback(
        videoid, list_item, is_up_next_enabled, skip_set_progress_status=True)

    resume_position = {}
    event_data = {}

    if g.IS_SKIN_CALL:
        # Workaround for resuming strm files from library
        resume_position = infos.get('resume', {}).get('position') \
            if g.ADDON.getSettingBool('ResumeManager_enabled') else None
        if resume_position:
            index_selected = ui.ask_for_resume(
                resume_position) if g.ADDON.getSettingBool(
                    'ResumeManager_dialog') else None
            if index_selected == -1:
                xbmcplugin.setResolvedUrl(handle=g.PLUGIN_HANDLE,
                                          succeeded=False,
                                          listitem=list_item)
                return
            if index_selected == 1:
                resume_position = None
    elif (g.ADDON.getSettingBool('ProgressManager_enabled') and
          videoid.mediatype in [common.VideoId.MOVIE, common.VideoId.EPISODE]):
        # To now we have this limits:
        # - enabled only with items played inside the addon then not Kodi library, need impl. JSON-RPC lib update code
        event_data = _get_event_data(videoid)
        event_data['videoid'] = videoid.to_dict()
        event_data['is_played_by_library'] = g.IS_SKIN_CALL
        # Todo: UpNext addon is incompatible with netflix watched status sync feature
        #  Problems:
        #  - Need to modify the cache (to update the watched status) on every played item
        #  - Modifying the cache after the stop, is wrong due to below problems
        #  - The new fast play, 'play_url' method, cause problems with the Player callbacks, after press play next is missing the Stop event in the controller!
        #  - To verify possibility problems of data mixing of controller._get_player_state()
        #  - The call of next video from UpNext is recognized as Skin call, because it's an external addon call, so it causes several operating problems
        is_up_next_enabled = False

    if 'raspberrypi' in common.get_system_platform(
    ) and '18' in common.GetKodiVersion().version:
        # OMX Player is not compatible with netflix video streams
        # Only Kodi 18 has this property, on Kodi 19 Omx Player has been removed
        value = common.json_rpc('Settings.GetSettingValue',
                                {'setting': 'videoplayer.useomxplayer'})
        if value.get('value'):
            common.json_rpc('Settings.SetSettingValue', {
                'setting': 'videoplayer.useomxplayer',
                'value': False
            })

    xbmcplugin.setResolvedUrl(handle=g.PLUGIN_HANDLE,
                              succeeded=True,
                              listitem=list_item)

    upnext_info = get_upnext_info(videoid, (infos, art),
                                  metadata) if is_up_next_enabled else None

    g.LOCAL_DB.set_value('last_videoid_played',
                         videoid.to_dict(),
                         table=TABLE_SESSION)

    common.debug('Sending initialization signal')
    common.send_signal(common.Signals.PLAYBACK_INITIATED, {
        'videoid': videoid.to_dict(),
        'infos': infos,
        'art': art,
        'timeline_markers': get_timeline_markers(metadata[0]),
        'upnext_info': upnext_info,
        'resume_position': resume_position,
        'event_data': event_data
    },
                       non_blocking=True)
Пример #15
0
def play(videoid):
    """Play an episode or movie as specified by the path"""
    is_upnext_enabled = g.ADDON.getSettingBool('UpNextNotifier_enabled')
    # For db settings 'upnext_play_callback_received' and 'upnext_play_callback_file_type' see action_controller.py
    is_upnext_callback_received = g.LOCAL_DB.get_value(
        'upnext_play_callback_received', False)
    is_upnext_callback_file_type_strm = g.LOCAL_DB.get_value(
        'upnext_play_callback_file_type', '') == 'strm'
    # This is the only way found to know if the played item come from the add-on itself or from Kodi library
    # also when Up Next Add-on is used
    is_played_from_addon = not g.IS_ADDON_EXTERNAL_CALL or (
        g.IS_ADDON_EXTERNAL_CALL and is_upnext_callback_received
        and not is_upnext_callback_file_type_strm)
    common.info('Playing {} from {} (Is Up Next Add-on call: {})', videoid,
                'add-on' if is_played_from_addon else 'external call',
                is_upnext_callback_received)

    metadata = [{}, {}]
    try:
        metadata = api.get_metadata(videoid)
        common.debug('Metadata is {}', metadata)
    except MetadataNotAvailable:
        common.warn('Metadata not available for {}', videoid)

    # Parental control PIN
    pin_result = _verify_pin(metadata[0].get('requiresPin', False))
    if not pin_result:
        if pin_result is not None:
            ui.show_notification(common.get_local_string(30106), time=8000)
        xbmcplugin.endOfDirectory(g.PLUGIN_HANDLE, succeeded=False)
        return

    list_item = get_inputstream_listitem(videoid)
    resume_position = None
    info_data = None
    event_data = {}
    videoid_next_episode = None

    if not is_played_from_addon or is_upnext_enabled:
        if is_upnext_enabled and videoid.mediatype == common.VideoId.EPISODE:
            # When UpNext is enabled, get the next episode to play
            videoid_next_episode = _upnext_get_next_episode_videoid(
                videoid, metadata)
        info_data = infolabels.get_info_from_netflix(
            [videoid, videoid_next_episode]
            if videoid_next_episode else [videoid])
        info, arts = info_data[videoid.value]
        # When a item is played from Kodi library or Up Next add-on is needed set info and art to list_item
        list_item.setInfo('video', info)
        list_item.setArt(arts)

    if not is_played_from_addon:
        # Workaround for resuming strm files from library
        resume_position = (
            infolabels.get_resume_info_from_library(videoid).get('position')
            if g.ADDON.getSettingBool('ResumeManager_enabled') else None)
        if resume_position:
            index_selected = (ui.ask_for_resume(resume_position)
                              if g.ADDON.getSettingBool('ResumeManager_dialog')
                              else None)
            if index_selected == -1:
                xbmcplugin.setResolvedUrl(handle=g.PLUGIN_HANDLE,
                                          succeeded=False,
                                          listitem=list_item)
                return
            if index_selected == 1:
                resume_position = None

    if (g.ADDON.getSettingBool('ProgressManager_enabled') and videoid.mediatype
            in [common.VideoId.MOVIE, common.VideoId.EPISODE]
            and is_played_from_addon):
        # Enable the progress manager only when:
        # - It is not an add-on external call
        # - It is an external call, but the played item is not a STRM file
        # Todo:
        #  in theory to enable in Kodi library need implement the update watched status code for items of Kodi library
        #  by using JSON RPC Files.SetFileDetails https://github.com/xbmc/xbmc/pull/17202
        #  that can be used only on Kodi 19.x
        event_data = _get_event_data(videoid)
        event_data['videoid'] = videoid.to_dict()
        event_data['is_played_by_library'] = not is_played_from_addon

    if 'raspberrypi' in common.get_system_platform(
    ) and g.KODI_VERSION.is_major_ver('18'):
        # OMX Player is not compatible with netflix video streams
        # Only Kodi 18 has this property, on Kodi 19 Omx Player has been removed
        value = common.json_rpc('Settings.GetSettingValue',
                                {'setting': 'videoplayer.useomxplayer'})
        if value.get('value'):
            common.json_rpc('Settings.SetSettingValue', {
                'setting': 'videoplayer.useomxplayer',
                'value': False
            })

    xbmcplugin.setResolvedUrl(handle=g.PLUGIN_HANDLE,
                              succeeded=True,
                              listitem=list_item)

    g.LOCAL_DB.set_value('last_videoid_played',
                         videoid.to_dict(),
                         table=TABLE_SESSION)

    common.debug('Sending initialization signal')
    common.send_signal(common.Signals.PLAYBACK_INITIATED, {
        'videoid':
        videoid.to_dict(),
        'videoid_next_episode':
        videoid_next_episode.to_dict() if videoid_next_episode else None,
        'metadata':
        metadata,
        'info_data':
        info_data,
        'is_played_from_addon':
        is_played_from_addon,
        'resume_position':
        resume_position,
        'event_data':
        event_data,
        'is_upnext_callback_received':
        is_upnext_callback_received
    },
                       non_blocking=True)
Пример #16
0
def _play(videoid, is_played_from_strm=False):
    """Play an episode or movie as specified by the path"""
    is_upnext_enabled = G.ADDON.getSettingBool('UpNextNotifier_enabled')
    LOG.info('Playing {}{}{}', videoid,
             ' [STRM file]' if is_played_from_strm else '',
             ' [external call]' if G.IS_ADDON_EXTERNAL_CALL else '')

    # Profile switch when playing from a STRM file (library)
    if is_played_from_strm:
        if not _profile_switch():
            xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False)
            return

    # Get metadata of videoid
    try:
        metadata = api.get_metadata(videoid)
        LOG.debug('Metadata is {}', metadata)
    except MetadataNotAvailable:
        LOG.warn('Metadata not available for {}', videoid)
        metadata = [{}, {}]

    # Check parental control PIN
    pin_result = _verify_pin(metadata[0].get('requiresPin', False))
    if not pin_result:
        if pin_result is not None:
            ui.show_notification(common.get_local_string(30106), time=8000)
        xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False)
        return

    # Generate the xbmcgui.ListItem to be played
    list_item = get_inputstream_listitem(videoid)

    # STRM file resume workaround (Kodi library)
    resume_position = _strm_resume_workaroud(is_played_from_strm, videoid)
    if resume_position == '':
        xbmcplugin.setResolvedUrl(handle=G.PLUGIN_HANDLE,
                                  succeeded=False,
                                  listitem=list_item)
        return

    info_data = None
    event_data = {}
    videoid_next_episode = None

    # Get Infolabels and Arts for the videoid to be played, and for the next video if it is an episode (for UpNext)
    if is_played_from_strm or is_upnext_enabled or G.IS_ADDON_EXTERNAL_CALL:
        if is_upnext_enabled and videoid.mediatype == common.VideoId.EPISODE:
            # When UpNext is enabled, get the next episode to play
            videoid_next_episode = _upnext_get_next_episode_videoid(
                videoid, metadata)
        info_data = infolabels.get_info_from_netflix(
            [videoid, videoid_next_episode]
            if videoid_next_episode else [videoid])
        info, arts = info_data[videoid.value]
        # When a item is played from Kodi library or Up Next add-on is needed set info and art to list_item
        list_item.setInfo('video', info)
        list_item.setArt(arts)

    # Get event data for videoid to be played (needed for sync of watched status with Netflix)
    if (G.ADDON.getSettingBool('ProgressManager_enabled') and videoid.mediatype
            in [common.VideoId.MOVIE, common.VideoId.EPISODE]):
        if not is_played_from_strm or is_played_from_strm and G.ADDON.getSettingBool(
                'sync_watched_status_library'):
            event_data = _get_event_data(videoid)
            event_data['videoid'] = videoid.to_dict()
            event_data['is_played_by_library'] = is_played_from_strm

    if 'raspberrypi' in common.get_system_platform():
        _raspberry_disable_omxplayer()

    # Start and initialize the action controller (see action_controller.py)
    LOG.debug('Sending initialization signal')
    common.send_signal(common.Signals.PLAYBACK_INITIATED, {
        'videoid':
        videoid.to_dict(),
        'videoid_next_episode':
        videoid_next_episode.to_dict() if videoid_next_episode else None,
        'metadata':
        metadata,
        'info_data':
        info_data,
        'is_played_from_strm':
        is_played_from_strm,
        'resume_position':
        resume_position,
        'event_data':
        event_data
    },
                       non_blocking=True)
    # Send callback after send the initialization signal
    # to give a bit of more time to the action controller (see note in initialize_playback of action_controller.py)
    xbmcplugin.setResolvedUrl(handle=G.PLUGIN_HANDLE,
                              succeeded=True,
                              listitem=list_item)
Пример #17
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 LOG.level == LOG.LEVEL_VERBOSE:
                LOG.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']

        LOG.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
        LOG.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 LOG.level == LOG.LEVEL_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