def start_services(self): """Start the background services""" self.dial_srv_instance.server_activate() self.dial_srv_thread.start() LOG.info('[DialServer] service started') self.ssdp_udp_srv_thread.start() LOG.info('[SSDPUDPServer] service started')
def reporthook(num_blocks, block_size, file_size, dlg, start_time, filename): try: percent = min(num_blocks * block_size * 100 / file_size, 100) currently_downloaded = float(num_blocks) * block_size / (1024 * 1024) kbps_speed = num_blocks * block_size / (time.time() - start_time) eta = 0 if kbps_speed > 0: eta = (file_size - num_blocks * block_size) / kbps_speed if eta < 0: eta = 0 kbps_speed = kbps_speed / 1024 eta = divmod(eta, 60) file_size_mb = float(file_size) / (1024 * 1024) status = '{:.02f} MB of {:.02f} MB '.format(currently_downloaded, file_size_mb) status += '{} {:.02f} Kb/s '.format(get_local_string(30501), kbps_speed) status += '{} {:02d}:{:02d}'.format(get_local_string(30502), int(eta[0]), int(eta[1])) dlg.update( int(percent), '{}[CR]{}[CR]{}'.format(get_local_string(30500), filename, status)) except Exception as exc: LOG.error('[download_file] reporthook raised an error: {}', exc) dlg.update(100) if dlg.iscanceled(): raise InterruptedError
def register_internal_apps(kodi_interface): """Register the internal DIAL applications based on the installed Kodi add-ons""" # The DIAL apps will be loaded from "resources/lib/apps" sub-folders directory_base = file_ops.join_folders_paths(G.ADDON_DATA_PATH, 'resources/lib/apps') dirs, _ = file_ops.list_dir(directory_base) MUTEX.acquire() for dir_name in dirs: try: if dir_name.startswith('_') or dir_name == 'dial_app_test': continue class_file_path = file_ops.join_folders_paths( directory_base, dir_name, dir_name + '.py') if not file_ops.file_exists(class_file_path): LOG.error( 'register_internal_apps: missing module file {}.py on {} folder', dir_name, dir_name) continue # Load the internal module loaded_module = importlib.import_module('resources.lib.apps.' + dir_name + '.' + dir_name) app_class = getattr(loaded_module, 'DialApp', None) if app_class is None: LOGGER.error( 'register_internal_apps: "DialApp" class not found in {}.py file', dir_name) continue register_app(app_class, kodi_interface) except Exception: LOGGER.error( 'register_internal_apps: could not import the DIAL app from {} folder', dir_name) import traceback LOG.error(traceback.format_exc()) MUTEX.release()
def init_globals(self): """Initialized globally used module variables. Needs to be called at start of each plugin instance!""" # xbmcaddon.Addon must be created at every instance otherwise it does not read any new changes to the settings self.ADDON = xbmcaddon.Addon() self.ADDON_ID = self.ADDON.getAddonInfo('id') self.PLUGIN = self.ADDON.getAddonInfo('name') self.ICON = self.ADDON.getAddonInfo('icon') self.ADDON_DATA_PATH = self.ADDON.getAddonInfo('path') # Add-on folder self.DATA_PATH = self.ADDON.getAddonInfo( 'profile') # Add-on user data folder try: self.IS_SERVICE = False except IndexError: self.IS_SERVICE = True # Initialize the log from resources.lib.helpers.logging import LOG LOG.initialize(self.ADDON_ID, G.ADDON.getSettingBool('enable_debug'), G.ADDON.getSettingBool('debug_dial_server'), G.ADDON.getSettingBool('debug_ssdp_server'), G.ADDON.getSettingBool('debug_apps')) # Set SSDP server variables self.SP_FRIENDLY_NAME = xbmc.getInfoLabel( "System.FriendlyName") or 'Kodi (AppCast)' self.SP_MODEL_NAME = 'MyDeviceModel' self.SP_MANUFACTURER_NAME = ' ' self.DEVICE_UUID = get_system_appcast_uuid()
def register_addonsignals_slot(callback, signal=None, source_id=None): """Register a callback with AddonSignals for return calls""" name = signal if signal else callback.__name__ AddonSignals.registerSlot(signaler_id=source_id or IPC_ADDON_ID, signal=name, callback=callback) LOG.debug('Registered AddonSignals slot {} to {}'.format(name, callback))
def register_apps(kodi_interface): """Register DIAL applications based on the Kodi add-ons that support Cast feature""" # We find out which add-ons has "script.appcast" optional dependency, # then for each add-on we try import the "DialApp" class included in the path "resources/lib/dial_app_test/" addons = kodi_ops.json_rpc( 'Addons.GetAddons', { 'type': 'unknown', 'properties': ['name', 'dependencies', 'enabled', 'path'] }) MUTEX.acquire() # # Clear previous sys modules added # for name in list(sys.modules.keys()): # if name.startswith('dial_app_'): # del sys.modules[name] for addon in addons['addons']: if not addon['enabled']: continue if any(dep['addonid'] == 'script.appcast' for dep in addon['dependencies']): # Try add the DIAL app included in the add-on try: name = addon['addonid'].split('.')[-1] package = 'dial_app_' + name module_path_folder1 = file_ops.join_folders_paths( addon['path'], package, package + '.py') module_path_folder2 = file_ops.join_folders_paths( addon['path'], 'resources/lib/' + package, package + '.py') # Check if the add-on has the package file if file_ops.file_exists(module_path_folder1): module_path = module_path_folder1 elif file_ops.file_exists(module_path_folder2): module_path = module_path_folder2 else: LOGGER.error( 'register_apps: missing module file {}.py on {} add-on', package, addon['addonid']) continue # Load the external module (and allow it's own relative imports) spec = importlib.util.spec_from_file_location( package, module_path, submodule_search_locations=[]) module = importlib.util.module_from_spec(spec) sys.modules[module.__name__] = module spec.loader.exec_module(module) # Get the "DialApp" class from the loaded module app_class = getattr(module, 'DialApp', None) if app_class is None: LOGGER.error( 'register_apps: "DialApp" class not found in {}.py file of {} add-on', package, addon['addonid']) continue register_app(app_class, kodi_interface) except Exception: LOGGER.error( 'register_apps: could not import the DIAL app from {}', addon['addonid']) import traceback LOG.error(traceback.format_exc()) MUTEX.release()
def run(self): while not self._stop_event.is_set(): if not self._notify_apps( 'on_playback_tick', {'is_playback_paused': self.is_playback_paused}): LOG.warn('PlaybackTick: Interrupted due to an error') break if self._stop_event.wait(self._timeout_secs): break # Stop requested by stop_join
def _notify_all_apps(self, callback_name, data=None, extra_data_app=None): for _app in self._apps: _data = deepcopy(data) if extra_data_app[0] == self._active_app: # If current app then send extra data only for this app _data.update(extra_data_app[1]) LOG.debug('Notify Kodi callback {} to {} with data: {}', callback_name, _app.DIAL_APP_NAME, _data) self._execute_notify(_app, callback_name, _data)
def _execute(executor_type, pathitems, params): """Execute an action as specified by the path""" try: executor = executor_type(params).__getattribute__( pathitems[0] if pathitems else 'root') except AttributeError as exc: raise InvalidPathError('Unknown action {}'.format( '/'.join(pathitems))) from exc LOG.debug('Invoking action: {}', executor.__name__) executor(pathitems=pathitems[1:])
def _notify_apps(self, callback_name, data=None): if self._active_app is None: LOG.warn('Ignored Kodi callback {}, no app currently active', callback_name) return False self._mutex.acquire() LOG.debug('Notify Kodi callback {} to {} with data: {}', callback_name, self._active_app.DIAL_APP_NAME, data) ret = self._execute_notify(self._active_app, callback_name, data) self._mutex.release() return ret
def run(self): """Main loop. Runs until xbmc.Monitor requests abort""" try: self.start_services() monitor = xbmc.Monitor() while not monitor.abortRequested(): monitor.waitForAbort(1) self.shutdown() except Exception: # pylint: disable=broad-except import traceback LOG.error(traceback.format_exc())
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_INSTALL: from resources.lib.navigation.install import install install(pathitems[1:], G.REQUEST_PARAMS) else: nav_handler = _get_nav_handler(root_handler, pathitems) _execute(nav_handler, pathitems[1:], G.REQUEST_PARAMS) return True
def _perform_ipc_return_call_instance(instance, func, data): try: result = _call_with_instance(instance, func, data) except Exception as exc: # pylint: disable=broad-except LOG.error('IPC callback raised exception: {exc}', exc=exc) import traceback LOG.error(traceback.format_exc()) result = ipc_convert_exc_to_json(exc) AddonSignals.returnCall(signal=func.__name__, source_id=IPC_ADDON_ID, data=result) return result
def _execute_notify(app, callback_name, data): try: method = getattr(app, callback_name) method(data) return True except Exception: # pylint: disable=broad-except LOG.error( 'The app {} has raised the following error on {} callback:', app.DIAL_APP_NAME, callback_name) import traceback LOG.error(traceback.format_exc()) return False
def install(pathitems, params): LOG.info('Start install Kodi "{}" (params "{}")', pathitems[-1], params) use_task_scheduler = G.ADDON.getSettingBool('usetaskscheduler') save_downloads = G.ADDON.getSettingBool('save_downloads') # Download the file if not kodi_ops.dlg_confirm( kodi_ops.get_local_string(30070), kodi_ops.get_local_string(30071).format(pathitems[-1])): return # Check if the task is installed if use_task_scheduler and not misc.check_task(): kodi_ops.dlg_ok(kodi_ops.get_local_string(30070), kodi_ops.get_local_string(30072)) return # Check if destination path exist if not folder_exists(G.INSTALLER_TEMP_PATH): create_folder(G.INSTALLER_TEMP_PATH) # Check if the setup installer is already downloaded dwn_filepath = join_folders_paths(G.DOWNLOADS_PATH, '/'.join(pathitems[:-1]), pathitems[-1]) # Temp file path will be used by the Windows Task scheduled temp_filepath = join_folders_paths(G.INSTALLER_TEMP_PATH, G.INSTALLER_TEMP_NAME) if params.get('is_local', 'False') == 'True': # Get the file to install from "downloads" folder if not file_exists(dwn_filepath): raise FileExistsError('The file {] not exists'.format( pathitems[:-1])) copy_file(dwn_filepath, temp_filepath) else: # Download the file if save_downloads and file_exists(dwn_filepath): # Copy the existing file to the temp file path copy_file(dwn_filepath, temp_filepath) else: # Download the setup installer file url_file_path = '/'.join(pathitems) if not download_file(G.MIRROR_BASE_URL + url_file_path, temp_filepath, pathitems[-1]): # Download cancelled kodi_ops.show_notification(kodi_ops.get_local_string(30073)) return # Save the setup installer file if save_downloads: copy_file(temp_filepath, dwn_filepath) with kodi_ops.show_busy_dialog(): # Run the "AutoUpdateWorker" bash script _run_auto_update_worker(use_task=use_task_scheduler) # Wait a bit before close Kodi, # the bash script have to read the executable path from the Kodi process before continue time.sleep(2) kodi_ops.json_rpc('Application.Quit')
def make_addonsignals_call(callname, data=None): """Make an IPC call via AddonSignals and wait for it to return. The contents of data will be expanded to kwargs and passed into the target function.""" LOG.debug('Handling AddonSignals IPC call to {}'.format(callname)) try: result = AddonSignals.makeCall(source_id=IPC_ADDON_ID, signal=callname, data=data, timeout_ms=IPC_TIMEOUT_SECS * 1000, use_timeout_exception=True) _raise_for_error(result) except AddonSignals.WaitTimeoutError: raise Exception('Addon Signals call timeout') return result
def shutdown(self): """Stop the background services""" self.dial_srv_instance.shutdown() self.dial_srv_instance.server_close() self.dial_srv_instance = None self.dial_srv_thread.join() self.dial_srv_thread = None LOG.info('[DialServer] service stopped') self.ssdp_udp_srv_instance.shutdown() self.ssdp_udp_srv_instance.server_close() self.ssdp_udp_srv_instance = None self.ssdp_udp_srv_thread.join() self.ssdp_udp_srv_thread = None LOG.info('[SSDPUPDServer] service stopped')
def init_servers(self): """Initialize the servers""" try: self.dial_srv_instance = dial_server.DialTCPServer((self.HOST_ADDRESS, G.DIAL_SERVER_PORT)) self.dial_srv_instance.allow_reuse_address = True self.dial_srv_thread = threading.Thread(target=self.dial_srv_instance.serve_forever) self.ssdp_udp_srv_instance = ssdp_server.SSDPUDPServer() self.ssdp_udp_srv_instance.allow_reuse_address = True self.ssdp_udp_srv_thread = threading.Thread(target=self.ssdp_udp_srv_instance.serve_forever) return True except Exception: # pylint: disable=broad-except import traceback LOG.error(traceback.format_exc()) return False
def convert_to_string(value): if value is None: return None data_type = type(value) if data_type == str: return value converter = None if data_type in (int, float, bool, tuple, datetime.datetime): converter = _conv_standard_to_string if data_type in (list, dict, OrderedDict): converter = _conv_json_to_string if not converter: LOG.error( 'convert_to_string: Data type {} not mapped'.format(data_type)) raise DataTypeNotMapped return converter(value)
def _get_system_uuid(): uuid_value = None system = get_system_platform() if system in ['windows', 'uwp']: uuid_value = _get_windows_uuid() elif system == 'android': uuid_value = _get_android_uuid() elif system == 'linux': uuid_value = _get_linux_uuid() elif system == 'osx': # Due to OS restrictions on 'ios' and 'tvos' is not possible to use _get_macos_uuid() # See python limits in the wiki development page uuid_value = _get_macos_uuid() if not uuid_value: LOG.debug('It is not possible to get a system UUID creating a new UUID') uuid_value = _get_fake_uuid(system not in ['android', 'linux']) return str(uuid_value)
def convert_from_string(value, to_data_type): if value is None: return None if to_data_type in (str, int, float): return to_data_type(value) if to_data_type in (bool, list, tuple): return literal_eval(value) converter = None if to_data_type == dict: converter = _conv_string_to_json if to_data_type == datetime.datetime: converter = _conv_string_to_datetime if not converter: LOG.error('convert_from_string: Data type {} not mapped'.format( to_data_type)) raise DataTypeNotMapped return converter(value)
def download_file(url, dest_path, filename): start_time = time.time() dlg = xbmcgui.DialogProgress() dlg.create(G.ADDON_ID, get_local_string(30499)) try: urlretrieve( url.rstrip('/'), dest_path, lambda num_blocks, block_size, file_size: reporthook( num_blocks, block_size, file_size, dlg, start_time, filename)) return True except InterruptedError: LOG.error('Download interrupted by user') except Exception as exc: LOG.error('Download failed due to an error: {}', exc) raise Exception('Download failed') from exc finally: dlg.close() return False
def init_globals(self, argv): """Initialized globally used module variables. Needs to be called at start of each plugin instance!""" # IS_ADDON_FIRSTRUN: specifies if the add-on has been initialized for the first time # (reuseLanguageInvoker not used yet) self.IS_ADDON_FIRSTRUN = self.IS_ADDON_FIRSTRUN is None # xbmcaddon.Addon must be created at every instance otherwise it does not read any new changes to the settings self.ADDON = xbmcaddon.Addon() self.URL = urlparse(argv[0]) self.REQUEST_PATH = unquote(self.URL[2][1:]) try: self.PARAM_STRING = argv[2][1:] except IndexError: self.PARAM_STRING = '' self.REQUEST_PARAMS = dict(parse_qsl(self.PARAM_STRING)) if self.IS_ADDON_FIRSTRUN: # Global variables that do not need to be generated at every instance self.ADDON_ID = self.ADDON.getAddonInfo('id') self.PLUGIN = self.ADDON.getAddonInfo('name') self.VERSION_RAW = self.ADDON.getAddonInfo('version') self.VERSION = remove_ver_suffix(self.VERSION_RAW) self.ICON = self.ADDON.getAddonInfo('icon') self.ADDON_DATA_PATH = self.ADDON.getAddonInfo('path') # Add-on folder self.DATA_PATH = self.ADDON.getAddonInfo('profile') # Add-on user data folder try: self.PLUGIN_HANDLE = int(argv[1]) self.IS_SERVICE = False self.BASE_URL = '{scheme}://{netloc}'.format(scheme=self.URL[0], netloc=self.URL[1]) except IndexError: self.PLUGIN_HANDLE = 0 self.IS_SERVICE = True self.BASE_URL = '{scheme}://{netloc}'.format(scheme='plugin', netloc=self.ADDON_ID) # Initialize the log from resources.lib.helpers.logging import LOG LOG.initialize(self.ADDON_ID, self.PLUGIN_HANDLE, self.ADDON.getSettingString('debug_log_level'), self.ADDON.getSettingBool('enable_timing')) # Temporary file path (use to download and run the installer) from resources.lib.helpers.file_ops import translate_path self.INSTALLER_TEMP_PATH = translate_path(G.DATA_PATH) + 'temp/' self.INSTALLER_TEMP_NAME = 'KodiInstaller.exe' # Mush be equal to all scripts self.DOWNLOADS_PATH = translate_path(G.DATA_PATH) + 'downloads/'
def _get_macos_uuid(): # pylint: disable=broad-except import subprocess sp_dict_values = None try: proc = subprocess.Popen( ['/usr/sbin/system_profiler', 'SPHardwareDataType', '-detaillevel', 'full', '-xml'], stdout=subprocess.PIPE) output_data = proc.communicate()[0].decode('utf-8') if output_data: sp_dict_values = _parse_osx_xml_plist_data(output_data) except Exception as exc: LOG.debug('Failed to fetch OSX/IOS system profile {}'.format(exc)) if sp_dict_values: if 'UUID' in list(sp_dict_values.keys()): return sp_dict_values['UUID'] if 'serialnumber' in list(sp_dict_values.keys()): return sp_dict_values['serialnumber'] return None
def initialize_connection(self): """Checks database existence and performs first connection tests""" try: LOG.debug('Trying connection to the database {}', self.db_filename) self.conn = sql.connect(self.db_file_path, check_same_thread=False) cur = self.conn.cursor() cur.execute(str('SELECT SQLITE_VERSION()')) LOG.debug('Database connection {} was successful (SQLite ver. {})', self.db_filename, cur.fetchone()[0]) cur.row_factory = lambda cursor, row: row[0] cur.execute( str('SELECT name FROM sqlite_master WHERE type=\'table\' ' 'AND name NOT LIKE \'sqlite_%\'')) list_tables = cur.fetchall() if not list_tables: # If no tables exist create a new one self.conn.close() db_create_sqlite.create_database(self.db_file_path, self.db_filename) except sql.Error as exc: LOG.error('SQLite error {}:', exc.args[0]) raise DBSQLiteConnectionError from exc finally: if self.conn: self.conn.close()
def wrapper(*args, **kwargs): if args[0].is_mysql_database: # If database is mysql pass to next decorator return func(*args, **kwargs) conn = None try: if not args[0].is_connected: args[0].mutex.acquire() args[0].conn = sql.connect( args[0].db_file_path, isolation_level=CONN_ISOLATION_LEVEL) args[0].is_connected = True conn = args[0].conn return func(*args, **kwargs) except sql.Error as exc: LOG.error('SQLite error {}:', exc.args[0]) raise DBSQLiteConnectionError from exc finally: if conn: args[0].is_connected = False conn.close() args[0].mutex.release()
def _executemany_non_query(self, query, params, cursor=None): try: if cursor is None: cursor = self.get_cursor() cursor.executemany(query, params) except sql.Error as exc: LOG.error('SQLite error {}:', exc.args[0]) raise DBSQLiteError from exc except ValueError: LOG.error('Value {}', str(params)) LOG.error('Value type {}', type(params)) raise
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 = False try: pathitems = [part for part in G.REQUEST_PATH.split('/') if part] success = route(pathitems) except Exception as exc: import traceback LOG.error(traceback.format_exc()) kodi_ops.dlg_ok( 'AutoUpdateKodi', kodi_ops.get_local_string(30700).format('[{}] {}'.format( exc.__class__.__name__, exc))) if not success: from xbmcplugin import endOfDirectory endOfDirectory(handle=G.PLUGIN_HANDLE, succeeded=False) LOG.log_time_trace()
def _get_linux_uuid(): # pylint: disable=broad-except import subprocess uuid_value = None try: uuid_value = subprocess.check_output(['cat', '/var/lib/dbus/machine-id']).decode('utf-8') except Exception as exc: import traceback LOG.error('_get_linux_uuid first attempt returned: {}', exc) LOG.error(traceback.format_exc()) if not uuid_value: try: # Fedora linux uuid_value = subprocess.check_output(['cat', '/etc/machine-id']).decode('utf-8') except Exception as exc: LOG.error('_get_linux_uuid second attempt returned: {}', exc) return uuid_value
def _execute_non_query(self, query, params=None, cursor=None, **kwargs): """ Execute a query without returning a value :param query: sql query :param params: tuple of values :param cursor: a cursor, if None get a instance of standard cursor """ try: if cursor is None: cursor = self.get_cursor() if params is not None: cursor.execute(query, params) else: cursor.execute(query) except sql.Error as exc: LOG.error('SQLite error {}:', exc.args[0]) raise DBSQLiteError from exc except ValueError: LOG.error('Value {}', str(params)) LOG.error('Value type {}', type(params)) raise