def login(self, credentials=None): """Perform account login""" try: # First we get the authentication url without logging in, required for login API call react_context = website.extract_json(self.get('login'), 'reactContext') auth_url = website.extract_api_data(react_context)['auth_url'] LOG.debug('Logging in...') 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.account_hash, self.session.cookies) return True except LoginValidateError as exc: self.session.cookies.clear() common.purge_credentials() raise_from(LoginError(unicode(exc)), exc) except (MbrStatusNeverMemberError, MbrStatusFormerMemberError) as exc: self.session.cookies.clear() LOG.warn('Membership status {} not valid for login', exc) raise_from(LoginError(common.get_local_string(30180)), exc) except Exception: # pylint: disable=broad-except self.session.cookies.clear() import traceback LOG.error(G.py2_decode(traceback.format_exc(), 'latin-1')) raise
def root(self, pathitems=None): # pylint: disable=unused-argument """Show profiles or home listing when profile auto-selection is enabled""" # Fetch initial page to refresh all session data current_directory = common.WndHomeProps[ common.WndHomeProps.CURRENT_DIRECTORY] if not current_directory: # Note when the profiles are updated to the database (by fetch_initial_page call), # the update sanitize also relative settings to profiles (see _delete_non_existing_profiles in website.py) common.make_call('fetch_initial_page') # When the add-on is used in a browser window, we do not have to execute the auto profile selection if not G.IS_ADDON_EXTERNAL_CALL: autoselect_profile_guid = G.LOCAL_DB.get_value( 'autoselect_profile_guid', '') if autoselect_profile_guid and not common.WndHomeProps[ common.WndHomeProps.IS_CONTAINER_REFRESHED]: if not current_directory: LOG.info('Performing auto-selection of profile {}', autoselect_profile_guid) self.params[ 'switch_profile_guid'] = autoselect_profile_guid self.home(None) return list_data, extra_data = common.make_call('get_profiles', {'request_update': False}) self._profiles(list_data, extra_data)
def root(self, pathitems=None): # pylint: disable=unused-argument """Show profiles or home listing when profile auto-selection is enabled""" # Get the URL parent path of the navigation: xbmc.getInfoLabel('Container.FolderPath') # it can be found in Kodi log as "ParentPath = [xyz]" but not always return the exact value is_parent_root_path = xbmc.getInfoLabel( 'Container.FolderPath') == G.BASE_URL + '/' # Fetch initial page to refresh all session data if is_parent_root_path and not G.IS_CONTAINER_REFRESHED: common.make_call('fetch_initial_page') # Note when the profiles are updated to the database (by fetch_initial_page call), # the update sanitize also relative settings to profiles (see _delete_non_existing_profiles in website.py) autoselect_profile_guid = G.LOCAL_DB.get_value( 'autoselect_profile_guid', '') if autoselect_profile_guid and not G.IS_CONTAINER_REFRESHED: if is_parent_root_path: LOG.info('Performing auto-selection of profile {}', autoselect_profile_guid) # Do not perform the profile switch if navigation come from a page that is not the root url, # prevents profile switching when returning to the main menu from one of the sub-menus if not is_parent_root_path or activate_profile( autoselect_profile_guid): self.home(None, True) return # IS_CONTAINER_REFRESHED is temporary set from the profiles context menu actions # to avoid perform the fetch_initial_page/auto-selection every time when the container will be refreshed G.IS_CONTAINER_REFRESHED = False list_data, extra_data = common.make_call('get_profiles', {'request_update': False}) self._profiles(list_data, extra_data)
def _process_event_request(self, event): """Do the event post request""" event.status = Event.STATUS_REQUESTED # Request attempts can be made up to a maximum of 3 times per event LOG.info('EVENT [{}] - Executing request', event) endpoint_url = ENDPOINTS['events'] + create_req_params( 20 if event.event_type == EVENT_START else 0, 'events/{}'.format(event)) try: response = self.chunked_request(endpoint_url, event.request_data, get_esn(), disable_msl_switch=False) # Malformed/wrong content in requests are ignored without returning error feedback in the response event.set_response(response) except Exception as exc: # pylint: disable=broad-except LOG.error('EVENT [{}] - The request has failed: {}', event, exc) event.set_response('RequestError') if event.event_type == EVENT_STOP and event.status == Event.STATUS_SUCCESS: self.clear_queue() if event.event_data['allow_request_update_loco']: # Calls to nfsession common.make_http_call('update_loco_context', {'context_name': 'continueWatching'}) common.make_http_call('update_videoid_bookmark', {'video_id': event.get_video_id()}) return True
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 _compute_next_schedule(date_last_start=None): try: if G.ADDON.getSettingBool('use_mysql'): client_uuid = G.LOCAL_DB.get_value('client_uuid') uuid = G.SHARED_DB.get_value('auto_update_device_uuid') if client_uuid != uuid: LOG.debug( 'The auto update has been disabled because another device ' 'has been set as the main update manager') return None time = G.ADDON.getSetting('lib_auto_upd_start') or '00:00' last_run = date_last_start or G.SHARED_DB.get_value( 'library_auto_update_last_start', datetime.utcfromtimestamp(0)) update_frequency = G.ADDON.getSettingInt('lib_auto_upd_freq') last_run = last_run.replace(hour=int(time[0:2]), minute=int(time[3:5])) next_run = last_run + timedelta(days=[1, 2, 5, 7][update_frequency]) if next_run >= datetime.now(): LOG.info('Next library auto update is scheduled for {}', next_run) return next_run except Exception: # pylint: disable=broad-except # If settings.xml was not created yet, as at first service run # G.ADDON.getSettingBool('use_mysql') will thrown a TypeError # If any other error appears, we don't want the service to crash, # let's return None in all case # import traceback # LOG.debug(G.py2_decode(traceback.format_exc(), 'latin-1')) LOG.warn('Managed error at _compute_next_schedule') return None
def on_playback_started(self, player_state): if self.resume_position: # Due to a bug on Kodi the resume on STRM files not works correctly, # so we force the skip to the resume point LOG.info('AMPlayback has forced resume point to {}', self.resume_position) xbmc.Player().seekTime(int(self.resume_position))
def activate_profile(self, guid): """Set the profile identified by guid as active""" LOG.debug('Switching to profile {}', guid) current_active_guid = G.LOCAL_DB.get_active_profile_guid() if guid == current_active_guid: LOG.info('The profile guid {} is already set, activation not needed.', guid) return if xbmc.Player().isPlayingVideo(): # Change the current profile while a video is playing can cause problems with outgoing HTTP requests # (MSL/NFSession) causing a failure in the HTTP request or sending data on the wrong profile raise Warning('It is not possible select a profile while a video is playing.') timestamp = time.time() LOG.info('Activating profile {}', guid) # 20/05/2020 - The method 1 not more working for switching PIN locked profiles # INIT Method 1 - HTTP mode # response = self._get('switch_profile', params={'tkn': guid}) # self.nfsession.auth_url = self.website_extract_session_data(response)['auth_url'] # END Method 1 # INIT Method 2 - API mode try: self.get_safe(endpoint='activate_profile', params={'switchProfileGuid': guid, '_': int(timestamp * 1000), 'authURL': self.auth_url}) except HttpError401 as exc: # Profile guid not more valid raise InvalidProfilesError('Unable to access to the selected profile.') from exc # Retrieve browse page to update authURL response = self.get_safe('browse') self.auth_url = website.extract_session_data(response)['auth_url'] # END Method 2 G.LOCAL_DB.switch_active_profile(guid) G.CACHE_MANAGEMENT.identifier_prefix = guid cookies.save(self.session.cookies)
def on_tick(self, player_state): # Stops playback when paused for more than one hour. # Some users leave the playback paused also for more than 12 hours, # this complicates things to resume playback, because the manifest data expires and with it also all # the streams urls are no longer guaranteed, so we force the stop of the playback. if self.is_player_in_pause and (time.time() - self.start_time) > 3600: LOG.info('The playback has been stopped because it has been exceeded 1 hour of pause') common.stop_playback()
def shutdown(self): """Stop the background services""" self._set_service_status('stopped') self.nf_server_instance.shutdown() self.nf_server_instance.server_close() self.nf_server_instance = None self.nf_server_thread.join() self.nf_server_thread = None LOG.info('Stopped MSL Service')
def _init_msl_handler(self): self.msl_requests = None try: msl_data = json.loads(common.load_file_def(MSL_DATA_FILENAME)) LOG.info('Loaded MSL data from disk') except Exception: # pylint: disable=broad-except msl_data = None self.msl_requests = MSLRequests(msl_data) self.switch_events_handler()
def sync_library_with_mylist(self): """ Perform a full sync of Kodi library with Netflix "My List", by deleting everything that was previously exported """ LOG.info('Performing sync of Kodi library with My list') # Clear all the library self.clear_library() # Start the sync self.auto_update_library(True, show_nfo_dialog=True, clear_on_cancel=True)
def import_library(self, path): """ Imports an already existing exported STRM library into the add-on library database, allows you to restore an existing library, by avoiding to recreate it from scratch. This operations also update the missing tv shows seasons and episodes, and automatically converts old STRM format type from add-on version 0.13.x or before 1.7.0 to new format. """ # If set ask to user if want to export NFO files nfo_settings = nfo.NFOSettings() nfo_settings.show_export_dialog() LOG.info('Start importing Kodi library') remove_folders = [ ] # List of failed imports paths to be optionally removed remove_titles = [ ] # List of failed imports titles to be optionally removed # Start importing STRM files folders = get_library_subfolders(FOLDER_NAME_MOVIES, path) + get_library_subfolders( FOLDER_NAME_SHOWS, path) with ui.ProgressDialog(True, max_value=len(folders)) as progress_bar: for folder_path in folders: folder_name = os.path.basename( G.py2_decode(translatePath(folder_path))) progress_bar.set_message(folder_name) try: videoid = self.import_videoid_from_existing_strm( folder_path, folder_name) if videoid is None: # Failed to import, add folder to remove list remove_folders.append(folder_path) remove_titles.append(folder_name) continue # Successfully imported, Execute the task for index, total_tasks, title in self.execute_library_task( videoid, self.export_item, nfo_settings=nfo_settings, notify_errors=True): 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('Import library interrupted by User') return if self.monitor.abortRequested(): LOG.warn('Import library interrupted by Kodi') return except ImportWarning: # Ignore it, something was wrong in STRM file (see _import_videoid in library_jobs.py) pass progress_bar.perform_step() progress_bar.set_wait_message() delay_anti_ban() ret = self._import_library_remove(remove_titles, remove_folders) request_kodi_library_update(scan=True, clean=ret)
def remove_from_library(self, videoid, show_prg_dialog=True): """ Remove an item from the Kodi library :param videoid: the videoid :param show_prg_dialog: if True show progress dialog, otherwise, a background progress bar """ LOG.info('Start removing {} from library', videoid) common.remove_videoid_from_kodi_library(videoid) self.execute_library_task_gui(videoid, self.remove_item, title=common.get_local_string(30030), show_prg_dialog=show_prg_dialog)
def run(argv): # Initialize globals right away to avoid stale values from the last addon invocation. # Otherwise Kodi's reuseLanguageInvoker will cause some really quirky behavior! # PR: https://github.com/xbmc/xbmc/pull/13814 G.init_globals(argv) LOG.info('Started (Version {})'.format(G.VERSION_RAW)) LOG.info('URL is {}'.format(G.URL)) success = True window_cls = Window(10000) # Kodi home window # If you use multiple Kodi profiles you need to distinguish the property of current profile prop_nf_service_status = G.py2_encode('nf_service_status_' + get_current_kodi_profile_name()) is_external_call = _check_addon_external_call(window_cls, prop_nf_service_status) service_status = _get_service_status(window_cls, prop_nf_service_status) if service_status.get('status') != 'running': if not is_external_call: if service_status.get('status') == 'error': # The services are not started due to an error exception from resources.lib.kodi.ui import show_error_info show_error_info( get_local_string(30105), get_local_string(30240).format( service_status.get('message')), False, False) else: # The services are not started yet from resources.lib.kodi.ui import show_backend_not_ready show_backend_not_ready() success = False if success: cancel_playback = False pathitems = [part for part in G.REQUEST_PATH.split('/') if part] if G.IS_ADDON_FIRSTRUN: is_first_run_install, cancel_playback = check_addon_upgrade() if is_first_run_install: from resources.lib.config_wizard import run_addon_configuration run_addon_configuration() if cancel_playback and G.MODE_PLAY in pathitems[:1]: # Temporary for migration library STRM to new format. todo: to be removed in future releases # When a user do the add-on upgrade, the first time that the add-on will be opened will be executed # the library migration. But if a user instead to open the add-on, try to play a video from Kodi # library, Kodi will open the old STRM file because the migration is executed after. success = False else: success = route(pathitems) if not success: from xbmcplugin import endOfDirectory endOfDirectory(handle=G.PLUGIN_HANDLE, succeeded=False) LOG.log_time_trace()
def shutdown(self): """ Stop the background services """ self._set_service_status('stopped') for server in self.SERVERS: server['instance'].shutdown() server['instance'].server_close() server['instance'] = None server['thread'].join() server['thread'] = None LOG.info('Stopped MSL Service')
def _auto_skip(self, section): LOG.info('Auto-skipping {}', section) player = xbmc.Player() ui.show_notification( common.get_local_string(SKIPPABLE_SECTIONS[section])) if self.pause_on_skip: player.pause() xbmc.sleep(1000) # give kodi the chance to execute player.seekTime(self.markers[section]['end']) xbmc.sleep(1000) # give kodi the chance to execute player.pause() # unpause playback at seek position else: player.seekTime(self.markers[section]['end'])
def login_auth_data(self, data=None, password=None): """Perform account login with authentication data""" from requests import exceptions LOG.debug('Logging in with authentication data') # Add the cookies to the session self.session.cookies.clear() for cookie in data['cookies']: self.session.cookies.set(cookie[0], cookie[1], **cookie[2]) cookies.log_cookie(self.session.cookies) # Try access to website try: website.extract_session_data(self.get('browse'), validate=True, update_profiles=True) except MbrStatusAnonymousError: # Access not valid return False # Get the account e-mail page_response = self.get('your_account').decode('utf-8') email_match = re.search(r'account-email[^<]+>([^<]+@[^</]+)</', page_response) email = email_match.group(1).strip() if email_match else None if not email: raise WebsiteParsingError('E-mail field not found') # Verify the password (with parental control api) try: response = self.post_safe('profile_hub', data={ 'destination': 'contentRestrictions', 'guid': G.LOCAL_DB.get_active_profile_guid(), 'password': password, 'task': 'auth' }) if response.get('status') != 'ok': raise LoginError(common.get_local_string( 12344)) # 12344=Passwords entered did not match. except exceptions.HTTPError as exc: if exc.response.status_code == 500: # This endpoint raise HTTP error 500 when the password is wrong raise LoginError(common.get_local_string(12344)) from exc raise common.set_credentials({'email': email, 'password': password}) LOG.info('Login successful') ui.show_notification(common.get_local_string(30109)) cookies.save(self.session.cookies) return True
def export_to_library(self, videoid, show_prg_dialog=True): """ Export an item to the Kodi library :param videoid: the videoid :param show_prg_dialog: if True show progress dialog, otherwise, a background progress bar """ LOG.info('Start exporting {} to the library', videoid) nfo_settings = nfo.NFOSettings() nfo_settings.show_export_dialog(videoid.mediatype) self.execute_library_task_gui(videoid, self.export_item, title=common.get_local_string(30018), nfo_settings=nfo_settings, show_prg_dialog=show_prg_dialog)
def _load_cookies(self): """Load stored cookies from disk""" # pylint: disable=broad-except if not self.session.cookies: try: self.session.cookies = cookies.load() except MissingCookiesError: return False except Exception as exc: import traceback LOG.error('Failed to load stored cookies: {}', type(exc).__name__) LOG.error(traceback.format_exc()) return False LOG.info('Successfully loaded stored cookies') return True
def _verify_session_cookies(self): """Verify that the session cookies have not expired""" if not self.session.cookies: return False for cookie_name in LOGIN_COOKIES: if cookie_name not in list(self.session.cookies.keys()): LOG.error('The cookie "{}" do not exist, it is not possible to check the expiration', cookie_name) return False for cookie in self.session.cookies.jar: if cookie.name != cookie_name: continue if cookie.expires <= int(time.time()): LOG.info('Login is expired') return False return True
def start_services(self): """Start the background services""" from resources.lib.services.library_updater import LibraryUpdateService self.nf_server_instance.server_activate() self.nf_server_thread.start() LOG.info('[NF_SERVER] Thread started') self.library_updater = LibraryUpdateService() # We reset the value in case of any eventuality (add-on disabled, update, etc) WndHomeProps[WndHomeProps.CURRENT_DIRECTORY] = None # Mark the service as active self._set_service_status('running') if not G.ADDON.getSettingBool('disable_startup_notification'): from resources.lib.kodi.ui import show_notification show_notification(get_local_string(30110))
def _init_session(self): """Initialize the session to use for all future connections""" try: self.session.close() LOG.info('Session closed') except AttributeError: pass from requests import session self.session = session() self.session.max_redirects = 10 # Too much redirects should means some problem self.session.headers.update({ 'User-Agent': common.get_user_agent(enable_android_mediaflag_fix=True), 'Accept-Encoding': 'gzip, deflate, br', 'Host': 'www.netflix.com' }) LOG.info('Initialized new session')
def export_to_library_new_episodes(self, videoid, show_prg_dialog=True): """ Export new episodes for a tv show by it's videoid :param videoid: The videoid of the tv show to process :param show_prg_dialog: if True show progress dialog, otherwise, a background progress bar """ LOG.info('Start exporting new episodes for {}', videoid) if videoid.mediatype != common.VideoId.SHOW: LOG.warn('{} is not a tv show, the operation is cancelled', videoid) return nfo_settings = nfo.NFOSettings() nfo_settings.show_export_dialog(videoid.mediatype) self.execute_library_task_gui(videoid, self.export_new_item, title=common.get_local_string(30198), nfo_settings=nfo_settings, show_prg_dialog=show_prg_dialog)
def logout(self): """Logout of the current account and reset the session""" LOG.debug('Logging out of current account') # Perform the website logout self.get('logout') G.settings_monitor_suspend(True) # Disable and reset auto-update / auto-sync features G.ADDON.setSettingInt('lib_auto_upd_mode', 1) G.ADDON.setSettingBool('lib_sync_mylist', False) G.SHARED_DB.delete_key('sync_mylist_profile_guid') # Disable and reset the auto-select profile G.LOCAL_DB.set_value('autoselect_profile_guid', '') G.ADDON.setSetting('autoselect_profile_name', '') G.ADDON.setSettingBool('autoselect_profile_enabled', False) # Reset of selected profile guid for library playback G.LOCAL_DB.set_value('library_playback_profile_guid', '') G.ADDON.setSetting('library_playback_profile', '') G.settings_monitor_suspend(False) # Delete cookie and credentials self.session.cookies.clear() cookies.delete(self.account_hash) common.purge_credentials() # Reset the ESN obtained from website/generated G.LOCAL_DB.set_value('esn', '', TABLE_SESSION) # Reinitialize the MSL handler (delete msl data file, then reset everything) common.send_signal(signal=common.Signals.REINITIALIZE_MSL_HANDLER, data=True) G.CACHE.clear(clear_database=True) LOG.info('Logout successful') ui.show_notification(common.get_local_string(30113)) self._init_session() common.container_update('path', True) # Go to a fake page to clear screen # Open root page common.container_update(G.BASE_URL, True)
def perform_key_handshake(self, data=None): # pylint: disable=unused-argument """Perform a key handshake and initialize crypto keys""" esn = get_esn() if not esn: LOG.warn('Cannot perform key handshake, missing ESN') return False LOG.info('Performing key handshake with ESN: {}', common.censure(esn) if G.ADDON.getSetting('esn') else esn) response = _process_json_response( self._post(ENDPOINTS['manifest'], self.handshake_request(esn))) header_data = self.decrypt_header_data(response['headerdata'], False) self.crypto.parse_key_response(header_data, esn, True) # 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 clear_library(self, show_prg_dialog=True): """ Delete all exported items to the library :param show_prg_dialog: if True, will be show a progress dialog window """ LOG.info('Start deleting exported library items') # This will clear all the add-on library data, to prevents possible inconsistencies when for some reason # such as improper use of the add-on, unexpected error or other has broken the library database data or files with ui.ProgressDialog(show_prg_dialog, common.get_local_string(30245), max_value=3) as progress_dlg: progress_dlg.perform_step() progress_dlg.set_wait_message() G.SHARED_DB.purge_library() for folder_name in [FOLDER_NAME_MOVIES, FOLDER_NAME_SHOWS]: progress_dlg.perform_step() progress_dlg.set_wait_message() section_root_dir = common.join_folders_paths(get_library_path(), folder_name) common.delete_folder_contents(section_root_dir, delete_subfolders=True) # Clean the Kodi library database common.clean_library(show_prg_dialog)
def root(self, pathitems=None): # pylint: disable=unused-argument """Show profiles or home listing when profile auto-selection is enabled""" # Fetch initial page to refresh all session data if G.CURRENT_LOADED_DIRECTORY is None: # Note when the profiles are updated to the database (by fetch_initial_page call), # the update sanitize also relative settings to profiles (see _delete_non_existing_profiles in website.py) common.make_call('fetch_initial_page') autoselect_profile_guid = G.LOCAL_DB.get_value( 'autoselect_profile_guid', '') if autoselect_profile_guid and not G.IS_CONTAINER_REFRESHED: if G.CURRENT_LOADED_DIRECTORY is None: LOG.info('Performing auto-selection of profile {}', autoselect_profile_guid) self.params['switch_profile_guid'] = autoselect_profile_guid self.home(None) return list_data, extra_data = common.make_call('get_profiles', {'request_update': False}) self._profiles(list_data, extra_data)
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 _import_library_remove(self, remove_titles, remove_folders): if not remove_folders: return False # If there are STRM files that it was not possible to import them, # we will ask to user if you want to delete them tot_folders = len(remove_folders) if tot_folders > 50: remove_titles = remove_titles[:50] + ['...'] message = common.get_local_string(30246).format(tot_folders) + '[CR][CR]' + ', '.join(remove_titles) if not ui.ask_for_confirmation(common.get_local_string(30140), message): return False # Delete all folders LOG.info('Start deleting folders') with ui.ProgressDialog(True, max_value=tot_folders) as progress_bar: for file_path in remove_folders: progress_bar.set_message('{}/{}'.format(progress_bar.value, tot_folders)) LOG.debug('Deleting folder: {}', file_path) common.delete_folder(file_path) progress_bar.perform_step() return True