def recommendations(self, pathitems): """Show video lists for a genre""" menu_data = G.MAIN_MENU_ITEMS.get(pathitems[1]) call_args = { 'menu_data': menu_data, 'genre_id': None, 'force_use_videolist_id': True, } list_data, extra_data = common.make_call('get_genres', call_args) finalize_directory(convert_list_to_dir_items(list_data), G.CONTENT_FOLDER, title=get_title(menu_data, extra_data), sort_type='sort_label') end_of_directory(False) return menu_data.get('view')
def home(self, pathitems=None): # pylint: disable=unused-argument """Show home listing""" if 'switch_profile_guid' in self.params and G.CURRENT_LOADED_DIRECTORY in [ None, 'root', 'profiles' ]: if not activate_profile(self.params['switch_profile_guid']): xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False) return LOG.debug('Showing home listing') list_data, extra_data = common.make_call('get_mainmenu') # pylint: disable=unused-variable finalize_directory( convert_list_to_dir_items(list_data), G.CONTENT_FOLDER, title=(G.LOCAL_DB.get_profile_config('profileName', '???') + ' - ' + common.get_local_string(30097))) end_of_directory(True)
def chunked_custom_video_list(chunked_video_list): """Retrieve a video list which contains the videos specified by video_ids""" chunked_video_ids = chunked_video_list['video_ids'] common.debug('Requesting custom video list with {} chunked video list', len(chunked_video_ids)) merged_response = {} for video_ids in chunked_video_ids: path_response = common.make_call( 'path_request', build_paths(['videos', video_ids], VIDEO_LIST_PARTIAL_PATHS)) common.merge_dicts(path_response, merged_response) perpetual_range_selector = chunked_video_list['perpetual_range_selector'] if perpetual_range_selector: merged_response.update(perpetual_range_selector) return CustomVideoList(merged_response)
def supplemental(self, pathitems): # pylint: disable=unused-argument """Show supplemental video list (eg. trailers) of a tv show / movie""" menu_data = {'path': ['is_context_menu_item', 'is_context_menu_item'], # Menu item do not exists 'title': common.get_local_string(30179)} from json import loads call_args = { 'menu_data': menu_data, 'video_id_dict': loads(self.params['video_id_dict']), 'supplemental_type': self.params['supplemental_type'] } list_data, extra_data = common.make_call('get_video_list_supplemental', call_args) finalize_directory(convert_list_to_dir_items(list_data), menu_data.get('content_type', g.CONTENT_SHOW), title=get_title(menu_data, extra_data)) end_of_directory(self.dir_update_listing) return menu_data.get('view')
def force_update_list(self, pathitems=None): # pylint: disable=unused-argument """Clear the cache of my list to force the update""" if self.params['menu_id'] == 'myList': G.CACHE.clear([cache_utils.CACHE_MYLIST], clear_database=False) if self.params['menu_id'] == 'continueWatching': # Delete the cache of continueWatching list # pylint: disable=unused-variable is_exists, list_id = common.make_call( 'get_continuewatching_videoid_exists', {'video_id': ''}) if list_id: G.CACHE.delete(cache_utils.CACHE_COMMON, list_id, including_suffixes=True) # When the continueWatching context is invalidated from a refreshListByContext call # the LoCo need to be updated to obtain the new list id, so we delete the cache to get new data G.CACHE.delete(cache_utils.CACHE_COMMON, 'loco_list')
def home(self, pathitems=None, cache_to_disc=True): # pylint: disable=unused-argument """Show home listing""" if 'switch_profile_guid' in self.params: # This is executed only when you have selected a profile from the profile list if not self._activate_profile(self.params['switch_profile_guid']): xbmcplugin.endOfDirectory(g.PLUGIN_HANDLE, succeeded=False) return common.debug('Showing home listing') list_data, extra_data = common.make_call('get_mainmenu') # pylint: disable=unused-variable finalize_directory( convert_list(list_data), g.CONTENT_FOLDER, title=(g.LOCAL_DB.get_profile_config('profileName', '???') + ' - ' + common.get_local_string(30097))) end_of_directory(False, cache_to_disc)
def video_list(self, pathitems): """Show a video list of a list ID""" menu_data = g.MAIN_MENU_ITEMS.get(pathitems[1]) if not menu_data: # Dynamic menus menu_data = g.LOCAL_DB.get_value(pathitems[1], table=TABLE_MENU_DATA, data_type=dict) call_args = { 'list_id': pathitems[2], 'menu_data': menu_data, 'is_dynamic_id': not g.is_known_menu_context(pathitems[2]) } list_data, extra_data = common.make_call('get_video_list', call_args) finalize_directory(convert_list_to_dir_items(list_data), menu_data.get('content_type', g.CONTENT_SHOW), title=get_title(menu_data, extra_data)) end_of_directory(False) return menu_data.get('view')
def home(self, pathitems=None, is_exec_profile_switch=True): # pylint: disable=unused-argument """Show home listing""" if is_exec_profile_switch and 'switch_profile_guid' in self.params and is_parent_root_path( ): # This must be executed only when you have selected a profile from the profile list if not activate_profile(self.params['switch_profile_guid']): xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False) return LOG.debug('Showing home listing') list_data, extra_data = common.make_call('get_mainmenu') # pylint: disable=unused-variable finalize_directory( convert_list_to_dir_items(list_data), G.CONTENT_FOLDER, title=(G.LOCAL_DB.get_profile_config('profileName', '???') + ' - ' + common.get_local_string(30097))) end_of_directory(True)
def episodes(videoid, videoid_value, perpetual_range_start=None): # pylint: disable=unused-argument """Retrieve episodes of a season""" if videoid.mediatype != common.VideoId.SEASON: raise common.InvalidVideoId('Cannot request episode list for {}' .format(videoid)) common.debug('Requesting episode list for {}', videoid) paths = [['seasons', videoid.seasonid, 'summary']] paths.extend(build_paths(['seasons', videoid.seasonid, 'episodes', RANGE_SELECTOR], EPISODES_PARTIAL_PATHS)) paths.extend(build_paths(['videos', videoid.tvshowid], ART_PARTIAL_PATHS + [['title']])) callargs = { 'paths': paths, 'length_params': ['stdlist_wid', ['seasons', videoid.seasonid, 'episodes']], 'perpetual_range_start': perpetual_range_start } return EpisodeList(videoid, common.make_call('perpetual_path_request', callargs))
def video_list(list_id): """Retrieve a single video list this type of request seems to have results fixed at ~40 from netflix and the 'length' tag never return to the actual total count of the elements """ common.debug('Requesting video list {}'.format(list_id)) return VideoList( common.make_call( 'perpetual_path_request', { 'path_type': 'videolist', 'paths': build_paths(['lists', list_id, RANGE_SELECTOR, 'reference'], VIDEO_LIST_PARTIAL_PATHS), 'length_params1': list_id }))
def remove_watched_status(videoid): """Request to Netflix service to delete the watched status (delete also the item from "continue watching" list)""" # WARNING: THE NF SERVICE MAY TAKE UNTIL TO 24 HOURS TO REMOVE IT try: data = common.make_call( 'post_safe', { 'endpoint': 'viewing_activity', 'data': { 'movieID': videoid.value, 'seriesAll': videoid.mediatype == common.VideoId.SHOW, 'guid': G.LOCAL_DB.get_active_profile_guid() } }) return data.get('status', False) except Exception as exc: # pylint: disable=broad-except LOG.error('remove_watched_status raised this error: {}', exc) return False
def _display_search_results(pathitems, perpetual_range_start, dir_update_listing): menu_data = g.MAIN_MENU_ITEMS['search'] call_args = { 'menu_data': menu_data, 'search_term': pathitems[2], 'pathitems': pathitems, 'perpetual_range_start': perpetual_range_start } list_data, extra_data = common.make_call('get_video_list_search', call_args) if list_data: _search_results_directory(pathitems, menu_data, list_data, extra_data, dir_update_listing) else: ui.show_notification(common.get_local_string(30013)) xbmcplugin.endOfDirectory(g.PLUGIN_HANDLE, succeeded=False)
def seasons(videoid): """Retrieve seasons of a TV show""" if videoid.mediatype != common.VideoId.SHOW: raise common.InvalidVideoId( 'Cannot request season list for {}'.format(videoid)) common.debug('Requesting season list for show {}'.format(videoid)) return SeasonList( videoid, common.make_call( 'perpetual_path_request', { 'path_type': 'seasonlist', 'length_params': [videoid.tvshowid], 'paths': build_paths(['videos', videoid.tvshowid], SEASONS_PARTIAL_PATHS) }))
def search(search_term, perpetual_range_start=None): """Retrieve a video list of search results""" common.debug('Searching for {}'.format(search_term)) base_path = [ 'search', 'byTerm', '|' + search_term, 'titles', MAX_PATH_REQUEST_SIZE ] paths = [base_path + [['id', 'name', 'requestId']]] paths.extend( build_paths(base_path + [RANGE_SELECTOR, 'reference'], VIDEO_LIST_PARTIAL_PATHS)) callargs = { 'paths': paths, 'length_params': ['searchlist', ['search', 'byReference']], 'perpetual_range_start': perpetual_range_start } return SearchVideoList(common.make_call('perpetual_path_request', callargs))
def genres(self, pathitems): """Show loco list of a genre or from loco root the list of contexts specified in the menu data""" menu_data = G.MAIN_MENU_ITEMS.get(pathitems[1]) if not menu_data: # Dynamic menus menu_data = G.LOCAL_DB.get_value(pathitems[1], table=TABLE_MENU_DATA, data_type=dict) call_args = { 'menu_data': menu_data, # When genre_id is None is loaded the loco root the list of contexts specified in the menu data 'genre_id': None if len(pathitems) < 3 else int(pathitems[2]), 'force_use_videolist_id': False, } list_data, extra_data = common.make_call('get_genres', call_args) finalize_directory(convert_list_to_dir_items(list_data), G.CONTENT_FOLDER, title=get_title(menu_data, extra_data), sort_type='sort_label') end_of_directory(False) return menu_data.get('view')
def login(ask_credentials=True): """Perform a login""" try: credentials = { 'credentials': ui.ask_credentials() } if ask_credentials else None if not common.make_call('login', credentials): return False return True except MissingCredentialsError: # Aborted from user or leave an empty field ui.show_notification(common.get_local_string(30112)) raise except LoginError as exc: # Login not valid ui.show_ok_dialog(common.get_local_string(30008), unicode(exc)) return False
def _exported_directory(self, pathitems, chunked_video_list, perpetual_range_selector): menu_data = G.MAIN_MENU_ITEMS['exported'] call_args = { 'pathitems': pathitems, 'menu_data': menu_data, 'chunked_video_list': chunked_video_list, 'perpetual_range_selector': perpetual_range_selector } list_data, extra_data = common.make_call('get_video_list_chunked', call_args) finalize_directory(convert_list_to_dir_items(list_data), menu_data.get('content_type', G.CONTENT_SHOW), title=get_title(menu_data, extra_data)) end_of_directory(self.dir_update_listing) return menu_data.get('view')
def video_list(list_id): """Retrieve a single video list""" common.debug('Requesting video list {}'.format(list_id)) return VideoList( common.make_call( 'perpetual_path_request', { 'path_type': 'videolist', 'paths': [[ 'lists', [list_id], # The length attribute MUST be present! ['displayName', 'context', 'genreId', 'length'] ]] + build_paths(['lists', [list_id], RANGE_SELECTOR, 'reference'], VIDEO_LIST_PARTIAL_PATHS) }))
def home(self, pathitems=None): # pylint: disable=unused-argument """Show home listing""" if 'switch_profile_guid' in self.params: if G.IS_ADDON_EXTERNAL_CALL: # Profile switch/ask PIN only once ret = not self.params['switch_profile_guid'] == G.LOCAL_DB.get_active_profile_guid() else: # Profile switch/ask PIN every time you come from ... ret = common.WndHomeProps[common.WndHomeProps.CURRENT_DIRECTORY] in ['', 'root', 'profiles'] if ret and not activate_profile(self.params['switch_profile_guid']): xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False) return LOG.debug('Showing home listing') dir_items, extra_data = common.make_call('get_mainmenu') # pylint: disable=unused-variable finalize_directory(dir_items, G.CONTENT_FOLDER, title=(G.LOCAL_DB.get_profile_config('profileName', '???') + ' - ' + common.get_local_string(30097))) end_of_directory(True)
def root_lists(): """Retrieve initial video lists to display on homepage""" common.debug('Requesting root lists from API') return LoLoMo(common.make_call( 'path_request', [['lolomo', {'from': 0, 'to': 40}, ['displayName', 'context', 'id', 'index', 'length', 'genreId']]] + # Titles of first 4 videos in each video list [['lolomo', {'from': 0, 'to': 40}, {'from': 0, 'to': 3}, 'reference', ['title', 'summary']]] + # Art for first video in each video list # (will be displayed as video list art) build_paths(['lolomo', {'from': 0, 'to': 40}, {'from': 0, 'to': 0}, 'reference'], ART_PARTIAL_PATHS)))
def update_lolomo_context(context_name, video_id=None): """Update the lolomo list by context""" # Should update the context list but it doesn't, what is missing? # The remaining requests made on the website that are missing here are of logging type, # it seems strange that they use log data to finish the operations are almost impossible to reproduce here: # pbo_logblobs /logblob # personalization/cl2 lolomo_data = common.make_call('path_request', [["lolomo", [context_name], ['context', 'id', 'index']]]) # Note: lolomo root seem differs according to the profile in use lolomo_root = lolomo_data['lolomo'][1] context_index = lolomo_data['lolomos'][lolomo_root][context_name][2] context_id = lolomo_data['lolomos'][lolomo_root][context_index][1] path = [['lolomos', lolomo_root, 'refreshListByContext']] params = [common.enclose_quotes(context_id), context_index, common.enclose_quotes(context_name), common.enclose_quotes(g.LOCAL_DB.get_value('request_id', table=TABLE_SESSION))] # path_suffixs = [ # [['trackIds', 'context', 'length', 'genreId', 'videoId', 'displayName', 'isTallRow', 'isShowAsARow', # 'impressionToken', 'showAsARow', 'id', 'requestId']], # [{'from': 0, 'to': 100}, 'reference', 'summary'], # [{'from': 0, 'to': 100}, 'reference', 'title'], # [{'from': 0, 'to': 100}, 'reference', 'titleMaturity'], # [{'from': 0, 'to': 100}, 'reference', 'userRating'], # [{'from': 0, 'to': 100}, 'reference', 'userRatingRequestId'], # [{'from': 0, 'to': 100}, 'reference', 'boxarts', '_342x192', 'jpg'], # [{'from': 0, 'to': 100}, 'reference', 'promoVideo'] # ] callargs = { 'callpaths': path, 'params': params, # 'path_suffixs': path_suffixs } response = common.make_http_call('callpath_request', callargs) common.debug('refreshListByContext response: {}', response) callargs = { 'callpaths': [['refreshVideoCurrentPositions']], 'params': ['[' + video_id + ']', ''], } response = common.make_http_call('callpath_request', callargs) common.debug('refreshVideoCurrentPositions response: {}', response)
def show_profiles_dialog(title=None, title_prefix=None, preselect_guid=None): """ Show a dialog to select a profile :return guid of selected profile or None """ if not title: title = g.ADDON.getLocalizedString(30128) if title_prefix: title = title_prefix + ' - ' + title # Get profiles data list_data, extra_data = make_call('get_profiles', {'request_update': True}) # pylint: disable=unused-variable return show_modal_dialog(False, Profiles, 'plugin-video-netflix-Profiles.xml', g.ADDON.getAddonInfo('path'), title=title, list_data=list_data, preselect_guid=preselect_guid)
def _seasons(self, videoid, pathitems): """Show the seasons list of a tv show""" call_args = { 'pathitems': pathitems, 'tvshowid_dict': videoid.to_dict(), 'perpetual_range_start': self.perpetual_range_start, } list_data, extra_data = common.make_call('get_seasons', call_args) if len(list_data) == 1: # Check if Kodi setting "Flatten TV show seasons" is enabled value = common.json_rpc('Settings.GetSettingValue', {'setting': 'videolibrary.flattentvshows'}).get('value') if value != 0: # Values: 0=never, 1=if one season, 2=always # If there is only one season, load and show the episodes now pathitems = list_data[0]['url'].replace(G.BASE_URL, '').strip('/').split('/')[1:] videoid = common.VideoId.from_path(pathitems) self._episodes(videoid, pathitems) return self._seasons_directory(list_data, extra_data)
def custom_video_list_basicinfo(context_name, switch_profiles=False): """ Retrieve a single video list used only to know which videos are in my list without requesting additional information """ common.debug('Requesting custom video list basic info for {}', context_name) paths = build_paths([context_name, 'az', RANGE_SELECTOR], VIDEO_LIST_BASIC_PARTIAL_PATHS) callargs = { 'paths': paths, 'length_params': ['stdlist', [context_name, 'az']], 'perpetual_range_start': None, 'no_limit_req': True } # When the list is empty the server returns an empty response callname = 'perpetual_path_request_switch_profiles'\ if switch_profiles else 'perpetual_path_request' path_response = common.make_call(callname, callargs) return {} if not path_response else VideoListSorted(path_response, context_name, None, 'az')
def video_list(list_id): """Retrieve a single video list""" common.debug('Requesting video list {}'.format(list_id)) return VideoList( common.make_call( 'perpetual_path_request', { 'path_type': 'videolist', 'paths': build_paths(['lists', list_id, RANGE_SELECTOR, 'reference'], VIDEO_LIST_PARTIAL_PATHS), 'length_params1': list_id # Note: using this format: ["lists", "20c4b91b-2a3d-4fd9-ba82-4c7c63c8ccfe_31272600X19XX1551626066006", {"to": 47, "from": 0}, "reference", ... # the 'length' parameter never return to the actual total count of the elements # because the limit seems fixed at 40 from netflix # to pass it, we have to use 'az' or 'su' request by using a context name see video_list_az() }))
def rate_thumb(videoid, rating, track_id_jaw): """Rate a video on Netflix""" common.debug('Thumb rating {} as {}', videoid.value, rating) event_uuid = common.get_random_uuid() response = common.make_call( 'post', {'component': 'set_thumb_rating', 'data': { 'eventUuid': event_uuid, 'titleId': int(videoid.value), 'trackId': track_id_jaw, 'rating': rating, }}) if response.get('status', '') == 'success': ui.show_notification(common.get_local_string(30045).split('|')[rating]) else: common.error('Rating thumb error, response detail: {}', response) ui.show_error_info('Rating error', 'Error type: {}' + response.get('status', '--'), True, True)
def episodes(videoid): """Retrieve episodes of a season""" if videoid.mediatype != common.VideoId.SEASON: raise common.InvalidVideoId( 'Cannot request episode list for {}'.format(videoid)) common.debug('Requesting episode list for {}'.format(videoid)) return EpisodeList( videoid, common.make_call( 'perpetual_path_request', { 'path_type': 'episodelist', 'length_params': [videoid.seasonid], 'paths': [['seasons', videoid.seasonid, 'summary']] + build_paths( ['seasons', videoid.seasonid, 'episodes', RANGE_SELECTOR], EPISODES_PARTIAL_PATHS) + build_paths(['videos', videoid.tvshowid], ART_PARTIAL_PATHS + [['title']]) }))
def trailer(self, videoid): """Get the trailer list""" from json import dumps menu_data = {'path': ['is_context_menu_item', 'is_context_menu_item'], # Menu item do not exists 'title': common.get_local_string(30179)} video_id_dict = videoid.to_dict() list_data, extra_data = common.make_call('get_video_list_supplemental', # pylint: disable=unused-variable { 'menu_data': menu_data, 'video_id_dict': video_id_dict, 'supplemental_type': SUPPLEMENTAL_TYPE_TRAILERS }) if list_data: url = common.build_url(['supplemental'], params={'video_id_dict': dumps(video_id_dict), 'supplemental_type': SUPPLEMENTAL_TYPE_TRAILERS}, mode=G.MODE_DIRECTORY) common.container_update(url) else: ui.show_notification(common.get_local_string(30111))
def _metadata(video_id): """Retrieve additional metadata for a video.This is a separate method from metadata(videoid) to work around caching issues when new episodes are added to a show by Netflix.""" common.debug('Requesting metadata for {}', video_id) # Always use params 'movieid' to all videoid identifier metadata_data = common.make_call( 'get', { 'component': 'metadata', 'req_type': 'api', 'params': {'movieid': video_id.value} }) if not metadata_data: # This return empty # - if the metadata is no longer available # - if it has been exported a tv show/movie from a specific language profile that is not # available using profiles with other languages raise MetadataNotAvailable return metadata_data['video']
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 and not _profile_switch(): 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 # When a video is played from Kodi library or Up Next add-on is needed set infoLabels and art info to list_item if is_played_from_strm or is_upnext_enabled or G.IS_ADDON_EXTERNAL_CALL: info, arts = common.make_call('get_videoid_info', videoid) list_item.setInfo('video', info) list_item.setArt(arts) # Start and initialize the action controller (see action_controller.py) LOG.debug('Sending initialization signal') # Do not use send_signal as threaded slow devices are not powerful to send in faster way and arrive late to service common.send_signal( common.Signals.PLAYBACK_INITIATED, { 'videoid': videoid, 'is_played_from_strm': is_played_from_strm, 'resume_position': resume_position }) xbmcplugin.setResolvedUrl(handle=G.PLUGIN_HANDLE, succeeded=True, listitem=list_item)