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
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()
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)
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))
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)
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', '')
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)
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'])
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
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
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)
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')
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)
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)
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)
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