Custom scanner - chrisism/plugin.program.akl GitHub Wiki
Before you start
Here we go through how to create a custom scanner 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 scanner base class: ScannerStrategyABC
Just as with the launcher you can use a base class to extend your own custom scanner from. Next to the bare minimum version you can extend from as ScannerStrategyABC there is also the RomScannerStrategy class which itself extends ScannerStrategyABC but already covers the basics of scanning file systems for certain files. So if you are building a file based scanner you might want to extend from that class instead.
In the following paragraphs we cover the methods which needs to be implemented in your child class.
Constructor / Initialization
When you create a new instance of the Scanner class you will need to pass through several parameters. Most of them are already provided through the addon call from kodi, from AKL as mentioned here. The only parameter you need to provide yourself is the progress_dialog argument. Luckily you can simply create one with progress_dialog = kodi.ProgressDialog()
and pass it through to the constructor.
def __init__(self,
scanner_id: str, # unique id for this instance provided by AKL (empty if new)
romcollection_id: str, # unique id of the collection to be scanned
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
progress_dialog: kodi.ProgressDialog): # a progress dialog
So after you have created the instance depending on the requested command send through by AKL, you need to call the correct methods to make it work.
configure cmd
Configuring can be done for new cases and for existing cases. For this you need to call the .configure() method and if succesfull store the created settings in the AKL addon.
if scanner.configure():
scanner.store_settings()
scan cmd
You create the instance and you need to call the .scan() method to actually make it scan. The results of the scan action needs to be stored in the AKL addon. If you call the .store_scanned_roms() method it will do that for you.
scanner.scan()
progress_dialog.endProgress()
logger.debug('scan_for_roms(): Finished scanning')
amount_dead = scanner.amount_of_dead_roms()
if amount_dead > 0:
logger.info('scan_for_roms(): {} roms marked as dead'.format(amount_dead))
scanner.remove_dead_roms()
amount_scanned = scanner.amount_of_scanned_roms()
if amount_scanned == 0:
logger.info('scan_for_roms(): No roms scanned')
else:
logger.info('scan_for_roms(): {} roms scanned'.format(amount_scanned))
scanner.store_scanned_roms()
kodi.notify('ROMs scanning done')
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_scanner_addon_id() : This will return the addon id.
Configuration
These methods are involved in configuring the addon or plugin for a specific ROM collection. The resulting configuration will be stored in the AKL database for that case and re-used when that specific case is called again.
def configure(self) -> bool
This method is called when the plugin needs to configure a scanner instance for a specific ROM collection case. It needs to provide the user with options or steps to fill in all the needed settings to make the plugin work for the specific case.
It returns True if Scanner was sucesfully built and it returns False if Scanner was not built (user canceled the dialogs or some other error happened). Just like with the launcher you can use the wizard dialogs to guide the user through the process. In this method the scanner_settings dictionary, a instance variable should be set.
Scanning
The main process of the scanner is to find new items to add to your collection and to clean up dead or removed items in that same collection. This can be achieved with the following two methods.
def scan(self):
The actual scan method that collects new items for the collection. All entries scanned should be added to the scanned_roms instance variable, an array of ROMObj items. When the scan operation is completed, the collected roms should be saved.
def cleanup(self): The method to clean up dead or removed items from the collection. Just like the scan method it will find dead or removed items in your collection and add those the marked_dead_roms instance variable, which is an array of ROMObj items.
Alternative base class: RomScannerStrategy
You can implement all the scanning and cleaning in your scanner class yourself, or you can extend from the RomScannerStrategy class instead and already have a lot of work done for you. This base class will cover the methods mentioned above and instead provide different abstract methods to implement doing only a portion of the work.
def _configure_get_wizard(self, wizard) -> kodi.WizardDialog
This method is for configuring a new instance of a Scanner. The base class will provide you with the starting wizard dialog instance as the parameter. You then can extend the wizard flow by linking your own dialogs to guide the user.
For example you can ask for a directory and how to scan that directory.
wizard = kodi.WizardDialog_FileBrowse(wizard, 'rompath', 'Select the ROMs path',0, '')
wizard = kodi.WizardDialog_YesNo(wizard, 'scan_recursive','Scan recursive', 'Scan through this directory and any subdirectories?')
def _configure_get_edit_options(self) -> dict
The method which is called when the user wants to configure an already existing scanner instance. It expects a dictionary which can be used to show the user a select list of options to configure. The keys of this dictionary are delegates (function calls) which will be called if the user select one of them from the select list. The follow up on this should be implemented by you.
def _configure_pre_wizard_hook(self) and def _configure_post_wizard_hook(self)
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.
def _getCandidates(self, launcher_report: report.Reporter) -> typing.List[ROMCandidateABC]:
This method should retrieve the candidates to be processed. In the case of a file scanner this is the place the actually scan the files.
The method also receives a Reporter class which can be used the log actions. Does actions are stored in a txt file and later retrievable by the user.
def _getDeadRoms(self, candidates:typing.List[ROMCandidateABC], roms: typing.List[ROMObj]) -> typing.List[ROMObj]
This method checks a given list of found candidates against a list of existing entries in the collection. This way it can detect dead or removed entries and mark them as such.
def _processFoundItems(self, candidates:typing.List[ROMCandidateABC], roms:typing.List[ROMObj], launcher_report: report.Reporter) -> typing.List[ROMObj] When all the candidates are collected this method will actually convert those which are new to ROMObj items which can be stored in AKL.
ROMObj, ROMCandidate
We introduce a couple of classes for refering to the ROMs we want to work with. The ROMObj is the entity you use to communicate with the AKL webserver. When you send ROMs to be stored they should be of this type. If you request the ROMs available for a certain collection you will receive a list of entities of this type. More about ROMObj in the page about the API.
However, actual ROMs are different from potential ROMs that we found on disk for example. These are the ROM candidates which have the type ROMCandidate. But what an actual candidate may look like is all up to you. The ROMCandidateABC class only needs the following two methods implemented.
get_ROM() -> ROMObj: A method that converts the candidate into a ROMObj te be send to AKL.
get_sort_value() -> str: A value to sort a collection of candidates on.
You can introduce your own candidate classes that deal with the type of scanning you are doing. If it is file disk scanning they might be references to files, if it is some online scanning then it might be JSON objects. It all depends on your case. If you want to keep some of your specific scan metadata in the AKL database so it can be used later on, then you want to create a dictionary variable and call the ROMObj.set_scanned_data(..) method to store that along with the ROM itself.
An example of the ROMFileCandidate:
class ROMFileCandidate(ROMCandidateABC):
def __init__(self, file: io.FileName):
self.file = file
super(ROMFileCandidate, self).__init__()
def get_ROM(self) -> api.ROMObj:
rom = api.ROMObj()
scanned_data = {
'file': self.file.getPath(),
'identifier': self.file.getBaseNoExt()
}
rom.set_name(self.file.getBaseNoExt())
rom.set_scanned_data(scanned_data)
return rom
def get_sort_value(self):
return self.file.getBase()