Skip to content
Christian Jungerius edited this page Oct 6, 2022 · 21 revisions

Before you start

Here we go through how to create a custom launcher plugin for AKL. We assume you already know all the basics about how to code with python, how to create kodi addons and how to work with git. On this page we only go in to the specifics about the needed code, so if not prepared, start here.

Code examples

A good example is the default plugin for AKL. Get the code here. We will refer to this codebase.

The launcher base class: LauncherABC

Although it is completely possible to code everything yourself the way you like, there is some code in module package that can make it easier. You will need to create your own implementation of the LauncherABC class which you can import from akl.launchers from the script.akl.module package.

When extending this class you will need to implement the several abstract methods. Some are for general information, some for configuring the launcher for a specific case (ROM or collections), some for the actual launching.

Constructor / Initialization

When you create a new instance of the launcher you will need to pass through some arguments to the base class.

def __init__(self,
  launcher_id: str,     # unique id for this instance provided by AKL (empty if new)
  romcollection_id: str,# unique id of the collection being used
  rom_id: str,          # unique id of the ROM being launched or configured
  webservice_host:str,  # host url/ip of the AKL webserve. Will be passed through from AKL when the addon is called
  webservice_port:int,  # host port of the AKL webserve. Will be passed through from AKL when the addon is called
  executorFactory: ExecutorFactoryABC = None,   # Factory class for creating executors  
  execution_settings: ExecutionSettings = None  # Settings for the executor instance

Most of the arguments you can pass through from the sys arguments provided when the addon is called through AKL, see here. But the executor factory and settings should be created by you beforehand. The executorfactory creates Executor instances based on your OS and preferences. The Executor does all the heavy duty work of actually calling external apps or scripts. You can get one through the get_executor_factory function you can import from akl.launchers from the module. Check the code for more details.

Depending on the call done by AKL it either needs to configure the launcher or actually launch. So you default.py file should react on that.

configure cmd
Call the .build() method on the launcher class and if succesfull you can store the settings in AKL.

if launcher.build():
  launcher.store_settings()

launch cmd
Call the launch method on the launcher class. Don't forget to get the executor factory and settings.

executor_factory = get_executor_factory(report_path)
launcher = AppLauncher(
  args.akl_addon_id, 
  args.romcollection_id, 
  args.rom_id, 
  args.server_host, 
  args.server_port,
  executor_factory, 
  execution_settings)

launcher.launch()

General information

These methods will probably be deprecated soon enough and will get the data from the settings file etc.
get_name() : A simple method to give back the 'friendly' name of this addon.
get_launcher_addon_id() : This will return the addon id.

Configuration

These methods are involved in configuring the addon or plugin for a specific ROM or collection. The resulting configuration will be stored in the AKL database for that case and re-used when that specific case is called again.

_builder_get_wizard()
This method is called by the base class when a launcher is being configured. It will expect a wizard, a linked or stacked collection of kodi dialogs that will follow eachother up and guide the user in configuring the launcher usecase. The dialogs are linked through the decorator pattern. The result of going through the wizard is a dictionary of settings that is stored in AKL. Each wizard dialog is associated with a key-value pair of that dictionary. If you need to do more than just simply setting the value for a key there is also support for calling helper methods when a dialog shows or leaves.
The akl module supports many different types of wizard dialogs. Take your pick and link them together.

Here is an example of the default plugin where the users must setup a path to the application and add arguments.

def _builder_get_wizard(self, wizard):    
    wizard = kodi.WizardDialog_FileBrowse(wizard, 'application', 'Select the launcher application', 1, self._builder_get_appbrowser_filter)
    wizard = kodi.WizardDialog_Dummy(wizard, 'args', '', self._builder_get_arguments_from_application_path)
    wizard = kodi.WizardDialog_Keyboard(wizard, 'args', 'Application arguments')

    return wizard

_build_pre_wizard_hook() and _build_post_wizard_hook()
These methods are pre- and post hooks for when the wizard is executed. So use the pre method to preconfigure any settings you will need during the wizard steps. Use the post method to adjust or do more stuff after the user has provided the settings.

_builder_get_edit_option()
While we use the wizard dialogs for configuring a new instance of the launcher, in the case of an existing one which we like to edit we use a simple select list dialog for each of the configurable settings. The _builder_get_edit_options method is called and expects a dictionary containing delegates (function calls) as keys and the descriptive names as values.
The base class will present the user a select list containing the descriptive namesand once one is selected will execute the associated delegate, calling the method. You can create your own methods that deal with actual editting of the setting.

Example from default plugin:

def _builder_get_edit_options(self):
  options = collections.OrderedDict()
  options[self._change_application]       = 'Change application ({})'.format(self.launcher_settings['application'])
  options[self._change_launcher_arguments]= "Modify Arguments: '{0}'".format(self.launcher_settings['args'])
  return options

The edit method itself needs to apply the change by changing the value in the launcher_settings dictionary in the launcher class. When the users closes the edit dialog AKL will ask if all the made changes needs to be saved. At that point AKL we retrieve the launcher_settings dictionary with new values and actually store it in the database.

def _change_launcher_arguments(self):
  args = self.launcher_settings['args']
  args = kodi.dialog_keyboard('Edit application arguments', text=args)
  if args is None: return
  self.launcher_settings['args'] = args

Launching

What is a launcher if it doesn't launch anything.

Executor and ExecutorFactory
As mentioned before, the base class makes use of a ExecutorFactory class to determine the actual executor that will do the dirty work. The default ExecutorFactory implementation will return the Executor class depending on a given application value which it intereprets as a file path. The application value combined with the OS it is running on will result in the Executor to be used when launching. Of course you can also implement your own ExecutorFactory to introduce a complete new Executor implementation.
The launcher class has several methods that will assist in combining all the needed data based on settings and the specific ROM subject to start the execution. The actual Executor class has the execute method being called by the launcher and expects the following arguments:

@abc.abstractmethod
def execute(self, application: str, *args, **kwargs): pass

Depending on the OS and executor implementation it will do it's magic.
We currently have the following Executors available:

  • XbmcExecutor (uses the xbmc.executebuiltin command from Kodi API)
  • LinuxExecutor (os action with subprocess for linux OS)
  • AndroidExecutor (os action tailored for Android OS) !! deprecated, use AndroidAcivityExecutor
  • AndroidActivityExecutor (kodi command tailored for Android OS)
  • OSXExecutor (os subprocess action tailored for OSX)
  • WindowsLnkFileExecutor (.lnk files on windows)
  • WindowsBatchFileExecutor (batch files on windows)
  • WindowsExecutor (default application execution on windows)
  • WebBrowserExecutor (opens URI formats with the webbrowser module from python)

The ExecutorFactory also accepts ExecutorSettings which by default are retrieved from the addon settings. With these settings you can adjust several behaviours of Kodi when the Executor does the actual execution.

class ExecutionSettings(object):
  is_non_blocking = True
  toggle_window = False
  display_launcher_notify = True
  media_state_action = 0 # id="media_state_action" default="0" values="Stop|Pause|Let Play"
  suspend_audio_engine = False
  suspend_screensaver = True
  delay_tempo = 1000

For more look into the code on github.

launcher.get_application()
If not implemented in your class it will by default simply return the 'application' value from the launcher_settings dictionary. You can implement this to set a hardcoded value or your own application path that works with the Executor. For example you can provide a default path to your emulator application.

launcher.get_arguments()
With the get_application method we deliver the application that will be started. However in the case of executing it for a specific ROM or game, we need to make sure the application knows which one to execute. Or maybe it needs some additional settings to make it work on system of the users. For this we use the get_arguments method.
The base class implementation already has full support for reading out the arguments value from the launcher settings and applying all kinds of variable substitution rules. Of course you can completely ignore that implementation and make your own if needed, or simply extend on it. The default plugin lets the user provide a arguments string and by default suggests $rom$. This is actually a placeholder to be substituted when the get_arguments() method is called. This way you can tokenize the arguments string, add placeholders and fill it in with the details that might be ROM or game depending. The actual values you can apply depends on the scanner being used to gather the ROMS or games. Most common scenarios are of course the filepath of the ROM etc.
Here is an overview of placeholders being replaced when using the base class implementation:

Placeholder Value
\$apppath$ Full path of application. e.g. c:/apps/retroarch/retroarch.exe
\$appbase$ The application file path parent/directory. e.g. c:/apps/retroarch/
\$rom$ Full path to ROM file. e.g. c:/roms/mygame.zip
\$romfile$ Same as \$rom$
\$rompath$ rompath
\$rombase$ rombase
\$rombasenoext$ Path to ROM without filename extension
%rom% Same as \$rom$, legacy support
%ROM% Same as \$rom$, legacy support
\$romID$ Unique database id from AKL for this ROM
\$romtitle$ Name/title as stored in AKL

Aside from these hardcoded placeholders it will also go through some of the data dictionaries like the launcher settings, the ROM instance and the ROM scanned data, looking for place holders in the format of $key$ and replace those with the corresponding value.
So \$m_year$ will be substituted with the release year set in the metadata of the ROM.

application.launch()
The actual launch method in the launch class. It will call the get_application and the get_arguments methods and then get the correct executor to execute with these parameters. You can complete ignore that default implementation and directly do the action you want and skipping all the previously mentioned actions, making all the code from above obsolete.
So if you want to do it all by yourself simply implement this method only. However, you might want to use the pre_exec and post_exec methods from the base class so that Kodi will be left in a correct state.

# --- Execute app ---
self._launch_pre_exec(self.get_name(), self.execution_settings.toggle_window)
executor.execute(application, *args, **kwargs)
self._launch_post_exec(self.execution_settings.toggle_window)

If you can make do with just setting a correct application path/value and the arguments I would recommend to leave the launch method as it is.