def prefetch_login(self): """Check if we have stored credentials. If so, do the login before the user requests it""" from requests import exceptions try: common.get_credentials() if not self.is_logged_in(): self.login() return True except MissingCredentialsError: pass except exceptions.RequestException as exc: # It was not possible to connect to the web service, no connection, network problem, etc import traceback LOG.error('Login prefetch: request exception {}', exc) LOG.debug(G.py2_decode(traceback.format_exc(), 'latin-1')) except Exception as exc: # pylint: disable=broad-except LOG.warn('Login prefetch: failed {}', exc) return False
def _migrate_strm_files(folder_path): # Change path in STRM files for filename in list_dir(folder_path)[1]: if not filename.endswith('.strm'): continue file_path = join_folders_paths(folder_path, filename) file_content = load_file(file_path) if not file_content: LOG.warn( 'Migrate error: "{}" skipped, STRM file empty or corrupted', file_path) continue if 'action=play_video' in file_content: LOG.warn('Migrate error: "{}" skipped, STRM file type of v0.13.x', file_path) continue file_content = file_content.strip('\t\n\r').replace( '/play/', '/play_strm/') save_file(file_path, file_content.encode('utf-8'))
def _get_player_state(self, player_id=None, time_override=None): try: player_state = common.json_rpc('Player.GetProperties', { 'playerid': self.active_player_id if player_id is None else player_id, 'properties': [ 'audiostreams', 'currentaudiostream', 'currentvideostream', 'subtitles', 'currentsubtitle', 'subtitleenabled', 'percentage', 'time'] }) except IOError as exc: LOG.warn('_get_player_state: {}', exc) return {} # convert time dict to elapsed seconds player_state['elapsed_seconds'] = (player_state['time']['hours'] * 3600 + player_state['time']['minutes'] * 60 + player_state['time']['seconds']) if time_override: player_state['time'] = time_override elapsed_seconds = (time_override['hours'] * 3600 + time_override['minutes'] * 60 + time_override['seconds']) player_state['percentage'] = player_state['percentage'] / player_state['elapsed_seconds'] * elapsed_seconds player_state['elapsed_seconds'] = elapsed_seconds # Sometimes may happen that when you stop playback the player status is partial, # this is because the Kodi player stop immediately but the stop notification (from the Monitor) # arrives late, meanwhile in this interval of time a service tick may occur. if ((player_state['audiostreams'] and player_state['elapsed_seconds']) or (player_state['audiostreams'] and not player_state['elapsed_seconds'] and not self._last_player_state)): # save player state self._last_player_state = player_state else: # use saved player state player_state = self._last_player_state return player_state
def _update_library(self, videoids_tasks, exp_tvshows_videoids_values, show_prg_dialog, show_nfo_dialog, clear_on_cancel): # If set ask to user if want to export NFO files (override user custom NFO settings for videoids) nfo_settings_override = None if show_nfo_dialog: nfo_settings_override = nfo.NFOSettings() nfo_settings_override.show_export_dialog() # Get the exported tvshows, but to be excluded from the updates excluded_videoids_values = G.SHARED_DB.get_tvshows_id_list(VidLibProp['exclude_update'], True) # Start the update operations with ui.ProgressDialog(show_prg_dialog, max_value=len(videoids_tasks)) as progress_bar: for videoid, task_handler in iteritems(videoids_tasks): # Check if current videoid is excluded from updates if int(videoid.value) in excluded_videoids_values: continue # Get the NFO settings for the current videoid if not nfo_settings_override and int(videoid.value) in exp_tvshows_videoids_values: # User custom NFO setting # it is possible that the user has chosen not to export NFO files for a specific tv show nfo_export = G.SHARED_DB.get_tvshow_property(videoid.value, VidLibProp['nfo_export'], False) nfo_settings = nfo.NFOSettings(nfo_export) else: nfo_settings = nfo_settings_override or nfo.NFOSettings() # Execute the task for index, total_tasks, title in self.execute_library_task(videoid, task_handler, nfo_settings=nfo_settings, notify_errors=show_prg_dialog): label_partial_op = ' ({}/{})'.format(index + 1, total_tasks) if total_tasks > 1 else '' progress_bar.set_message(title + label_partial_op) if progress_bar.is_cancelled(): LOG.warn('Auto update of the Kodi library interrupted by User') if clear_on_cancel: self.clear_library(True) return False if self.monitor.abortRequested(): LOG.warn('Auto update of the Kodi library interrupted by Kodi') return False progress_bar.perform_step() progress_bar.set_wait_message() delay_anti_ban() return True
def route(pathitems): """Route to the appropriate handler""" LOG.debug('Routing navigation request') root_handler = pathitems[0] if pathitems else G.MODE_DIRECTORY if root_handler == G.MODE_PLAY: from resources.lib.navigation.player import play play(videoid=pathitems[1:]) elif root_handler == G.MODE_PLAY_STRM: from resources.lib.navigation.player import play_strm play_strm(videoid=pathitems[1:]) elif root_handler == 'extrafanart': LOG.warn('Route: ignoring extrafanart invocation') return False else: nav_handler = _get_nav_handler(root_handler) if not nav_handler: raise InvalidPathError('No root handler for path {}'.format('/'.join(pathitems))) _execute(nav_handler, pathitems[1:], G.REQUEST_PARAMS) return True
def _process_event_request(self, event_type, event_data, player_state): """Build and make the event post request""" if event_type == EVENT_START: # We get at every new video playback a fresh LoCo data self.loco_data = self.nfsession.get_loco_data() url = event_data['manifest']['links']['events']['href'] from resources.lib.services.nfsession.msl.msl_request_builder import MSLRequestBuilder request_data = MSLRequestBuilder.build_request_data( url, self._build_event_params(event_type, event_data, player_state, event_data['manifest'], self.loco_data)) # Request attempts can be made up to a maximum of 3 times per event LOG.info('EVENT [{}] - Executing request', event_type) endpoint_url = ENDPOINTS['events'] + create_req_params( 20 if event_type == EVENT_START else 0, f'events/{event_type}') try: response = self.chunked_request(endpoint_url, request_data, get_esn(), disable_msl_switch=False) # Malformed/wrong content in requests are ignored without returning any error in the response or exception LOG.debug('EVENT [{}] - Request response: {}', event_type, response) if event_type == EVENT_STOP: if event_data['allow_request_update_loco']: if 'list_context_name' in self.loco_data: self.nfsession.update_loco_context( self.loco_data['root_id'], self.loco_data['list_context_name'], self.loco_data['list_id'], self.loco_data['list_index']) else: LOG.warn( 'EventsHandler: LoCo list not updated due to missing list context data' ) video_id = request_data['params']['sessionParams'][ 'uiplaycontext']['video_id'] self.nfsession.update_videoid_bookmark(video_id) self.loco_data = None except Exception as exc: # pylint: disable=broad-except LOG.error('EVENT [{}] - The request has failed: {}', event_type, exc)
def compile_jobs_data(self, videoid, task_type, nfo_settings=None): """Compile a list of jobs data based on the videoid""" LOG.debug( 'Compiling list of jobs data for task handler "{}" and videoid "{}"', task_type.__name__, videoid) jobs_data = None try: if task_type == self.export_item: metadata = self.ext_func_get_metadata(videoid) # pylint: disable=not-callable if videoid.mediatype == common.VideoId.MOVIE: jobs_data = [ self._create_export_movie_job(videoid, metadata[0], nfo_settings) ] if videoid.mediatype in common.VideoId.TV_TYPES: jobs_data = self._create_export_tvshow_jobs( videoid, metadata, nfo_settings) if task_type == self.export_new_item: metadata = self.ext_func_get_metadata(videoid, True) # pylint: disable=not-callable jobs_data = self._create_export_new_episodes_jobs( videoid, metadata, nfo_settings) if task_type == self.remove_item: if videoid.mediatype == common.VideoId.MOVIE: jobs_data = [self._create_remove_movie_job(videoid)] if videoid.mediatype == common.VideoId.SHOW: jobs_data = self._create_remove_tvshow_jobs(videoid) if videoid.mediatype == common.VideoId.SEASON: jobs_data = self._create_remove_season_jobs(videoid) if videoid.mediatype == common.VideoId.EPISODE: jobs_data = [self._create_remove_episode_job(videoid)] except MetadataNotAvailable: LOG.warn( 'Unavailable metadata for videoid "{}", list of jobs not compiled', videoid) return None if jobs_data is None: LOG.error( 'Unexpected job compile case for task type "{}" and videoid "{}", list of jobs not compiled', task_type.__name__, videoid) return jobs_data
def _get_authentication_key_data(file_path, pin): """Open the auth key file""" from resources.lib.kodi import ui try: file_content = load_file(file_path) iv = '\x00' * 16 cipher = AES.new((pin + pin + pin + pin).encode("utf-8"), AES.MODE_CBC, iv.encode("utf-8")) decoded = Padding.unpad(padded_data=cipher.decrypt( base64.b64decode(file_content)), block_size=16) return json.loads(decoded.decode('utf-8')) except ValueError: # ValueError should always means wrong decryption due to wrong key ui.show_ok_dialog(get_local_string(30342), get_local_string(30106)) return '' except Exception as exc: # pylint: disable=broad-except LOG.warn('Exception raised: {}', exc) ui.show_ok_dialog(get_local_string(30342), get_local_string(30343)) return None
def route(pathitems): """Route to the appropriate handler""" LOG.debug('Routing navigation request') if pathitems: if 'extrafanart' in pathitems: LOG.warn('Route: ignoring extrafanart invocation') return False root_handler = pathitems[0] else: root_handler = G.MODE_DIRECTORY if root_handler == G.MODE_PLAY: from resources.lib.navigation.player import play play(videoid=pathitems[1:]) elif root_handler == G.MODE_PLAY_STRM: from resources.lib.navigation.player import play_strm play_strm(videoid=pathitems[1:]) else: nav_handler = _get_nav_handler(root_handler, pathitems) _execute(nav_handler, pathitems[1:], G.REQUEST_PARAMS, root_handler) return True
def initialize(self, data): if not xbmc.getCondVisibility('System.AddonIsEnabled(service.upnext)'): return videoid_next_ep = _upnext_get_next_episode_videoid( data['videoid'], data['metadata']) if not videoid_next_ep: return info_next_ep = self.nfsession.get_videoid_info(videoid_next_ep) try: self.upnext_info = self._get_upnext_info( videoid_next_ep, info_next_ep, data['metadata'], data['is_played_from_strm']) except DBRecordNotExistError: # The videoid record of the STRM episode is missing in add-on database when: # - The metadata have a new episode, but the STRM is not exported yet # - User try to play STRM files copied from another/previous add-on installation (without import them) # - User try to play STRM files from a shared path (like SMB) of another device (without use shared db) LOG.warn( 'Up Next add-on signal skipped, the videoid for the next episode does not exist in the database' )
def rate_thumb(self, videoid): """Rate an item on Netflix. Ask for a thumb rating""" # Get updated user rating info for this videoid raw_data = api.get_video_raw_data([videoid], VIDEO_LIST_RATING_THUMB_PATHS) if raw_data.get('videos', {}).get(videoid.value): video_data = raw_data['videos'][videoid.value] title = video_data.get('title') track_id_jaw = video_data.get('trackIds', {})['trackId_jaw'] is_thumb_rating = video_data.get('userRating', {}).get('type', '') == 'thumb' user_rating = video_data.get('userRating', {}).get('userRating') if is_thumb_rating else None ui.show_modal_dialog(False, ui.xmldialogs.RatingThumb, 'plugin-video-netflix-RatingThumb.xml', G.ADDON.getAddonInfo('path'), videoid=videoid, title=title, track_id_jaw=track_id_jaw, user_rating=user_rating) else: LOG.warn('Rating thumb video list api request no got results for {}', videoid)
def import_videoid_from_existing_strm(self, folder_path, folder_name): """ Get a VideoId from an existing STRM file that was exported """ for filename in common.list_dir(folder_path)[1]: filename = G.py2_decode(filename) if not filename.endswith('.strm'): continue file_path = common.join_folders_paths(folder_path, filename) # Only get a VideoId from the first file in each folder. # For tv shows all episodes will result in the same VideoId, the movies only contain one file. file_content = common.load_file(file_path) if not file_content: LOG.warn('Import error: folder "{}" skipped, STRM file empty or corrupted', folder_name) return None if 'action=play_video' in file_content: LOG.debug('Trying to import (v0.13.x): {}', file_path) return self._import_videoid_old(file_content, folder_name) LOG.debug('Trying to import: {}', file_path) return self._import_videoid(file_content, folder_name)
def login(self, credentials=None): """Perform account login with credentials""" try: # First we get the authentication url without logging in, required for login API call self.session.cookies.clear() react_context = website.extract_json(self.get('login'), 'reactContext') auth_url = website.extract_api_data(react_context)['auth_url'] LOG.debug('Logging in with credentials') login_response = self.post( 'login', headers={ 'Accept-Language': _get_accept_language_string(react_context) }, data=_login_payload(credentials or common.get_credentials(), auth_url, react_context)) website.extract_session_data(login_response, validate=True, update_profiles=True) if credentials: # Save credentials only when login has succeeded common.set_credentials(credentials) LOG.info('Login successful') ui.show_notification(common.get_local_string(30109)) cookies.save(self.session.cookies) return True except LoginValidateError as exc: self.session.cookies.clear() common.purge_credentials() raise LoginError(str(exc)) from exc except (MbrStatusNeverMemberError, MbrStatusFormerMemberError) as exc: self.session.cookies.clear() LOG.warn('Membership status {} not valid for login', exc) raise LoginError(common.get_local_string(30180)) from exc except Exception: # pylint: disable=broad-except self.session.cookies.clear() import traceback LOG.error(traceback.format_exc()) raise
def _request(self, method, endpoint, session_refreshed, **kwargs): from requests import exceptions endpoint_conf = ENDPOINTS[endpoint] url = (_api_url(endpoint_conf['address']) if endpoint_conf['is_api_call'] else _document_url(endpoint_conf['address'], kwargs)) LOG.debug('Executing {verb} request to {url}', verb='GET' if method == self.session.get else 'POST', url=url) data, headers, params = self._prepare_request_properties(endpoint_conf, kwargs) start = time.perf_counter() try: response = method( url=url, verify=self.verify_ssl, headers=headers, params=params, data=data, timeout=8) except exceptions.ReadTimeout as exc: LOG.error('HTTP Request ReadTimeout error: {}', exc) raise HttpErrorTimeout from exc LOG.debug('Request took {}s', time.perf_counter() - start) LOG.debug('Request returned status code {}', response.status_code) # for redirect in response.history: # LOG.warn('Redirected to: [{}] {}', redirect.status_code, redirect.url) if not session_refreshed: # We refresh the session when happen: # Error 404: It happen when Netflix update the build_identifier version and causes the api address to change # Error 401: This is a generic error, can happen when the http request for some reason has failed, # we allow the refresh only for shakti endpoint, sometimes for unknown reasons it is necessary to update # the session for the request to be successful if response.status_code == 404 or (response.status_code == 401 and endpoint == 'shakti'): LOG.warn('Attempt to refresh the session due to HTTP error {}', response.status_code) if self.try_refresh_session_data(): return self._request(method, endpoint, True, **kwargs) if response.status_code == 401: raise HttpError401 response.raise_for_status() return (_raise_api_error(response.json() if response.content else {}) if endpoint_conf['is_api_call'] else response.content)
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 LOG.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) LOG.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 LOG.warn('refreshListByContext failed: {}', exc) if not LOG.level == LOG.LEVEL_VERBOSE: return ui.show_notification(title=common.get_local_string(30105), msg='An error prevented the update the loco context on Netflix', time=10000)
def _import_videoid(self, file_content, folder_name): file_content = file_content.strip('\t\n\r') if G.BASE_URL not in file_content: LOG.warn( 'Import error: folder "{}" skipped, unrecognized plugin name in STRM file', folder_name) raise ImportWarning file_content = file_content.replace(G.BASE_URL, '') # file_content should result as, example: # - Old STRM path: '/play/show/xxxxxxxx/season/xxxxxxxx/episode/xxxxxxxx/' (used before ver 1.7.0) # - New STRM path: '/play_strm/show/xxxxxxxx/season/xxxxxxxx/episode/xxxxxxxx/' (used from ver 1.7.0) pathitems = file_content.strip('/').split('/') if G.MODE_PLAY not in pathitems and G.MODE_PLAY_STRM not in pathitems: LOG.warn( 'Import error: folder "{}" skipped, unsupported play path in STRM file', folder_name) raise ImportWarning pathitems = pathitems[1:] try: if pathitems[0] == common.VideoId.SHOW: # Get always VideoId of tvshow type (not season or episode) videoid = common.VideoId.from_path(pathitems[:2]) else: videoid = common.VideoId.from_path(pathitems) # Try to get the videoid metadata, to know if the videoid still exists on netflix self.ext_func_get_metadata(videoid) # pylint: disable=not-callable return videoid except MetadataNotAvailable: LOG.warn( 'Import error: folder {} skipped, metadata not available for videoid {}', folder_name, pathitems[1]) return None
def remove_item(self, job_data, library_home=None): # pylint: disable=unused-argument """Remove an item from the Kodi library, delete it from disk, remove add-on database references""" videoid = job_data['videoid'] LOG.debug('Removing {} ({}) from add-on library', videoid, job_data['title']) try: # Remove the STRM file exported exported_file_path = G.py2_decode(translatePath(job_data['file_path'])) common.delete_file_safe(exported_file_path) parent_folder = G.py2_decode(translatePath(os.path.dirname(exported_file_path))) # Remove the NFO file of the related STRM file nfo_file = os.path.splitext(exported_file_path)[0] + '.nfo' common.delete_file_safe(nfo_file) dirs, files = common.list_dir(parent_folder) # Remove the tvshow NFO file (only when it is the last file in the folder) tvshow_nfo_file = common.join_folders_paths(parent_folder, 'tvshow.nfo') # (users have the option of removing even single seasons) if xbmcvfs.exists(tvshow_nfo_file) and not dirs and len(files) == 1: xbmcvfs.delete(tvshow_nfo_file) # Delete parent folder xbmcvfs.rmdir(parent_folder) # Delete parent folder when empty if not dirs and not files: xbmcvfs.rmdir(parent_folder) # Remove videoid records from add-on database remove_videoid_from_db(videoid) except ItemNotFound: LOG.warn('The videoid {} not exists in the add-on library database', videoid) except Exception as exc: # pylint: disable=broad-except import traceback LOG.error(G.py2_decode(traceback.format_exc(), 'latin-1')) ui.show_addon_error_info(exc)
def add_event_to_queue(self, event_type, event_data, player_state): """Adds an event in the queue of events to be processed""" videoid = common.VideoId.from_dict(event_data['videoid']) # pylint: disable=unused-variable previous_data, previous_player_state = self.cache_data_events.get(videoid.value, ({}, None)) manifest = get_manifest(videoid) url = manifest['links']['events']['href'] if previous_data.get('xid') in self.banned_events_ids: LOG.warn('EVENT [{}] - Not added to the queue. The xid {} is banned due to a previous failed request', event_type, previous_data.get('xid')) return from resources.lib.services.msl.msl_request_builder import MSLRequestBuilder request_data = MSLRequestBuilder.build_request_data(url, self._build_event_params(event_type, event_data, player_state, manifest)) try: self.queue_events.put_nowait(Event(request_data, event_data)) except queue.Full: LOG.warn('EVENT [{}] - Not added to the queue. The event queue is full.', event_type)
def _import_videoid_old(self, file_content, folder_name): try: # The STRM file in add-on v13.x is different and can contains two lines, example: # #EXTINF:-1,Tv show title - "meta data ..." # plugin://plugin.video.netflix/?video_id=12345678&action=play_video # Get last line and extract the videoid value match = re.search(r'video_id=(\d+)', file_content.split('\n')[-1]) # Create a videoid of UNSPECIFIED type (we do not know the real type of videoid) videoid = common.VideoId(videoid=match.groups()[0]) # Try to get the videoid metadata: # - To know if the videoid still exists on netflix # - To get the videoid type # - To get the Tv show videoid, in the case of STRM of an episode metadata = self.ext_func_get_metadata(videoid)[0] # pylint: disable=not-callable # Generate the a good videoid if metadata['type'] == 'show': return common.VideoId(tvshowid=metadata['id']) return common.VideoId(movieid=metadata['id']) except MetadataNotAvailable: LOG.warn('Import error: folder {} skipped, metadata not available', folder_name) return None except (AttributeError, IndexError): LOG.warn('Import error: folder {} skipped, STRM not conform to v0.13.x format', folder_name) return None
def try_refresh_session_data(self, raise_exception=False): """Refresh session data from the Netflix website""" from requests import exceptions try: self.auth_url = website.extract_session_data( self.get('browse'))['auth_url'] cookies.save(self.session.cookies) LOG.debug('Successfully refreshed session data') return True except MbrStatusError: raise except (WebsiteParsingError, MbrStatusAnonymousError) as exc: import traceback LOG.warn( 'Failed to refresh session data, login can be expired or the password has been changed ({})', type(exc).__name__) LOG.debug(G.py2_decode(traceback.format_exc(), 'latin-1')) self.session.cookies.clear() if isinstance(exc, MbrStatusAnonymousError): # This prevent the MSL error: No entity association record found for the user common.send_signal(signal=common.Signals.CLEAR_USER_ID_TOKENS) # Needed to do a new login common.purge_credentials() ui.show_notification(common.get_local_string(30008)) raise_from(NotLoggedInError, exc) except exceptions.RequestException: import traceback LOG.warn( 'Failed to refresh session data, request error (RequestException)' ) LOG.warn(G.py2_decode(traceback.format_exc(), 'latin-1')) if raise_exception: raise except Exception: # pylint: disable=broad-except import traceback LOG.warn( 'Failed to refresh session data, login expired (Exception)') LOG.debug(G.py2_decode(traceback.format_exc(), 'latin-1')) self.session.cookies.clear() if raise_exception: raise return False
def try_refresh_session_data(self, raise_exception=False): """Refresh session data from the Netflix website""" try: self.auth_url = website.extract_session_data( self.get('browse'))['auth_url'] cookies.save(self.session.cookies.jar) LOG.debug('Successfully refreshed session data') return True except MbrStatusError: raise except (WebsiteParsingError, MbrStatusAnonymousError) as exc: import traceback LOG.warn( 'Failed to refresh session data, login can be expired or the password has been changed ({})', type(exc).__name__) LOG.debug(traceback.format_exc()) self.session.cookies.clear() if isinstance(exc, MbrStatusAnonymousError): # This prevent the MSL error: No entity association record found for the user self.msl_handler.clear_user_id_tokens() # Needed to do a new login common.purge_credentials() ui.show_notification(common.get_local_string(30008)) raise NotLoggedInError from exc except httpx.RequestError: import traceback LOG.warn( 'Failed to refresh session data, request error (RequestError)') LOG.warn(traceback.format_exc()) if raise_exception: raise except Exception: # pylint: disable=broad-except import traceback LOG.warn( 'Failed to refresh session data, login expired (Exception)') LOG.debug(traceback.format_exc()) self.session.cookies.clear() if raise_exception: raise return False
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 {}', json.dumps(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 # 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) xbmcplugin.setResolvedUrl(handle=G.PLUGIN_HANDLE, succeeded=True, listitem=list_item)
def onNotification(self, sender, method, data): # pylint: disable=unused-argument """ Callback for Kodi notifications that handles and dispatches playback events """ # WARNING: Do not get playerid from 'data', # Because when Up Next add-on play a video while we are inside Netflix add-on and # not externally like Kodi library, the playerid become -1 this id does not exist if not self.tracking or not method.startswith('Player.'): return try: if method == 'Player.OnPlay': if self.init_count > 0: # In this case the user has chosen to play another video while another one is in playing, # then we send the missing Stop event for the current video self._on_playback_stopped() self._initialize_am() elif method == 'Player.OnAVStart': self._on_playback_started() self.tracking_tick = True elif method == 'Player.OnSeek': self._on_playback_seek(json.loads(data)['player']['time']) elif method == 'Player.OnPause': self._is_pause_called = True self._on_playback_pause() elif method == 'Player.OnResume': # Kodi call this event instead the "Player.OnStop" event when you try to play a video # while another one is in playing (also if the current video is in pause) # Can be one of following cases: # - When you use ctx menu "Play From Here", this happen when click to next button # - When you use UpNext add-on # - When you play a non-Netflix video when a Netflix video is in playback in background # - When you play a video over another in playback (back in menus) if not self._is_pause_called: return if self.init_count == 0: # This should never happen, we have to avoid this event when you try to play a video # while another non-netflix video is in playing return self._is_pause_called = False self._on_playback_resume() elif method == 'Player.OnStop': self.tracking = False if self.active_player_id is None: # if playback does not start due to an error in streams initialization # OnAVStart notification will not be called, then active_player_id will be None LOG.debug( 'ActionController: Player.OnStop event has been ignored' ) LOG.warn( 'ActionController: Action managers disabled due to a playback initialization error' ) self.action_managers = None self.init_count -= 1 return self._on_playback_stopped() except Exception: # pylint: disable=broad-except import traceback LOG.error(traceback.format_exc()) self.tracking = False self.tracking_tick = False self.init_count = 0
def __init__(self): super(AndroidMSLCrypto, self).__init__() self.crypto_session = None self.keyset_id = None self.key_id = None self.hmac_key_id = None try: self.crypto_session = xbmcdrm.CryptoSession( 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', 'AES/CBC/NoPadding', 'HmacSHA256') LOG.debug('Widevine CryptoSession successful constructed') except Exception: # pylint: disable=broad-except import traceback LOG.error(G.py2_decode(traceback.format_exc(), 'latin-1')) raise MSLError('Failed to construct Widevine CryptoSession') drm_info = { 'version': self.crypto_session.GetPropertyString('version'), 'system_id': self.crypto_session.GetPropertyString('systemId'), # 'device_unique_id': self.crypto_session.GetPropertyByteArray('deviceUniqueId') 'hdcp_level': self.crypto_session.GetPropertyString('hdcpLevel'), 'hdcp_level_max': self.crypto_session.GetPropertyString('maxHdcpLevel'), 'security_level': self.crypto_session.GetPropertyString('securityLevel') } if not drm_info['version']: # Possible cases where no data is obtained: # - Device with custom ROM or without Widevine support # - Using Kodi debug build with a InputStream Adaptive release build (yes users do it) raise MSLError( 'It was not possible to get the data from Widevine CryptoSession.\r\n' 'Your system is not Widevine certified or you have a wrong Kodi version installed.' ) G.LOCAL_DB.set_value('drm_system_id', drm_info['system_id'], TABLE_SESSION) G.LOCAL_DB.set_value('drm_security_level', drm_info['security_level'], TABLE_SESSION) G.LOCAL_DB.set_value('drm_hdcp_level', drm_info['hdcp_level'], TABLE_SESSION) LOG.debug('Widevine version: {}', drm_info['version']) if drm_info['system_id']: LOG.debug('Widevine CryptoSession system id: {}', drm_info['system_id']) else: LOG.warn('Widevine CryptoSession system id not obtained!') LOG.debug('Widevine CryptoSession security level: {}', drm_info['security_level']) if G.ADDON.getSettingBool('force_widevine_l3'): LOG.warn( 'Widevine security level is forced to L3 by user settings!') LOG.debug('Widevine CryptoSession current hdcp level: {}', drm_info['hdcp_level']) LOG.debug('Widevine CryptoSession max hdcp level supported: {}', drm_info['hdcp_level_max']) LOG.debug('Widevine CryptoSession algorithms: {}', self.crypto_session.GetPropertyString('algorithms'))
def remove_videoid_from_kodi_library(videoid): """Remove an item from the Kodi library database (not related files)""" try: # Get a single file result by searching by videoid kodi_library_items = [get_library_item_by_videoid(videoid)] LOG.debug( 'Removing {} ({}) from Kodi library', videoid, kodi_library_items[0].get('showtitle', kodi_library_items[0]['title'])) media_type = videoid.mediatype if videoid.mediatype in [VideoId.SHOW, VideoId.SEASON]: # Retrieve the all episodes in the export folder tvshow_path = os.path.dirname(kodi_library_items[0]['file']) filters = { 'and': [{ 'field': 'path', 'operator': 'startswith', 'value': tvshow_path }, { 'field': 'filename', 'operator': 'endswith', 'value': '.strm' }] } if videoid.mediatype == VideoId.SEASON: # Use the single file result to figure out what the season is, # then add a season filter to get only the episodes of the specified season filters['and'].append({ 'field': 'season', 'operator': 'is', 'value': str(kodi_library_items[0]['season']) }) kodi_library_items = get_library_items(VideoId.EPISODE, filters) media_type = VideoId.EPISODE rpc_params = { 'movie': ['VideoLibrary.RemoveMovie', 'movieid'], # We should never remove an entire show # 'show': ['VideoLibrary.RemoveTVShow', 'tvshowid'], # Instead we delete all episodes listed in the JSON query above 'show': ['VideoLibrary.RemoveEpisode', 'episodeid'], 'season': ['VideoLibrary.RemoveEpisode', 'episodeid'], 'episode': ['VideoLibrary.RemoveEpisode', 'episodeid'] } list_rpc_params = [] # Collect multiple json-rpc commands for item in kodi_library_items: params = rpc_params[media_type] list_rpc_params.append({params[1]: item[params[1]]}) rpc_method = rpc_params[media_type][0] # Execute all the json-rpc commands in one call json_rpc_multi(rpc_method, list_rpc_params) except ItemNotFound: LOG.warn('Cannot remove {} from Kodi library, item not present', videoid) except KeyError as exc: from resources.lib.kodi import ui ui.show_notification(get_local_string(30120), time=7500) LOG.error( 'Cannot remove {} from Kodi library, mediatype not supported', exc)
def _request(self, method, endpoint, session_refreshed, **kwargs): endpoint_conf = ENDPOINTS[endpoint] url = (_api_url(endpoint_conf['address']) if endpoint_conf['is_api_call'] else _document_url( endpoint_conf['address'], kwargs)) data, headers, params = self._prepare_request_properties( endpoint_conf, kwargs) retry = 1 while True: try: LOG.debug('Executing {verb} request to {url}', verb=method, url=url) start = time.perf_counter() if method == 'GET': response = self.session.get(url=url, headers=headers, params=params, timeout=8) else: response = self.session.post(url=url, headers=headers, params=params, data=data, timeout=8) LOG.debug('Request took {}s', time.perf_counter() - start) LOG.debug('Request returned status code {}', response.status_code) break except httpx.RemoteProtocolError as exc: if 'Server disconnected' in str(exc): # Known reasons: # - The server has revoked cookies validity # - The user has executed "Sign out of all devices" from account settings # Clear the user ID tokens are tied to the credentials self.msl_handler.clear_user_id_tokens() raise NotLoggedInError from exc raise except httpx.ConnectError as exc: LOG.error('HTTP request error: {}', exc) if retry == 3: raise retry += 1 LOG.warn('Another attempt will be performed ({})', retry) # for redirect in response.history: # LOG.warn('Redirected to: [{}] {}', redirect.status_code, redirect.url) if not session_refreshed: # We refresh the session when happen: # Error 404: It happen when Netflix update the build_identifier version and causes the api address to change # Error 401: This is a generic error, can happen when the http request for some reason has failed, # we allow the refresh only for shakti endpoint, sometimes for unknown reasons it is necessary to update # the session for the request to be successful if response.status_code == 404 or (response.status_code == 401 and endpoint == 'shakti'): LOG.warn('Attempt to refresh the session due to HTTP error {}', response.status_code) if self.try_refresh_session_data(): return self._request(method, endpoint, True, **kwargs) if response.status_code == 401: raise HttpError401 response.raise_for_status() return (_raise_api_error(response.json() if response.content else {}) if endpoint_conf['is_api_call'] else response.content)
def initialize(self, data): if not data['event_data']: LOG.warn('AMVideoEvents: disabled due to no event data') self.enabled = False return self.event_data = data['event_data']