From 51b61eab0ac867edbf6b687630b59c91cb20d39d Mon Sep 17 00:00:00 2001 From: chrisism Date: Wed, 7 Jun 2023 19:19:57 +0200 Subject: [PATCH 01/71] Apply default launcher with standalone rom --- resources/lib/commands/api_commands.py | 11 +-- resources/lib/commands/rom_commands.py | 74 +++++++++++-------- .../lib/commands/romcollection_commands.py | 22 ++++-- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/resources/lib/commands/api_commands.py b/resources/lib/commands/api_commands.py index 2b6900c1..0383bc83 100644 --- a/resources/lib/commands/api_commands.py +++ b/resources/lib/commands/api_commands.py @@ -84,14 +84,15 @@ def cmd_set_launcher_args(args) -> bool: kodi.notify(f'Configured launcher {addon.get_name()}') return True + # ------------------------------------------------------------------------------------------------- # ROMCollection scanner API commands # ------------------------------------------------------------------------------------------------- def cmd_set_scanner_settings(args) -> bool: - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - scanner_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - addon_id:str = args['addon_id'] if 'addon_id' in args else None - settings:dict = args['settings'] if 'settings' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + scanner_id: str = args['akl_addon_id'] if 'akl_addon_id' in args else None + addon_id: str = args['addon_id'] if 'addon_id' in args else None + settings: dict = args['settings'] if 'settings' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -103,7 +104,7 @@ def cmd_set_scanner_settings(args) -> bool: if scanner_id is None: romcollection.add_scanner(addon, settings) - else: + else: scanner = romcollection.get_scanner(scanner_id) scanner.set_settings(settings) diff --git a/resources/lib/commands/rom_commands.py b/resources/lib/commands/rom_commands.py index c368f379..8cf3718d 100644 --- a/resources/lib/commands/rom_commands.py +++ b/resources/lib/commands/rom_commands.py @@ -25,7 +25,7 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals, editors -from resources.lib.repositories import CategoryRepository, ROMsRepository, ROMCollectionRepository, UnitOfWork +from resources.lib.repositories import CategoryRepository, ROMsRepository, ROMCollectionRepository, AelAddonRepository, UnitOfWork from resources.lib.domain import g_assetFactory, ROM logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ @AppMediator.register('EDIT_ROM') def cmd_edit_rom(args): logger.debug('EDIT_ROM: cmd_edit_rom() BEGIN') - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None if rom_id is None: logger.warning('cmd_edit_rom(): No ROM id supplied.') @@ -76,7 +76,7 @@ def cmd_edit_rom(args): # --- Submenu commands --- @AppMediator.register('ROM_EDIT_METADATA') def cmd_rom_metadata(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None selected_option = None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -84,30 +84,30 @@ def cmd_rom_metadata(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - plot_str = text.limit_string(rom.get_plot(), constants.PLOT_STR_MAXSIZE) - rating = rom.get_rating() if rom.get_rating() != -1 else 'not rated' - NFO_FileName = rom.get_nfo_file() + plot_str = text.limit_string(rom.get_plot(), constants.PLOT_STR_MAXSIZE) + rating = rom.get_rating() if rom.get_rating() != -1 else 'not rated' + NFO_FileName = rom.get_nfo_file() NFO_found_str = 'NFO found' if NFO_FileName and NFO_FileName.exists() else 'NFO not found' options = collections.OrderedDict() - options['ROM_EDIT_METADATA_TITLE'] = f"{kodi.translate(40863)}: '{rom.get_name()}'" - options['ROM_EDIT_METADATA_PLATFORM'] = f"{kodi.translate(40864)}: {rom.get_platform()}" + options['ROM_EDIT_METADATA_TITLE'] = f"{kodi.translate(40863)}: '{rom.get_name()}'" + options['ROM_EDIT_METADATA_PLATFORM'] = f"{kodi.translate(40864)}: {rom.get_platform()}" options['ROM_EDIT_METADATA_RELEASEYEAR'] = f"{kodi.translate(40865)}: {rom.get_releaseyear()}" - options['ROM_EDIT_METADATA_GENRE'] = "Edit Genre: '{}'".format(rom.get_genre()) - options['ROM_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(rom.get_developer()) - options['ROM_EDIT_METADATA_NPLAYERS'] = "Edit NPlayers: '{}'".format(rom.get_number_of_players()) - options['ROM_EDIT_METADATA_NPLAYERS_ONL']= "Edit NPlayers online: '{}'".format(rom.get_number_of_players_online()) - options['ROM_EDIT_METADATA_ESRB'] = "Edit ESRB rating: '{}'".format(rom.get_esrb_rating()) - options['ROM_EDIT_METADATA_PEGI'] = "Edit PEGI rating: '{}'".format(rom.get_pegi_rating()) - options['ROM_EDIT_METADATA_RATING'] = "Edit Rating: '{}'".format(rating) - options['ROM_EDIT_METADATA_PLOT'] = "Edit Plot: '{}'".format(plot_str) - options['ROM_EDIT_METADATA_TAGS'] = "Edit Tags" - options['ROM_EDIT_METADATA_BOXSIZE'] = "Edit Box Size: '{}'".format(rom.get_box_sizing()) - options['ROM_LOAD_PLOT'] = "Load Plot from TXT file ..." - options['ROM_IMPORT_NFO_FILE_DEFAULT'] = 'Import NFO file (default {})'.format(NFO_found_str) - options['ROM_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' - options['ROM_SAVE_NFO_FILE_DEFAULT'] = 'Save NFO file (default location)' - options['SCRAPE_ROM_METADATA'] = 'Scrape Metadata' + options['ROM_EDIT_METADATA_GENRE'] = "Edit Genre: '{}'".format(rom.get_genre()) + options['ROM_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(rom.get_developer()) + options['ROM_EDIT_METADATA_NPLAYERS'] = "Edit NPlayers: '{}'".format(rom.get_number_of_players()) + options['ROM_EDIT_METADATA_NPLAYERS_ONL'] = "Edit NPlayers online: '{}'".format(rom.get_number_of_players_online()) + options['ROM_EDIT_METADATA_ESRB'] = "Edit ESRB rating: '{}'".format(rom.get_esrb_rating()) + options['ROM_EDIT_METADATA_PEGI'] = "Edit PEGI rating: '{}'".format(rom.get_pegi_rating()) + options['ROM_EDIT_METADATA_RATING'] = "Edit Rating: '{}'".format(rating) + options['ROM_EDIT_METADATA_PLOT'] = "Edit Plot: '{}'".format(plot_str) + options['ROM_EDIT_METADATA_TAGS'] = "Edit Tags" + options['ROM_EDIT_METADATA_BOXSIZE'] = "Edit Box Size: '{}'".format(rom.get_box_sizing()) + options['ROM_LOAD_PLOT'] = "Load Plot from TXT file ..." + options['ROM_IMPORT_NFO_FILE_DEFAULT'] = 'Import NFO file (default {})'.format(NFO_found_str) + options['ROM_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' + options['ROM_SAVE_NFO_FILE_DEFAULT'] = 'Save NFO file (default location)' + options['SCRAPE_ROM_METADATA'] = 'Scrape Metadata' s = 'Edit ROM "{0}" metadata'.format(rom.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) @@ -675,7 +675,7 @@ def cmd_manage_rom_tags(args): options = collections.OrderedDict() options['ADD_TAG'] = "[Add tag]" if available_tags is not None and len(available_tags) > 0: - options.update({value:key for key, value in available_tags.items()}) + options.update({value: key for key, value in available_tags.items()}) selected_option = 'ADD_TAG' did_tag_change = False @@ -720,7 +720,7 @@ def cmd_add_rom(args): category_repository = CategoryRepository(uow) roms_repository = ROMsRepository(uow) - parent_category = category_repository.find_category(parent_id) if parent_id is not None else None + parent_category = category_repository.find_category(parent_id) if parent_id is not None else None grand_parent_category = category_repository.find_category(grand_parent_id) if grand_parent_id is not None else None if grand_parent_category is not None: @@ -731,22 +731,37 @@ def cmd_add_rom(args): rom_name = "" is_file_based = kodi.dialog_yesno(kodi.translate(40952)) - file_path = None + file_path = path = None if is_file_based: file_path = kodi.dialog_get_file(kodi.translate(40953)) if file_path is not None: path = io.FileName(file_path) rom_name = path.getBaseNoExt() - + rom_name = kodi.dialog_keyboard("Name", rom_name) if rom_name is None: return + dialog = kodi.ListDialog() + selected_idx = dialog.select('Select the platform', platforms.AKL_platform_list) + platform = platforms.AKL_platform_list[selected_idx] + rom_obj = ROM() rom_obj.set_name(rom_name) + rom_obj.set_platform(platform) if file_path: rom_obj.set_scanned_data_element("file", file_path) - + if is_file_based: + addon_repository = AelAddonRepository(uow) + addon_id = "script.akl.defaults" + addon = addon_repository.find_by_addon_id(addon_id, constants.AddonType.LAUNCHER) + rom_obj.add_launcher(addon, { + "addon_id": addon_id, + "application": file_path, + "args": "", + "secname": path.getBase() + }) + roms_repository.insert_rom(rom_obj) category_repository.add_rom_to_category(parent_category.get_id(), rom_obj.get_id()) uow.commit() @@ -754,5 +769,4 @@ def cmd_add_rom(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_TITLE_ID}) kodi.notify(f"Created new standalone ROM '{rom_name}'") - kodi.refresh_container() - \ No newline at end of file + kodi.refresh_container() \ No newline at end of file diff --git a/resources/lib/commands/romcollection_commands.py b/resources/lib/commands/romcollection_commands.py index 472a366e..453132ee 100644 --- a/resources/lib/commands/romcollection_commands.py +++ b/resources/lib/commands/romcollection_commands.py @@ -38,25 +38,31 @@ def cmd_add_collection(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = CategoryRepository(uow) - parent_category = repository.find_category(parent_id) if parent_id is not None else None - grand_parent_category = repository.find_category(grand_parent_id) if grand_parent_id is not None else None + repository = CategoryRepository(uow) + parent_category = repository.find_category(parent_id) if parent_id is not None else None + grand_parent_category = repository.find_category(grand_parent_id) if grand_parent_id is not None else None if grand_parent_category is not None: options_dialog = kodi.ListDialog() - selected_option = options_dialog.select('Add ROM collection in?',[parent_category.get_name(), grand_parent_category.get_name()]) - if selected_option is None: return - if selected_option > 0: parent_category = grand_parent_category + selected_option = options_dialog.select('Add ROM collection in?', [ + parent_category.get_name(), + grand_parent_category.get_name() + ]) + if selected_option is None: + return + if selected_option > 0: + parent_category = grand_parent_category wizard = kodi.WizardDialog_Selection(None, 'platform', 'Select the platform', platforms.AKL_platform_list) wizard = kodi.WizardDialog_Dummy(wizard, 'm_name', '', _get_name_from_platform) wizard = kodi.WizardDialog_Keyboard(wizard, 'm_name', 'Set the title of the launcher') - wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', 'Select asset/artwork directory',0, '') + wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', 'Select asset/artwork directory', 0, '') romcollection = ROMCollection() entity_data = romcollection.get_data_dic() entity_data = wizard.runWizard(entity_data) - if entity_data is None: return + if entity_data is None: + return romcollection.import_data_dic(entity_data) From 792ddefe762979b25f851bb0971af4182e13a8dc Mon Sep 17 00:00:00 2001 From: Christian Jungerius Date: Sat, 17 Jun 2023 13:19:08 +0200 Subject: [PATCH 02/71] Feature/extra field (#31) * Added extra field for undocumented values --- resources/lib/domain.py | 25 +++++++++- resources/lib/queries.py | 4 +- resources/lib/repositories.py | 7 +++ resources/migrations/1.5.0_001.sql | 79 ++++++++++++++++++++++++++++++ resources/schema.sql | 4 ++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 resources/migrations/1.5.0_001.sql diff --git a/resources/lib/domain.py b/resources/lib/domain.py index 965007e0..85ae7fbd 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -88,6 +88,11 @@ class EntityABC(object): def __init__(self, entity_data: typing.Dict[str, typing.Any]): self.entity_data = entity_data + + if not "extra" in self.entity_data or not self.entity_data["extra"]: + self.entity_data["extra"] = {} + elif isinstance(self.entity_data["extra"], str): + self.entity_data["extra"] = json.loads(self.entity_data["extra"]) # --- Database ID and utilities --------------------------------------------------------------- def set_id(self, id: str): @@ -610,8 +615,7 @@ def __init__(self, entity_data: typing.Dict[str, typing.Any], assets: typing.List[Asset], asset_paths_data: typing.List[AssetPath] = None, - asset_mappings: typing.List[AssetMapping] = []): - + asset_mappings: typing.List[AssetMapping] = []): self.assets: typing.Dict[str, Asset] = {} if assets is not None: for asset in assets: @@ -684,6 +688,18 @@ def get_plot(self): def set_plot(self, plot): self.entity_data['m_plot'] = plot + def get_extras(self): + return self.entity_data["extra"] + + def set_extras(self, extras: dict): + self.entity_data["extra"] = extras + + def set_extra_data(self, key, value): + self.entity_data["extra"][key] = value + + def get_extra_data(self, key): + return self.entity_data["extra"][key] + # # Used when rendering Categories/Launchers/ROMs # @@ -1819,6 +1835,11 @@ def update_with(self, if constants.META_TAGS_ID in metadata_to_update and api_rom_obj.get_tags() is not None: for tag in api_rom_obj.get_tags(): self.add_tag(tag) + + extra_data: dict = api_rom_obj.get_custom_attribute("extra") + if extra_data: + for key, value in extra_data.items(): + self.set_extra_data(key, value) if len(assets_to_update) > 0: for asset_id in assets_to_update: diff --git a/resources/lib/queries.py b/resources/lib/queries.py index b1ad535b..5dbcc620 100644 --- a/resources/lib/queries.py +++ b/resources/lib/queries.py @@ -4,10 +4,10 @@ AKL_SELECT_MIGRATIONS = "SELECT * FROM akl_migrations" # Shared Queries -INSERT_METADATA = "INSERT INTO metadata (id,year,genre,developer,rating,plot,assets_path,finished) VALUES (?,?,?,?,?,?,?,?)" +INSERT_METADATA = "INSERT INTO metadata (id,year,genre,developer,rating,plot,extra,assets_path,finished) VALUES (?,?,?,?,?,?,?,?,?)" INSERT_ASSET = "INSERT INTO assets (id, filepath, asset_type) VALUES (?,?,?)" INSERT_ASSET_PATH = "INSERT INTO assetpaths (id, path, asset_type) VALUES (?,?,?)" -UPDATE_METADATA = "UPDATE metadata SET year=?, genre=?, developer=?, rating=?, plot=?, assets_path=?, finished=? WHERE id=?" +UPDATE_METADATA = "UPDATE metadata SET year=?, genre=?, developer=?, rating=?, plot=?, extra=?, assets_path=?, finished=? WHERE id=?" UPDATE_ASSET = "UPDATE assets SET filepath = ?, asset_type = ? WHERE id = ?" UPDATE_ASSET_PATH = "UPDATE assetpaths SET path = ?, asset_type = ? WHERE id = ?" diff --git a/resources/lib/repositories.py b/resources/lib/repositories.py index 2e4b46e9..e56e4919 100644 --- a/resources/lib/repositories.py +++ b/resources/lib/repositories.py @@ -2,6 +2,7 @@ import logging import typing +import json import datetime from distutils.version import LooseVersion @@ -638,6 +639,7 @@ def insert_category(self, category_obj: Category, parent_obj: Category = None): category_obj.get_developer(), category_obj.get_rating(), category_obj.get_plot(), + json.dumps(category_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, category_obj.is_finished()) @@ -664,6 +666,7 @@ def update_category(self, category_obj: Category): category_obj.get_developer(), category_obj.get_rating(), category_obj.get_plot(), + json.dumps(category_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, category_obj.is_finished(), category_obj.get_custom_attribute('metadata_id')) @@ -950,6 +953,7 @@ def insert_romcollection(self, romcollection_obj: ROMCollection, parent_obj: Cat romcollection_obj.get_developer(), romcollection_obj.get_rating(), romcollection_obj.get_plot(), + json.dumps(romcollection_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, romcollection_obj.is_finished()) @@ -1004,6 +1008,7 @@ def update_romcollection(self, romcollection_obj: ROMCollection): romcollection_obj.get_developer(), romcollection_obj.get_rating(), romcollection_obj.get_plot(), + json.dumps(romcollection_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, romcollection_obj.is_finished(), romcollection_obj.get_custom_attribute('metadata_id')) @@ -1365,6 +1370,7 @@ def insert_rom(self, rom_obj: ROM): rom_obj.get_developer(), rom_obj.get_rating(), rom_obj.get_plot(), + json.dumps(rom_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, rom_obj.is_finished()) @@ -1410,6 +1416,7 @@ def update_rom(self, rom_obj: ROM): rom_obj.get_developer(), rom_obj.get_rating(), rom_obj.get_plot(), + json.dumps(rom_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, rom_obj.is_finished(), rom_obj.get_custom_attribute('metadata_id')) diff --git a/resources/migrations/1.5.0_001.sql b/resources/migrations/1.5.0_001.sql new file mode 100644 index 00000000..387126bb --- /dev/null +++ b/resources/migrations/1.5.0_001.sql @@ -0,0 +1,79 @@ +DROP VIEW vw_roms; +DROP VIEW vw_romcollections; +DROP VIEW vw_categories; + +ALTER TABLE metadata + ADD extra TEXT; + +CREATE VIEW IF NOT EXISTS vw_categories AS SELECT + c.id AS id, + c.parent_id AS parent_id, + c.metadata_id, + c.name AS m_name, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished AS finished, + m.assets_path AS assets_path, + (SELECT COUNT(*) FROM categories AS sc WHERE sc.parent_id = c.id) AS num_categories, + (SELECT COUNT(*) FROM romcollections AS sr WHERE sr.parent_id = c.id) AS num_collections +FROM categories AS c + INNER JOIN metadata AS m ON c.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT + r.id AS id, + r.parent_id AS parent_id, + r.metadata_id, + r.name AS m_name, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished AS finished, + m.assets_path AS assets_path, + r.platform AS platform, + r.box_size AS box_size, + (SELECT COUNT(*) FROM roms AS rms INNER JOIN roms_in_romcollection AS rrs ON rms.id = rrs.rom_id AND rrs.romcollection_id = r.id) as num_roms +FROM romcollections AS r + INNER JOIN metadata AS m ON r.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_roms AS SELECT + r.id AS id, + r.metadata_id, + r.name AS m_name, + r.num_of_players AS m_nplayers, + r.num_of_players_online AS m_nplayers_online, + r.esrb_rating AS m_esrb, + r.pegi_rating AS m_pegi, + r.nointro_status AS nointro_status, + r.pclone_status AS pclone_status, + r.cloneof AS cloneof, + r.platform AS platform, + r.box_size AS box_size, + r.scanned_by_id AS scanned_by_id, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished, + r.rom_status, + r.is_favourite, + r.launch_count, + r.last_launch_timestamp, + m.assets_path AS assets_path, + ( + SELECT group_concat(t.tag) AS rom_tags + FROM tags AS t + INNER JOIN metatags AS mt ON t.id = mt.tag_id + WHERE mt.metadata_id = r.metadata_id + GROUP BY mt.metadata_id + ) AS rom_tags +FROM roms AS r + INNER JOIN metadata AS m ON r.metadata_id = m.id; diff --git a/resources/schema.sql b/resources/schema.sql index 1f734129..eca71222 100644 --- a/resources/schema.sql +++ b/resources/schema.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS metadata( developer TEXT, rating INTEGER NULL, plot TEXT, + extra TEXT, assets_path TEXT, finished INTEGER DEFAULT 0 ); @@ -236,6 +237,7 @@ CREATE VIEW IF NOT EXISTS vw_categories AS SELECT m.developer AS m_developer, m.rating AS m_rating, m.plot AS m_plot, + m.extra AS extra, m.finished AS finished, m.assets_path AS assets_path, (SELECT COUNT(*) FROM categories AS sc WHERE sc.parent_id = c.id) AS num_categories, @@ -253,6 +255,7 @@ CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT m.developer AS m_developer, m.rating AS m_rating, m.plot AS m_plot, + m.extra AS extra, m.finished AS finished, m.assets_path AS assets_path, r.platform AS platform, @@ -280,6 +283,7 @@ CREATE VIEW IF NOT EXISTS vw_roms AS SELECT m.developer AS m_developer, m.rating AS m_rating, m.plot AS m_plot, + m.extra AS extra, m.finished, r.rom_status, r.is_favourite, From 32a7e2d0b23064498804d4ada1a107396a280071 Mon Sep 17 00:00:00 2001 From: chrisism Date: Wed, 21 Jun 2023 19:25:11 +0200 Subject: [PATCH 03/71] ignore fuse files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index abe608c4..7b43f140 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ routing.py pyrightconfig.json .env tests/assets/test_db.db -tools/ \ No newline at end of file +tools/ +.fuse_hidden000* \ No newline at end of file From a9575dd7aa72025a5eee757a37226301401cf61a Mon Sep 17 00:00:00 2001 From: chrisism Date: Wed, 7 Jun 2023 19:19:57 +0200 Subject: [PATCH 04/71] Apply default launcher with standalone rom --- resources/lib/commands/api_commands.py | 11 +-- resources/lib/commands/rom_commands.py | 74 +++++++++++-------- .../lib/commands/romcollection_commands.py | 22 ++++-- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/resources/lib/commands/api_commands.py b/resources/lib/commands/api_commands.py index 2b6900c1..0383bc83 100644 --- a/resources/lib/commands/api_commands.py +++ b/resources/lib/commands/api_commands.py @@ -84,14 +84,15 @@ def cmd_set_launcher_args(args) -> bool: kodi.notify(f'Configured launcher {addon.get_name()}') return True + # ------------------------------------------------------------------------------------------------- # ROMCollection scanner API commands # ------------------------------------------------------------------------------------------------- def cmd_set_scanner_settings(args) -> bool: - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - scanner_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - addon_id:str = args['addon_id'] if 'addon_id' in args else None - settings:dict = args['settings'] if 'settings' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + scanner_id: str = args['akl_addon_id'] if 'akl_addon_id' in args else None + addon_id: str = args['addon_id'] if 'addon_id' in args else None + settings: dict = args['settings'] if 'settings' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -103,7 +104,7 @@ def cmd_set_scanner_settings(args) -> bool: if scanner_id is None: romcollection.add_scanner(addon, settings) - else: + else: scanner = romcollection.get_scanner(scanner_id) scanner.set_settings(settings) diff --git a/resources/lib/commands/rom_commands.py b/resources/lib/commands/rom_commands.py index c368f379..8cf3718d 100644 --- a/resources/lib/commands/rom_commands.py +++ b/resources/lib/commands/rom_commands.py @@ -25,7 +25,7 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals, editors -from resources.lib.repositories import CategoryRepository, ROMsRepository, ROMCollectionRepository, UnitOfWork +from resources.lib.repositories import CategoryRepository, ROMsRepository, ROMCollectionRepository, AelAddonRepository, UnitOfWork from resources.lib.domain import g_assetFactory, ROM logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ @AppMediator.register('EDIT_ROM') def cmd_edit_rom(args): logger.debug('EDIT_ROM: cmd_edit_rom() BEGIN') - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None if rom_id is None: logger.warning('cmd_edit_rom(): No ROM id supplied.') @@ -76,7 +76,7 @@ def cmd_edit_rom(args): # --- Submenu commands --- @AppMediator.register('ROM_EDIT_METADATA') def cmd_rom_metadata(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None selected_option = None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -84,30 +84,30 @@ def cmd_rom_metadata(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - plot_str = text.limit_string(rom.get_plot(), constants.PLOT_STR_MAXSIZE) - rating = rom.get_rating() if rom.get_rating() != -1 else 'not rated' - NFO_FileName = rom.get_nfo_file() + plot_str = text.limit_string(rom.get_plot(), constants.PLOT_STR_MAXSIZE) + rating = rom.get_rating() if rom.get_rating() != -1 else 'not rated' + NFO_FileName = rom.get_nfo_file() NFO_found_str = 'NFO found' if NFO_FileName and NFO_FileName.exists() else 'NFO not found' options = collections.OrderedDict() - options['ROM_EDIT_METADATA_TITLE'] = f"{kodi.translate(40863)}: '{rom.get_name()}'" - options['ROM_EDIT_METADATA_PLATFORM'] = f"{kodi.translate(40864)}: {rom.get_platform()}" + options['ROM_EDIT_METADATA_TITLE'] = f"{kodi.translate(40863)}: '{rom.get_name()}'" + options['ROM_EDIT_METADATA_PLATFORM'] = f"{kodi.translate(40864)}: {rom.get_platform()}" options['ROM_EDIT_METADATA_RELEASEYEAR'] = f"{kodi.translate(40865)}: {rom.get_releaseyear()}" - options['ROM_EDIT_METADATA_GENRE'] = "Edit Genre: '{}'".format(rom.get_genre()) - options['ROM_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(rom.get_developer()) - options['ROM_EDIT_METADATA_NPLAYERS'] = "Edit NPlayers: '{}'".format(rom.get_number_of_players()) - options['ROM_EDIT_METADATA_NPLAYERS_ONL']= "Edit NPlayers online: '{}'".format(rom.get_number_of_players_online()) - options['ROM_EDIT_METADATA_ESRB'] = "Edit ESRB rating: '{}'".format(rom.get_esrb_rating()) - options['ROM_EDIT_METADATA_PEGI'] = "Edit PEGI rating: '{}'".format(rom.get_pegi_rating()) - options['ROM_EDIT_METADATA_RATING'] = "Edit Rating: '{}'".format(rating) - options['ROM_EDIT_METADATA_PLOT'] = "Edit Plot: '{}'".format(plot_str) - options['ROM_EDIT_METADATA_TAGS'] = "Edit Tags" - options['ROM_EDIT_METADATA_BOXSIZE'] = "Edit Box Size: '{}'".format(rom.get_box_sizing()) - options['ROM_LOAD_PLOT'] = "Load Plot from TXT file ..." - options['ROM_IMPORT_NFO_FILE_DEFAULT'] = 'Import NFO file (default {})'.format(NFO_found_str) - options['ROM_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' - options['ROM_SAVE_NFO_FILE_DEFAULT'] = 'Save NFO file (default location)' - options['SCRAPE_ROM_METADATA'] = 'Scrape Metadata' + options['ROM_EDIT_METADATA_GENRE'] = "Edit Genre: '{}'".format(rom.get_genre()) + options['ROM_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(rom.get_developer()) + options['ROM_EDIT_METADATA_NPLAYERS'] = "Edit NPlayers: '{}'".format(rom.get_number_of_players()) + options['ROM_EDIT_METADATA_NPLAYERS_ONL'] = "Edit NPlayers online: '{}'".format(rom.get_number_of_players_online()) + options['ROM_EDIT_METADATA_ESRB'] = "Edit ESRB rating: '{}'".format(rom.get_esrb_rating()) + options['ROM_EDIT_METADATA_PEGI'] = "Edit PEGI rating: '{}'".format(rom.get_pegi_rating()) + options['ROM_EDIT_METADATA_RATING'] = "Edit Rating: '{}'".format(rating) + options['ROM_EDIT_METADATA_PLOT'] = "Edit Plot: '{}'".format(plot_str) + options['ROM_EDIT_METADATA_TAGS'] = "Edit Tags" + options['ROM_EDIT_METADATA_BOXSIZE'] = "Edit Box Size: '{}'".format(rom.get_box_sizing()) + options['ROM_LOAD_PLOT'] = "Load Plot from TXT file ..." + options['ROM_IMPORT_NFO_FILE_DEFAULT'] = 'Import NFO file (default {})'.format(NFO_found_str) + options['ROM_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' + options['ROM_SAVE_NFO_FILE_DEFAULT'] = 'Save NFO file (default location)' + options['SCRAPE_ROM_METADATA'] = 'Scrape Metadata' s = 'Edit ROM "{0}" metadata'.format(rom.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) @@ -675,7 +675,7 @@ def cmd_manage_rom_tags(args): options = collections.OrderedDict() options['ADD_TAG'] = "[Add tag]" if available_tags is not None and len(available_tags) > 0: - options.update({value:key for key, value in available_tags.items()}) + options.update({value: key for key, value in available_tags.items()}) selected_option = 'ADD_TAG' did_tag_change = False @@ -720,7 +720,7 @@ def cmd_add_rom(args): category_repository = CategoryRepository(uow) roms_repository = ROMsRepository(uow) - parent_category = category_repository.find_category(parent_id) if parent_id is not None else None + parent_category = category_repository.find_category(parent_id) if parent_id is not None else None grand_parent_category = category_repository.find_category(grand_parent_id) if grand_parent_id is not None else None if grand_parent_category is not None: @@ -731,22 +731,37 @@ def cmd_add_rom(args): rom_name = "" is_file_based = kodi.dialog_yesno(kodi.translate(40952)) - file_path = None + file_path = path = None if is_file_based: file_path = kodi.dialog_get_file(kodi.translate(40953)) if file_path is not None: path = io.FileName(file_path) rom_name = path.getBaseNoExt() - + rom_name = kodi.dialog_keyboard("Name", rom_name) if rom_name is None: return + dialog = kodi.ListDialog() + selected_idx = dialog.select('Select the platform', platforms.AKL_platform_list) + platform = platforms.AKL_platform_list[selected_idx] + rom_obj = ROM() rom_obj.set_name(rom_name) + rom_obj.set_platform(platform) if file_path: rom_obj.set_scanned_data_element("file", file_path) - + if is_file_based: + addon_repository = AelAddonRepository(uow) + addon_id = "script.akl.defaults" + addon = addon_repository.find_by_addon_id(addon_id, constants.AddonType.LAUNCHER) + rom_obj.add_launcher(addon, { + "addon_id": addon_id, + "application": file_path, + "args": "", + "secname": path.getBase() + }) + roms_repository.insert_rom(rom_obj) category_repository.add_rom_to_category(parent_category.get_id(), rom_obj.get_id()) uow.commit() @@ -754,5 +769,4 @@ def cmd_add_rom(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_TITLE_ID}) kodi.notify(f"Created new standalone ROM '{rom_name}'") - kodi.refresh_container() - \ No newline at end of file + kodi.refresh_container() \ No newline at end of file diff --git a/resources/lib/commands/romcollection_commands.py b/resources/lib/commands/romcollection_commands.py index 472a366e..453132ee 100644 --- a/resources/lib/commands/romcollection_commands.py +++ b/resources/lib/commands/romcollection_commands.py @@ -38,25 +38,31 @@ def cmd_add_collection(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = CategoryRepository(uow) - parent_category = repository.find_category(parent_id) if parent_id is not None else None - grand_parent_category = repository.find_category(grand_parent_id) if grand_parent_id is not None else None + repository = CategoryRepository(uow) + parent_category = repository.find_category(parent_id) if parent_id is not None else None + grand_parent_category = repository.find_category(grand_parent_id) if grand_parent_id is not None else None if grand_parent_category is not None: options_dialog = kodi.ListDialog() - selected_option = options_dialog.select('Add ROM collection in?',[parent_category.get_name(), grand_parent_category.get_name()]) - if selected_option is None: return - if selected_option > 0: parent_category = grand_parent_category + selected_option = options_dialog.select('Add ROM collection in?', [ + parent_category.get_name(), + grand_parent_category.get_name() + ]) + if selected_option is None: + return + if selected_option > 0: + parent_category = grand_parent_category wizard = kodi.WizardDialog_Selection(None, 'platform', 'Select the platform', platforms.AKL_platform_list) wizard = kodi.WizardDialog_Dummy(wizard, 'm_name', '', _get_name_from_platform) wizard = kodi.WizardDialog_Keyboard(wizard, 'm_name', 'Set the title of the launcher') - wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', 'Select asset/artwork directory',0, '') + wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', 'Select asset/artwork directory', 0, '') romcollection = ROMCollection() entity_data = romcollection.get_data_dic() entity_data = wizard.runWizard(entity_data) - if entity_data is None: return + if entity_data is None: + return romcollection.import_data_dic(entity_data) From dfad557dbf207a8f75fc863fc62d8814b2d7b827 Mon Sep 17 00:00:00 2001 From: Christian Jungerius Date: Sat, 17 Jun 2023 13:19:08 +0200 Subject: [PATCH 05/71] Feature/extra field (#31) * Added extra field for undocumented values --- resources/lib/domain.py | 25 +++++++++- resources/lib/queries.py | 4 +- resources/lib/repositories.py | 7 +++ resources/migrations/1.5.0_001.sql | 79 ++++++++++++++++++++++++++++++ resources/schema.sql | 4 ++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 resources/migrations/1.5.0_001.sql diff --git a/resources/lib/domain.py b/resources/lib/domain.py index 965007e0..85ae7fbd 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -88,6 +88,11 @@ class EntityABC(object): def __init__(self, entity_data: typing.Dict[str, typing.Any]): self.entity_data = entity_data + + if not "extra" in self.entity_data or not self.entity_data["extra"]: + self.entity_data["extra"] = {} + elif isinstance(self.entity_data["extra"], str): + self.entity_data["extra"] = json.loads(self.entity_data["extra"]) # --- Database ID and utilities --------------------------------------------------------------- def set_id(self, id: str): @@ -610,8 +615,7 @@ def __init__(self, entity_data: typing.Dict[str, typing.Any], assets: typing.List[Asset], asset_paths_data: typing.List[AssetPath] = None, - asset_mappings: typing.List[AssetMapping] = []): - + asset_mappings: typing.List[AssetMapping] = []): self.assets: typing.Dict[str, Asset] = {} if assets is not None: for asset in assets: @@ -684,6 +688,18 @@ def get_plot(self): def set_plot(self, plot): self.entity_data['m_plot'] = plot + def get_extras(self): + return self.entity_data["extra"] + + def set_extras(self, extras: dict): + self.entity_data["extra"] = extras + + def set_extra_data(self, key, value): + self.entity_data["extra"][key] = value + + def get_extra_data(self, key): + return self.entity_data["extra"][key] + # # Used when rendering Categories/Launchers/ROMs # @@ -1819,6 +1835,11 @@ def update_with(self, if constants.META_TAGS_ID in metadata_to_update and api_rom_obj.get_tags() is not None: for tag in api_rom_obj.get_tags(): self.add_tag(tag) + + extra_data: dict = api_rom_obj.get_custom_attribute("extra") + if extra_data: + for key, value in extra_data.items(): + self.set_extra_data(key, value) if len(assets_to_update) > 0: for asset_id in assets_to_update: diff --git a/resources/lib/queries.py b/resources/lib/queries.py index 6f662056..b6bda8c8 100644 --- a/resources/lib/queries.py +++ b/resources/lib/queries.py @@ -4,10 +4,10 @@ AKL_SELECT_MIGRATIONS = "SELECT * FROM akl_migrations" # Shared Queries -INSERT_METADATA = "INSERT INTO metadata (id,year,genre,developer,rating,plot,assets_path,finished) VALUES (?,?,?,?,?,?,?,?)" +INSERT_METADATA = "INSERT INTO metadata (id,year,genre,developer,rating,plot,extra,assets_path,finished) VALUES (?,?,?,?,?,?,?,?,?)" INSERT_ASSET = "INSERT INTO assets (id, filepath, asset_type) VALUES (?,?,?)" INSERT_ASSET_PATH = "INSERT INTO assetpaths (id, path, asset_type) VALUES (?,?,?)" -UPDATE_METADATA = "UPDATE metadata SET year=?, genre=?, developer=?, rating=?, plot=?, assets_path=?, finished=? WHERE id=?" +UPDATE_METADATA = "UPDATE metadata SET year=?, genre=?, developer=?, rating=?, plot=?, extra=?, assets_path=?, finished=? WHERE id=?" UPDATE_ASSET = "UPDATE assets SET filepath = ?, asset_type = ? WHERE id = ?" UPDATE_ASSET_PATH = "UPDATE assetpaths SET path = ?, asset_type = ? WHERE id = ?" diff --git a/resources/lib/repositories.py b/resources/lib/repositories.py index 2e4b46e9..e56e4919 100644 --- a/resources/lib/repositories.py +++ b/resources/lib/repositories.py @@ -2,6 +2,7 @@ import logging import typing +import json import datetime from distutils.version import LooseVersion @@ -638,6 +639,7 @@ def insert_category(self, category_obj: Category, parent_obj: Category = None): category_obj.get_developer(), category_obj.get_rating(), category_obj.get_plot(), + json.dumps(category_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, category_obj.is_finished()) @@ -664,6 +666,7 @@ def update_category(self, category_obj: Category): category_obj.get_developer(), category_obj.get_rating(), category_obj.get_plot(), + json.dumps(category_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, category_obj.is_finished(), category_obj.get_custom_attribute('metadata_id')) @@ -950,6 +953,7 @@ def insert_romcollection(self, romcollection_obj: ROMCollection, parent_obj: Cat romcollection_obj.get_developer(), romcollection_obj.get_rating(), romcollection_obj.get_plot(), + json.dumps(romcollection_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, romcollection_obj.is_finished()) @@ -1004,6 +1008,7 @@ def update_romcollection(self, romcollection_obj: ROMCollection): romcollection_obj.get_developer(), romcollection_obj.get_rating(), romcollection_obj.get_plot(), + json.dumps(romcollection_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, romcollection_obj.is_finished(), romcollection_obj.get_custom_attribute('metadata_id')) @@ -1365,6 +1370,7 @@ def insert_rom(self, rom_obj: ROM): rom_obj.get_developer(), rom_obj.get_rating(), rom_obj.get_plot(), + json.dumps(rom_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, rom_obj.is_finished()) @@ -1410,6 +1416,7 @@ def update_rom(self, rom_obj: ROM): rom_obj.get_developer(), rom_obj.get_rating(), rom_obj.get_plot(), + json.dumps(rom_obj.get_extras()), assets_path.getPath() if assets_path is not None else None, rom_obj.is_finished(), rom_obj.get_custom_attribute('metadata_id')) diff --git a/resources/migrations/1.5.0_001.sql b/resources/migrations/1.5.0_001.sql new file mode 100644 index 00000000..387126bb --- /dev/null +++ b/resources/migrations/1.5.0_001.sql @@ -0,0 +1,79 @@ +DROP VIEW vw_roms; +DROP VIEW vw_romcollections; +DROP VIEW vw_categories; + +ALTER TABLE metadata + ADD extra TEXT; + +CREATE VIEW IF NOT EXISTS vw_categories AS SELECT + c.id AS id, + c.parent_id AS parent_id, + c.metadata_id, + c.name AS m_name, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished AS finished, + m.assets_path AS assets_path, + (SELECT COUNT(*) FROM categories AS sc WHERE sc.parent_id = c.id) AS num_categories, + (SELECT COUNT(*) FROM romcollections AS sr WHERE sr.parent_id = c.id) AS num_collections +FROM categories AS c + INNER JOIN metadata AS m ON c.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT + r.id AS id, + r.parent_id AS parent_id, + r.metadata_id, + r.name AS m_name, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished AS finished, + m.assets_path AS assets_path, + r.platform AS platform, + r.box_size AS box_size, + (SELECT COUNT(*) FROM roms AS rms INNER JOIN roms_in_romcollection AS rrs ON rms.id = rrs.rom_id AND rrs.romcollection_id = r.id) as num_roms +FROM romcollections AS r + INNER JOIN metadata AS m ON r.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_roms AS SELECT + r.id AS id, + r.metadata_id, + r.name AS m_name, + r.num_of_players AS m_nplayers, + r.num_of_players_online AS m_nplayers_online, + r.esrb_rating AS m_esrb, + r.pegi_rating AS m_pegi, + r.nointro_status AS nointro_status, + r.pclone_status AS pclone_status, + r.cloneof AS cloneof, + r.platform AS platform, + r.box_size AS box_size, + r.scanned_by_id AS scanned_by_id, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished, + r.rom_status, + r.is_favourite, + r.launch_count, + r.last_launch_timestamp, + m.assets_path AS assets_path, + ( + SELECT group_concat(t.tag) AS rom_tags + FROM tags AS t + INNER JOIN metatags AS mt ON t.id = mt.tag_id + WHERE mt.metadata_id = r.metadata_id + GROUP BY mt.metadata_id + ) AS rom_tags +FROM roms AS r + INNER JOIN metadata AS m ON r.metadata_id = m.id; diff --git a/resources/schema.sql b/resources/schema.sql index 1f734129..eca71222 100644 --- a/resources/schema.sql +++ b/resources/schema.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS metadata( developer TEXT, rating INTEGER NULL, plot TEXT, + extra TEXT, assets_path TEXT, finished INTEGER DEFAULT 0 ); @@ -236,6 +237,7 @@ CREATE VIEW IF NOT EXISTS vw_categories AS SELECT m.developer AS m_developer, m.rating AS m_rating, m.plot AS m_plot, + m.extra AS extra, m.finished AS finished, m.assets_path AS assets_path, (SELECT COUNT(*) FROM categories AS sc WHERE sc.parent_id = c.id) AS num_categories, @@ -253,6 +255,7 @@ CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT m.developer AS m_developer, m.rating AS m_rating, m.plot AS m_plot, + m.extra AS extra, m.finished AS finished, m.assets_path AS assets_path, r.platform AS platform, @@ -280,6 +283,7 @@ CREATE VIEW IF NOT EXISTS vw_roms AS SELECT m.developer AS m_developer, m.rating AS m_rating, m.plot AS m_plot, + m.extra AS extra, m.finished, r.rom_status, r.is_favourite, From d829f09ac129595ef3e0dbc1d96898b9966a71f2 Mon Sep 17 00:00:00 2001 From: chrisism Date: Wed, 21 Jun 2023 19:25:11 +0200 Subject: [PATCH 06/71] ignore fuse files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index abe608c4..7b43f140 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ routing.py pyrightconfig.json .env tests/assets/test_db.db -tools/ \ No newline at end of file +tools/ +.fuse_hidden000* \ No newline at end of file From ae1dbcc3ad774ff17f1562be593c1173d3006827 Mon Sep 17 00:00:00 2001 From: chrisism Date: Tue, 4 Jul 2023 16:33:45 +0200 Subject: [PATCH 07/71] Updated imports --- addon.xml | 2 +- changelog.md | 1 + requirements.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index 699b6b82..d6b6dcf2 100644 --- a/addon.xml +++ b/addon.xml @@ -3,7 +3,7 @@ - + executable game diff --git a/changelog.md b/changelog.md index f029590a..9aa220fc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,5 @@ ## Current +- Added extra field to metadata item - Search term mode applicable for multi ROM scraping - Refactoring of default asset mapping - Added setting to disable view rendering notifications diff --git a/requirements.txt b/requirements.txt index 87b39290..bb0665ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ kodi-addon-checker==0.0.26 Kodistubs==19.0.3 routing==0.2.3 pytest==6.2.5 -script.module.akl==1.0.11 +script.module.akl==1.0.12 requests==2.22.0 flake8==5.0.4 From 42f5b4141ebd11dfac8b5d65d9a4f9a870b00348 Mon Sep 17 00:00:00 2001 From: Christian Jungerius Date: Sun, 30 Jul 2023 13:39:02 +0200 Subject: [PATCH 08/71] Feature/translate texts (#33) * Added translations * translated all notifications * Translated all question dialogs * Translated all dialog titles * Translated all list options * Translated other texts * Fix for tests --- addon.py | 2 +- .../resource.language.en_gb/strings.po | 1698 ++++++++++++++++- resources/lib/commands/addon_commands.py | 28 +- resources/lib/commands/api_commands.py | 16 +- resources/lib/commands/category_commands.py | 90 +- resources/lib/commands/chk_commands.py | 10 +- resources/lib/commands/mediator.py | 2 +- resources/lib/commands/misc_commands.py | 40 +- resources/lib/commands/rom_commands.py | 174 +- .../lib/commands/rom_launcher_commands.py | 62 +- .../lib/commands/rom_scanner_commands.py | 30 +- .../lib/commands/rom_scraper_commands.py | 121 +- .../lib/commands/romcollection_commands.py | 110 +- .../commands/romcollection_roms_commands.py | 91 +- resources/lib/commands/search_commands.py | 33 +- resources/lib/commands/stats_commands.py | 2 +- .../lib/commands/view_rendering_commands.py | 46 +- resources/lib/domain.py | 248 +-- resources/lib/editors.py | 129 +- resources/lib/globals.py | 43 +- resources/lib/services.py | 14 +- resources/lib/viewqueries.py | 167 +- resources/lib/views.py | 16 +- resources/lib/webservice.py | 6 +- service.py | 2 +- tests/addon_commands_test.py | 4 +- 26 files changed, 2419 insertions(+), 765 deletions(-) diff --git a/addon.py b/addon.py index 41bb58bb..2275dc7c 100644 --- a/addon.py +++ b/addon.py @@ -37,4 +37,4 @@ views.run_plugin(sys.argv) except Exception as ex: logger.fatal('Exception in plugin', exc_info=ex) - kodi.notify_error("General failure") + kodi.notify_error(kodi.translate(40956)) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 4ed288ee..d8447d9b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1,6 +1,13 @@ # Kodi Media Center language file # Addon Name: Advanced Kodi Launcher # Addon id: plugin.program.AEL + +# strings 30000 thru 30999 reserved for plugins and plugin settings +# strings 31000 thru 31999 reserved for skins +# strings 32000 thru 32999 reserved for scripts +# strings 33000 thru 33999 reserved for common strings used in add-ons + + msgid "" msgstr "" @@ -204,6 +211,52 @@ msgctxt "#40613" msgid "Automatic fallback to Retroplayer" msgstr "settings.xml" +############################ +# Scraping settings +############################ +msgctxt "#20000" +msgid "Skip" +msgstr "settings.xml" + +msgctxt "#20010" +msgid "None" +msgstr "settings.xml" + +msgctxt "#20030" +msgid "Local files" +msgstr "settings.xml" + +msgctxt "#20050" +msgid "Local files + Scrapers" +msgstr "settings.xml" + +msgctxt "#20060" +msgid "Scrapers" +msgstr "settings.xml" + +############################ +# Scraping mode +############################ + +msgctxt "#20510" +msgid "Manual" +msgstr "settings.xml" + +msgctxt "#20520" +msgid "Automatic" +msgstr "settings.xml" + +############################ +# ROM Audit options +############################ +msgctxt "#20530" +msgid "Parents" +msgstr "settings.xml" + +msgctxt "#20531" +msgid "Clones" +msgstr "settings.xml" + ############################ # Setting options - Addons ############################ @@ -216,6 +269,8 @@ msgctxt "#40702" msgid "Google scraper plugin" msgstr "settings.xml" +################################################################################################################ + ############################ # Metadata labels ############################ @@ -272,6 +327,137 @@ msgctxt "#40813" msgid "Identifier" msgstr "" +msgctxt "#40814" +msgid "Tag" +msgstr "" + +msgctxt "#40815" +msgid "Name" +msgstr "" + +msgctxt "#40816" +msgid "Default box size" +msgstr "" + +############################ +# Constant Enum values +############################ + +msgctxt "#43001" +msgid "Icon" +msgstr "Assets" + +msgctxt "#43002" +msgid "Fanart" +msgstr "Assets" + +msgctxt "#43003" +msgid "Banner" +msgstr "Assets" + +msgctxt "#43004" +msgid "Poster" +msgstr "Assets" + +msgctxt "#43005" +msgid "Clearlogo" +msgstr "Assets" + +msgctxt "#43006" +msgid "Controller" +msgstr "Assets" + +msgctxt "#43007" +msgid "Trailer" +msgstr "Assets" + +msgctxt "#43008" +msgid "Title" +msgstr "Assets" + +msgctxt "#43009" +msgid "Snap" +msgstr "Assets" + +msgctxt "#43010" +msgid "Boxfront" +msgstr "Assets" + +msgctxt "#43011" +msgid "Boxback" +msgstr "Assets" + +msgctxt "#43012" +msgid "Cartridge" +msgstr "Assets" + +msgctxt "#43013" +msgid "Flyer" +msgstr "Assets" + +msgctxt "#43014" +msgid "Map" +msgstr "Assets" + +msgctxt "#43015" +msgid "Manual" +msgstr "Assets" + +msgctxt "#43016" +msgid "3D Box" +msgstr "Assets" + + +############################ +# Object names +############################ + +msgctxt "#42501" +msgid "Category" +msgstr "domain" + +msgctxt "#42502" +msgid "Virtual Category" +msgstr "domain" + +msgctxt "#42503" +msgid "ROM Collection" +msgstr "domain" + +msgctxt "#42504" +msgid "Virtual Collection" +msgstr "domain" + +msgctxt "#42505" +msgid "ROM" +msgstr "domain" + +############################ +# Advanced Enum values +############################ + +msgctxt "#30911" +msgid "ERROR" +msgstr "LOG ENUM" + +msgctxt "#30912" +msgid "WARNING" +msgstr "LOG ENUM" + +msgctxt "#30913" +msgid "INFO" +msgstr "LOG ENUM" + +msgctxt "#30914" +msgid "VERBOSE" +msgstr "LOG ENUM" + +msgctxt "#30915" +msgid "DEBUG" +msgstr "LOG ENUM" + +################################################################################################################ + ############################ # Actions ############################ @@ -325,19 +511,215 @@ msgid "Delete Category" msgstr "" msgctxt "#40863" -msgid "Edit Title" +msgid "Edit Title: '{}'" msgstr "" msgctxt "#40864" -msgid "Edit Platform" +msgid "Edit Platform: '{}'" msgstr "" msgctxt "#40865" -msgid "Edit Release Year" +msgid "Edit Release Year: '{}'" +msgstr "" + +msgctxt "#40866" +msgid "Edit Tags" +msgstr "" + +msgctxt "#40867" +msgid "Edit Genre: '{}'" +msgstr "" + +msgctxt "#40868" +msgid "Edit Developer: '{}'" +msgstr "" + +msgctxt "#40869" +msgid "Edit Rating: '{}'" +msgstr "" + +msgctxt "#40870" +msgid "Edit Plot: '{}'" +msgstr "" + +msgctxt "#40871" +msgid "Edit NPlayers: '{}'" +msgstr "" + +msgctxt "#40872" +msgid "Edit NPlayers online: '{}'" +msgstr "" + +msgctxt "#40873" +msgid "Edit PEGI Rating: '{}'" +msgstr "" + +msgctxt "#40874" +msgid "Edit ESRB Rating: '{}'" +msgstr "" + +msgctxt "#40875" +msgid "Edit Box Size: '{}'" +msgstr "" + +msgctxt "#40876" +msgid "Import NFO file (default, {})" +msgstr "" + +msgctxt "#40877" +msgid "Import NFO file (browse NFO file) ..." +msgstr "" + +msgctxt "#40878" +msgid "Save NFO file (default location)" +msgstr "" + +msgctxt "#40879" +msgid "Save NFO file (default location)" +msgstr "" + +msgctxt "#40879" +msgid "Load Plot from TXT file ..." +msgstr "" + +msgctxt "#40880" +msgid "Load Plot from TXT file ..." +msgstr "" + +msgctxt "#40881" +msgid "Scrape" +msgstr "" + +msgctxt "#40882" +msgid "View ROM" +msgstr "" + +msgctxt "#40883" +msgid "Edit ROM" +msgstr "" + +msgctxt "#40884" +msgid "Link ROM in other collection" +msgstr "" + +msgctxt "#40885" +msgid "Add ROM to AKL Favourites" +msgstr "" + +msgctxt "#40886" +msgid "View Category" +msgstr "" + +msgctxt "#40887" +msgid "Edit Category" +msgstr "" + +msgctxt "#40888" +msgid "Add new Category" +msgstr "" + +msgctxt "#40889" +msgid "Add new ROM Collection" +msgstr "" + +msgctxt "#40890" +msgid "Add new ROM (Standalone)" +msgstr "" + +msgctxt "#40891" +msgid "View ROM Collection" +msgstr "" + +msgctxt "#40892" +msgid "Edit ROM Collection" +msgstr "" + +msgctxt "#40893" +msgid "Rebuild {0} view" +msgstr "" + +msgctxt "#40894" +msgid "Search ROM in collection" +msgstr "" + +msgctxt "#40895" +msgid "Open Kodi file manager" +msgstr "" + +msgctxt "#40896" +msgid "AKL addon settings" +msgstr "" + +msgctxt "#40897" +msgid "Utilities" +msgstr "" + +msgctxt "#40898" +msgid "Global Reports" +msgstr "" + +msgctxt "#40899" +msgid "Reset database" +msgstr "" + +msgctxt "#40900" +msgid "Rebuild virtual views" +msgstr "" + +msgctxt "#40901" +msgid "Scan for plugin-addons" +msgstr "" + +msgctxt "#40902" +msgid "Show plugin-addons" +msgstr "" + +msgctxt "#40903" +msgid "Manage ROM tags" +msgstr "" + +msgctxt "#40904" +msgid "Import category/launcher XML configuration file" +msgstr "" + +msgctxt "#40905" +msgid "Export category/rom collection XML configuration file" +msgstr "" + +msgctxt "#40906" +msgid "Check collections" +msgstr "" + +msgctxt "#40907" +msgid "Check ROMs artwork image integrity" +msgstr "" + +msgctxt "#40908" +msgid "Delete ROMs redundant artwork" +msgstr "" + +msgctxt "#40909" +msgid "Show detected No-Intro/Redump DATs" +msgstr "" + +msgctxt "#40910" +msgid "Global ROM statistics" +msgstr "" + +msgctxt "#40911" +msgid "Global ROM Audit statistics (All)" +msgstr "" + +msgctxt "#40912" +msgid "Global ROM Audit statistics (No-Intro only)" +msgstr "" + +msgctxt "#40913" +msgid "Global ROM Audit statistics (Redump only)" msgstr "" ############################ -# Headers and texts / notifications +# Headers and texts / notifications / dialogs ############################ msgctxt "#40950" msgid "Select action for Category" @@ -363,72 +745,1274 @@ msgctxt "#40955" msgid "Should new platform be applied to existing ROMs in this collection?" msgstr "" -############################ -# Scraping settings -############################ -msgctxt "#20000" -msgid "Skip" -msgstr "settings.xml" +msgctxt "#40956" +msgid "General failure" +msgstr "" -msgctxt "#20010" -msgid "None" -msgstr "settings.xml" +msgctxt "#40957" +msgid "Cannot launch ROM" +msgstr "" -msgctxt "#20030" -msgid "Local files" -msgstr "settings.xml" +msgctxt "#40958" +msgid "Failed to store launchers settings" +msgstr "" -msgctxt "#20050" -msgid "Local files + Scrapers" -msgstr "settings.xml" +msgctxt "#40959" +msgid "Building initial views" +msgstr "" -msgctxt "#20060" -msgid "Scrapers" -msgstr "settings.xml" +msgctxt "#40960" +msgid "Failed to execute route or command" +msgstr "" -############################ -# Scraping mode -############################ +msgctxt "#40961" +msgid "Current view is not rendered correctly. Re-render views first." +msgstr "" -msgctxt "#20510" -msgid "Manual" -msgstr "settings.xml" +msgctxt "#40962" +msgid "Updated addon" +msgstr "" -msgctxt "#20520" -msgid "Automatic" -msgstr "settings.xml" +msgctxt "#40963" +msgid "No AKL addons found. Search and install default plugin addons for AKL?" +msgstr "" -############################ -# ROM Audit options -############################ -msgctxt "#20530" -msgid "Parents" -msgstr "settings.xml" +msgctxt "#40964" +msgid "Views rendered" +msgstr "" -msgctxt "#20531" -msgid "Clones" -msgstr "settings.xml" +msgctxt "#40965" +msgid "Virtual views rendered" +msgstr "" + +msgctxt "#40966" +msgid "Selected views rendered" +msgstr "" + +msgctxt "#40967" +msgid "Rendering views" +msgstr "" + +msgctxt "#40968" +msgid "Rendering all views" +msgstr "" + +msgctxt "#40969" +msgid "All views rendered" +msgstr "" + +msgctxt "#40970" +msgid "Rendering virtual category '{0}'" +msgstr "" + +msgctxt "#40971" +msgid "{0} view rendered" +msgstr "" + +msgctxt "#40972" +msgid "Cannot find virtual category id#{0}" +msgstr "" + +msgctxt "#40973" +msgid "Rendering virtual collection '{0}'" +msgstr "" + +msgctxt "#40974" +msgid "Rendering romcollection views" +msgstr "" + +msgctxt "#40975" +msgid "Rendering all views containing ROM#{0}" +msgstr "" + +msgctxt "#40976" +msgid "Failed to process ROM collection {0}" +msgstr "" + +msgctxt "#40977" +msgid "Cleared ROMs from collection" +msgstr "" + +msgctxt "#40978" +msgid "Finished importing ROMS" +msgstr "" + +msgctxt "#40979" +msgid "Preparing scraper" +msgstr "" + +msgctxt "#40980" +msgid "Preparing scanner" +msgstr "" + +msgctxt "#40981" +msgid "Creating new AKL database" +msgstr "" + +msgctxt "#40982" +msgid "No scanners configured for this romcollection!" +msgstr "" + +msgctxt "#40983" +msgid "{0} {1} mapped to {2}" +msgstr "" + +msgctxt "#40984" +msgid "Changed rom asset dir for {0} to {1}" +msgstr "" + +msgctxt "#40985" +msgid "Imported {0} NFO files" +msgstr "" + +msgctxt "#40986" +msgid "{0} {1} is now {2}" +msgstr "" + +msgctxt "#40987" +msgid "{0} {1} not changed" +msgstr "" + +msgctxt "#40988" +msgid "{0} Rating not changed" +msgstr "" + +msgctxt "#40989" +msgid "{0} rating is now {1}" +msgstr "" + +msgctxt "#40990" +msgid "{0} {1} has been updated" +msgstr "" + +msgctxt "#40991" +msgid "new_asset_file and dest_asset_file are the same. Returning" +msgstr "" + +msgctxt "#40992" +msgid "Failure while copying file" +msgstr "" + +msgctxt "#40993" +msgid "{0} {1} has been unset" +msgstr "" + +msgctxt "#40994" +msgid "{0} {1} has been updated" +msgstr "" + +msgctxt "#40995" +msgid "Category {0} has no items. Add romcollections or categories first." +msgstr "" + +msgctxt "#40996" +msgid "Collection {0} has no items. Add ROMs" +msgstr "" + +msgctxt "#40997" +msgid "No field specified" +msgstr "" + +msgctxt "#40998" +msgid "Scanning for AKL supported addons" +msgstr "" + +msgctxt "#40999" +msgid "Scan completed. Found {0} addons" +msgstr "" + +msgctxt "#41000" +msgid "No ROM scanners configured." +msgstr "" + +msgctxt "#41001" +msgid "No launcher configured." +msgstr "" + +msgctxt "#41002" +msgid "No launchers configured for this ROM!" +msgstr "" + +msgctxt "#41003" +msgid "No launchers configured for this collection." +msgstr "" + +msgctxt "#41004" +msgid "Removed launcher '{0}'" +msgstr "" + +msgctxt "#41005" +msgid "Configured launcher '{0}'" +msgstr "" + +msgctxt "#41006" +msgid "Configured ROM scanner '{0}'" +msgstr "" + +msgctxt "#41007" +msgid "Stored scanned ROMS in ROMs Collection '{0}'" +msgstr "" + +msgctxt "#41008" +msgid "Stored scraped ROMS in ROMs Collection '{0}'" +msgstr "" + +msgctxt "#41009" +msgid "Stored scraped ROM '{0}'" +msgstr "" + +msgctxt "#41010" +msgid "Removed ROMS from ROMs Collection '{0}'" +msgstr "" + +msgctxt "#41011" +msgid "Cleaned up redundant files" +msgstr "" + +msgctxt "#41012" +msgid "Finished importing Categories/Launchers" +msgstr "" + +msgctxt "#41013" +msgid "Category/Launcher XML exporting cancelled" +msgstr "" + +msgctxt "#41014" +msgid "Exported AKL Categories and Collections to XML configuration" +msgstr "" + +msgctxt "#41015" +msgid "Finished resetting the database" +msgstr "" + +msgctxt "#41016" +msgid "Done running migrations on the database" +msgstr "" + +msgctxt "#41017" +msgid "ROM Collection {0} created" +msgstr "" + +msgctxt "#41018" +msgid "Deleted romcollection {0}" +msgstr "" + +msgctxt "#41019" +msgid "Imported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41020" +msgid "Exported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41021" +msgid "Changed category for collection" +msgstr "" + +msgctxt "#41022" +msgid "Export of ROMCollection XML cancelled" +msgstr "" + +msgctxt "#41023" +msgid "Exported ROMCollection '{0}' XML config" +msgstr "" + +msgctxt "#41024" +msgid "Deleted ROM {0}" +msgstr "" + +msgctxt "#41025" +msgid "Changed ROM NPlayers" +msgstr "" + +msgctxt "#41026" +msgid "Changed ROM NPlayers online" +msgstr "" + +msgctxt "#41027" +msgid "Removing tag {0}" +msgstr "" + +msgctxt "#41028" +msgid "Updating ROM with removed tags" +msgstr "" + +msgctxt "#41029" +msgid "Adding tag {0}" +msgstr "" + +msgctxt "#41030" +msgid "Updating ROM with added tags" +msgstr "" + +msgctxt "#41031" +msgid "Removed all tags from ROM {0}" +msgstr "" + +msgctxt "#41032" +msgid "Imported ROM Plot" +msgstr "" + +msgctxt "#41033" +msgid "Imported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41034" +msgid "Exported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41035" +msgid "Created new standalone ROM {0}" +msgstr "" + +msgctxt "#41036" +msgid "Category {0} created" +msgstr "" + +msgctxt "#41037" +msgid "Deleted category {0}" +msgstr "" + +msgctxt "#41038" +msgid "Imported Category NFO file {0}" +msgstr "" + +msgctxt "#41039" +msgid "Exported Category NFO file {0}" +msgstr "" + +msgctxt "#41040" +msgid "Export of Category XML cancelled" +msgstr "" + +msgctxt "#41041" +msgid "Exported Category '{0}' XML config" +msgstr "" + +msgctxt "#41042" +msgid "Failure while writing NFO file {0}" +msgstr "" + +msgctxt "#41043" +msgid "Failure processing command '{0}'" +msgstr "" + +msgctxt "#41044" +msgid "Exception reading NFO file {0}" +msgstr "" + +msgctxt "#41045" +msgid "NFO file not found {0}" +msgstr "" + +msgctxt "#41046" +msgid "Imported {0}" +msgstr "" + +msgctxt "#41047" +msgid "No local assets path configured. Configure now?\n Else we will use addon default directories." +msgstr "" + +msgctxt "#41048" +msgid "Virtual category '{0}' has no items. Regenerate the views now?" +msgstr "" + +msgctxt "#41049" +msgid "Virtual collection '{0}' has no items. Regenerate the views now?" +msgstr "" + +msgctxt "#41050" +msgid "Do you want to overwrite collection metadata properties with values from the launcher?" +msgstr "" + +msgctxt "#41051" +msgid "Scan for ROMs now?" +msgstr "" + +msgctxt "#41052" +msgid "Overwrite file {0}?" +msgstr "" + +msgctxt "#41053" +msgid "Are you sure you want to reset the database?" +msgstr "" + +msgctxt "#41054" +msgid "AKL_configuration.xml found in the selected directory. Overwrite?" +msgstr "" + +msgctxt "#41055" +msgid "Run migration {0}?" +msgstr "" + +msgctxt "#41056" +msgid "Are you sure you want to delete '{0}'?\nThis will delete the ROM from all views." +msgstr "" + +msgctxt "#41057" +msgid "Remove tag '{0}'?" +msgstr "" + +msgctxt "#41058" +msgid "Clear all tags from ROM '{0}'?" +msgstr "" + +msgctxt "#41059" +msgid "Are you sure to delete launcher '{}'?" +msgstr "" + +msgctxt "#41060" +msgid "Are you sure to delete ROM scanner '{}'?" +msgstr "" + +msgctxt "#41061" +msgid "Overwrite existing assets?" +msgstr "" + +msgctxt "#41062" +msgid "Apply new path to all current asset paths?" +msgstr "" + +msgctxt "#41063" +msgid "ROM '{}' found in AKL database. Overwrite?" +msgstr "" + +msgctxt "#41064" +msgid "Delete the ROMs completely from the AKL database and not collection only?" +msgstr "" + +msgctxt "#41065" +msgid "Move '{0}' to category '{1}'?" +msgstr "" + +msgctxt "#41066" +msgid "Are you sure you want to delete '{}'?" +msgstr "" + +msgctxt "#41067" +msgid "Category '{0}' has {1} sub-categories and {2} romcollections. Deleting it will also delete related items." +msgstr "" + +msgctxt "#41068" +msgid "Category '{0}' has no categories or romcollections." +msgstr "" + +msgctxt "#41069" +msgid "ROMCollection '{0}' has {1} ROMs." +msgstr "" + +msgctxt "#41070" +msgid "File '{0}' has {1} bytes and it is very big. Are you sure this is the correct file?" +msgstr "" + +msgctxt "#41071" +msgid "Delete {0} files marked as redundant?\nWarning! This will actually delete the files!\m Backup filesnow if needed." +msgstr "" + +msgctxt "#41072" +msgid "Category '{0}' found in AKL database. Overwrite?" +msgstr "" + +msgctxt "#41073" +msgid "ROMCollection '{0}' found in AKL database. Overwrite?" +msgstr "" + +msgctxt "#41074" +msgid "Change {0} {1}" +msgstr "" + +msgctxt "#41075" +msgid "Edit {0} {1}" +msgstr "" + +msgctxt "#41076" +msgid "Edit {0} Assets/Artwork" +msgstr "" + +msgctxt "#41077" +msgid "Edit {0} default Assets/Artwork" +msgstr "" + +msgctxt "#41078" +msgid "Edit {0} {1} mapped Assets/Artwork" +msgstr "" + +msgctxt "#41079" +msgid "Select the {0} Rating" +msgstr "" + +msgctxt "#41080" +msgid "Addons" +msgstr "" + +msgctxt "#41081" +msgid "Addon: {0}" +msgstr "" + +msgctxt "#41082" +msgid "Supported metadata: {0}" +msgstr "" + +msgctxt "#41083" +msgid "Supported assets: {0}" +msgstr "" + +msgctxt "#41084" +msgid "Add category in?" +msgstr "" + +msgctxt "#41085" +msgid "New Category Name" +msgstr "" + +msgctxt "#41086" +msgid "Edit Category '{0}' metadata" +msgstr "" + +msgctxt "#41087" +msgid "Collections to process" +msgstr "" + +msgctxt "#41088" +msgid "Select migrations to execute (Current version {0})" +msgstr "" + +msgctxt "#41089" +msgid "Migration {0}" +msgstr "" + +msgctxt "#41090" +msgid "Run migration" +msgstr "" + +msgctxt "#41091" +msgid "Mark as executed without running" +msgstr "" + +msgctxt "#41092" +msgid "Edit ROM '{0}'" +msgstr "" + +msgctxt "#41093" +msgid "Edit ROM '{0}' metadata" +msgstr "" + +msgctxt "#41094" +msgid "Edit ROM NPlayers" +msgstr "" + +msgctxt "#41095" +msgid "Edit ROM NPlayers online" +msgstr "" + +msgctxt "#41096" +msgid "Tag to add" +msgstr "" + +msgctxt "#41097" +msgid "Manage tags" +msgstr "" + +msgctxt "#41098" +msgid "Add ROM in?" +msgstr "" + +msgctxt "#41099" +msgid "Select the platform" +msgstr "" + +msgctxt "#41100" +msgid "Manage Launchers for '{}'" +msgstr "" + +msgctxt "#41101" +msgid "Choose launcher to associate" +msgstr "" + +msgctxt "#41102" +msgid "Choose launcher to edit" +msgstr "" + +msgctxt "#41103" +msgid "Choose launcher to remove" +msgstr "" + +msgctxt "#41104" +msgid "Choose launcher to set as default" +msgstr "" + +msgctxt "#41105" +msgid "Choose launcher" +msgstr "" + +msgctxt "#41106" +msgid "Manage ROM scanners for {}" +msgstr "" + +msgctxt "#41107" +msgid "Choose scanner to associate" +msgstr "" + +msgctxt "#41108" +msgid "Choose scanner to edit" +msgstr "" + +msgctxt "#41109" +msgid "Choose scanner to remove" +msgstr "" + +msgctxt "#41110" +msgid "Choose ROM scanner" +msgstr "" + +msgctxt "#41111" +msgid "Scrape collection '{0}' ROMs with '{1}'" +msgstr "" + +msgctxt "#41112" +msgid "Scrape ROM '{0}' with '{1}'" +msgstr "" + +msgctxt "#41113" +msgid "Metadata to scrape" +msgstr "" + +msgctxt "#41114" +msgid "Assets to scrape" +msgstr "" + +msgctxt "#41115" +msgid "Metadata scan policy '{}'" +msgstr "" + +msgctxt "#41116" +msgid "Asset scan policy '{}'" +msgstr "" + +msgctxt "#41117" +msgid "Game search mode '{}'" +msgstr "" + +msgctxt "#41118" +msgid "Game selection mode '{}'" +msgstr "" + +msgctxt "#41119" +msgid "Asset selection mode '{}'" +msgstr "" + +msgctxt "#41120" +msgid "Scrape ROM assets" +msgstr "" + +msgctxt "#41121" +msgid "Scrape ROM {0} assets" +msgstr "" + +msgctxt "#41122" +msgid "Scrape ROM metadata" +msgstr "" + +msgctxt "#41123" +msgid "Scrape ROM {0}" +msgstr "" + +msgctxt "#41124" +msgid "Scrape collection '{0}' ROMs" +msgstr "" + +msgctxt "#41125" +msgid "Add ROM collection in?" +msgstr "" + +msgctxt "#41126" +msgid "Select action for ROM Collection {0}" +msgstr "" + +msgctxt "#41127" +msgid "Edit Launcher '{0}' metadata" +msgstr "" + +msgctxt "#41128" +msgid "Move to category" +msgstr "" + +msgctxt "#41128" +msgid "Manage ROM Collection '{}' ROMs" +msgstr "" + +msgctxt "#41129" +msgid "ROM Asset directories" +msgstr "" + +msgctxt "#41130" +msgid "Import ROMs in ROMCollection '{}'" +msgstr "" + +msgctxt "#41131" +msgid "Search ROMs..." +msgstr "" + +msgctxt "#41132" +msgid "Select a Rating..." +msgstr "" + +msgctxt "#41133" +msgid "Select a Genre..." +msgstr "" + +msgctxt "#41134" +msgid "Select a Release year..." +msgstr "" + +msgctxt "#41135" +msgid "Select a Developer..." +msgstr "" + +msgctxt "#41136" +msgid "Enter the ROM Title search string..." +msgstr "" + +msgctxt "#41137" +msgid "Edit {0} '{1}' {2}" +msgstr "" + +msgctxt "#41138" +msgid "Assets root path for entry '{0}'" +msgstr "" + +msgctxt "#41139" +msgid "Directory to store artwork not found.\nConfigure it before you can edit artwork." +msgstr "" + +msgctxt "#41140" +msgid "Unknown obj_instance.get_assets_kind() {}.\nThis is a bug, please report it." +msgstr "" + +msgctxt "#41141" +msgid "Select {0} {1}" +msgstr "" + +msgctxt "#41142" +msgid "Collection '{0}' has {1} ROMs. Are you sure you want to clear them from this collection?" +msgstr "" + +msgctxt "#41143" +msgid "Select NFO description file" +msgstr "" + +msgctxt "#41144" +msgid "Select directory to export XML" +msgstr "" + +msgctxt "#41145" +msgid "Select XML category/launcher configuration file" +msgstr "" + +msgctxt "#41146" +msgid "Category '{}' status is now {}" +msgstr "" + +msgctxt "#41147" +msgid "Duplicated asset directories: {0}.\nAKL will refuse to add/edit ROMs if there are duplicate asset directories." +msgstr "" + +msgctxt "#41148" +msgid "No NFO file available" +msgstr "" + +msgctxt "#41149" +msgid "Assets directories not set: {0}.\nAsset scanner will be disabled for this/those." +msgstr "" + +msgctxt "#41150" +msgid "ROMCollection '{}' status is now {}" +msgstr "" + +msgctxt "#41151" +msgid "Collection has no ROMs. Nothing to do." +msgstr "" + +msgctxt "#41152" +msgid "Checking ROM artwork integrity..." +msgstr "" + +msgctxt "#41153" +msgid "Processing NFO files" +msgstr "" + +msgctxt "#41154" +msgid "Saving ROM JSON database ..." +msgstr "" + +msgctxt "#41155" +msgid "Select ROMs JSON file" +msgstr "" + +msgctxt "#41156" +msgid "Unknown" +msgstr "" + +msgctxt "#41157" +msgid "Select description file (TXT|DAT)" +msgstr "" + +msgctxt "#41158" +msgid "Undefined" +msgstr "" + +msgctxt "#41159" +msgid "Select root assets path" +msgstr "" + +msgctxt "#41160" +msgid "Select {} path" +msgstr "" ############################ -# Advanced Enum values -############################ +# List/Action options +############################ -msgctxt "#30911" -msgid "ERROR" -msgstr "LOG ENUM" +msgctxt "#42001" +msgid "Not set" +msgstr "" -msgctxt "#30912" -msgid "WARNING" -msgstr "LOG ENUM" +msgctxt "#42002" +msgid "Rating {0}" +msgstr "" -msgctxt "#30913" -msgid "INFO" -msgstr "LOG ENUM" +msgctxt "#42003" +msgid "Edit {0} ..." +msgstr "" -msgctxt "#30914" -msgid "VERBOSE" -msgstr "LOG ENUM" +msgctxt "#42004" +msgid "Link to local {0} image" +msgstr "" -msgctxt "#30915" -msgid "DEBUG" -msgstr "LOG ENUM" \ No newline at end of file +msgctxt "#42005" +msgid "Import local {0} (copy and rename)" +msgstr "" + +msgctxt "#42006" +msgid "Unset artwork/asset" +msgstr "" + +msgctxt "#42007" +msgid "Scrape {0}" +msgstr "" + +msgctxt "#42008" +msgid "> Scan for new or updated addons" +msgstr "" + +msgctxt "#42009" +msgid "Refresh/update addon" +msgstr "" + +msgctxt "#42010" +msgid "Supported metadata" +msgstr "" + +msgctxt "#42011" +msgid "Supported assets" +msgstr "" + +msgctxt "#42012" +msgid "Plugin settings" +msgstr "" + +msgctxt "#42013" +msgid "ROM status: {0}" +msgstr "" + +msgctxt "#42014" +msgid "Finished" +msgstr "" + +msgctxt "#42015" +msgid "Unfinished" +msgstr "" + +msgctxt "#42016" +msgid "Manage associated launchers" +msgstr "" + +msgctxt "#42017" +msgid "Add new launcher to ROM" +msgstr "" + +msgctxt "#42018" +msgid "Delete ROM" +msgstr "" + +msgctxt "#42019" +msgid "NFO found" +msgstr "" + +msgctxt "#42020" +msgid "NFO not found" +msgstr "" + +msgctxt "#42021" +msgid "not rated" +msgstr "" + +msgctxt "#42022" +msgid "Manual entry" +msgstr "" + +msgctxt "#42023" +msgid "[Add tag]" +msgstr "" + +msgctxt "#42024" +msgid "[Clear all tags]" +msgstr "" + +msgctxt "#42025" +msgid "[Manual insert tag]" +msgstr "" + +msgctxt "#42026" +msgid "Add new launcher" +msgstr "" + +msgctxt "#42027" +msgid "Edit launcher" +msgstr "" + +msgctxt "#42028" +msgid "Remove launcher" +msgstr "" + +msgctxt "#42029" +msgid "Set default launcher: '{}'" +msgstr "" + +msgctxt "#42030" +msgid "Metadata to scrape: '{}'" +msgstr "" + +msgctxt "#42031" +msgid "Assets to scrape: '{}'" +msgstr "" + +msgctxt "#42032" +msgid "Overwrite existing metadata: '{}'" +msgstr "" + +msgctxt "#42033" +msgid "Overwrite existing assets/files: '{}'" +msgstr "" + +msgctxt "#42034" +msgid "Ignore scraped titles: '{}'" +msgstr "" + +msgctxt "#42035" +msgid "Yes" +msgstr "" + +msgctxt "#42036" +msgid "No" +msgstr "" + +msgctxt "#42037" +msgid "Set the title of the launcher" +msgstr "" + +msgctxt "#42038" +msgid "Select asset/artwork directory" +msgstr "" + +msgctxt "#42039" +msgid "Manage ROMs ..." +msgstr "" + +msgctxt "#42040" +msgid "Change Category: '{}'" +msgstr "" + +msgctxt "#42041" +msgid "ROM Collection status: '{}'" +msgstr "" + +msgctxt "#42042" +msgid "Export ROM Collection XML configuration ..." +msgstr "" + +msgctxt "#42043" +msgid "Delete ROM Collection" +msgstr "" + +msgctxt "#42044" +msgid "Choose ROMs default artwork ..." +msgstr "" + +msgctxt "#42045" +msgid "Manage ROMs asset directories ..." +msgstr "" + +msgctxt "#42046" +msgid "Scan for new ROMs" +msgstr "" + +msgctxt "#42047" +msgid "Remove dead/missing ROMs" +msgstr "" + +msgctxt "#42048" +msgid "Configure ROM scanners" +msgstr "" + +msgctxt "#42049" +msgid "Add new ROM scanner" +msgstr "" + +msgctxt "#42050" +msgid "Import ROMs (files/metadata)" +msgstr "" + +msgctxt "#42051" +msgid "Export ROMs metadata to NFO files" +msgstr "" + +msgctxt "#42052" +msgid "Scrape ROMs" +msgstr "" + +msgctxt "#42053" +msgid "Delete ROMs NFO files" +msgstr "" + +msgctxt "#42054" +msgid "Clear ROMs from ROMCollection" +msgstr "" + +msgctxt "#42055" +msgid "Choose asset for {0} (currently {1})" +msgstr "" + +msgctxt "#42056" +msgid "Import ROMs metadata from NFO files" +msgstr "" + +msgctxt "#42057" +msgid "Import ROMs data from JSON files" +msgstr "" + +msgctxt "#42058" +msgid "By ROM Title" +msgstr "" + +msgctxt "#42059" +msgid "By Release Year" +msgstr "" + +msgctxt "#42060" +msgid "By Genre" +msgstr "" + +msgctxt "#42061" +msgid "By Developer" +msgstr "" + +msgctxt "#42062" +msgid "By Rating" +msgstr "" + +msgctxt "#42063" +msgid "" +msgstr "" + +msgctxt "#42064" +msgid "[Recently played ROMs]" +msgstr "" + +msgctxt "#42065" +msgid "[Most played ROMs]" +msgstr "" + +msgctxt "#42066" +msgid "Browse by..." +msgstr "" + +msgctxt "#42067" +msgid "Browse by Title" +msgstr "" + +msgctxt "#42068" +msgid "Browse by Year" +msgstr "" + +msgctxt "#42069" +msgid "Browse by Genre" +msgstr "" + +msgctxt "#42070" +msgid "Browse by Developer" +msgstr "" + +msgctxt "#42071" +msgid "Browse by Number of Players" +msgstr "" + +msgctxt "#42072" +msgid "Browse by ESRB Rating" +msgstr "" + +msgctxt "#42073" +msgid "Browse by PEGI Rating" +msgstr "" + +msgctxt "#42074" +msgid "Browse by Rating" +msgstr "" + +msgctxt "#42080" +msgid "Add new scanner" +msgstr "" + +msgctxt "#42081" +msgid "Edit scanner" +msgstr "" + +msgctxt "#42082" +msgid "Remove scanner" +msgstr "" + +msgctxt "#42083" +msgid "Change root assets path: '{}'" +msgstr "" + +msgctxt "#42084" +msgid "Change {} path: '{}'" +msgstr "" + +############################ +# Context menu item descriptions +############################ + +msgctxt "#42001" +msgid "Execute several [COLOR orange]Utilities[/COLOR]." +msgstr "viewqueries" + +msgctxt "#42002" +msgid "Generate and view [COLOR orange]Global Reports[/COLOR]." +msgstr "viewqueries" + +msgctxt "#42003" +msgid "Reset the AKL database. You will loose all data." +msgstr "viewqueries" + +msgctxt "#42004" +msgid "Rebuild all the container views in the application." +msgstr "viewqueries" + +msgctxt "#42005" +msgid "Browse AKL Favourite ROMs" +msgstr "domain" + +msgctxt "#42006" +msgid "Browse the ROMs you played recently" +msgstr "domain" + +msgctxt "#42007" +msgid "Browse the ROMs you play most" +msgstr "domain" + +msgctxt "#42008" +msgid "Browse ROMs filtered on '{0}'" +msgstr "domain" + +msgctxt "#42009" +msgid "Browse the ROMs by specifics" +msgstr "domain" + +msgctxt "#42010" +msgid "Browse the ROMs by title" +msgstr "domain" + +msgctxt "#42011" +msgid "Browse the ROMs by year" +msgstr "domain" + +msgctxt "#42012" +msgid "Browse the ROMs by genre" +msgstr "domain" + +msgctxt "#42013" +msgid "Browse the ROMs by developer" +msgstr "domain" + +msgctxt "#42014" +msgid "Browse the ROMs by number of players" +msgstr "domain" + +msgctxt "#42015" +msgid "Browse the ROMs by ESRB rating" +msgstr "domain" + +msgctxt "#42016" +msgid "Browse the ROMs by PEGI rating" +msgstr "domain" + +msgctxt "#42017" +msgid "Browse the ROMs by rating" +msgstr "domain" + +msgctxt "#42018" +msgid "Rebuild all the virtual categories and collections in the container" +msgstr "viewqueries" + +msgctxt "#42019" +msgid "Scan for addons that can be used by AKL (launchers, scrapers etc.)" +msgstr "viewqueries" + +msgctxt "#42020" +msgid "Shows previously scanned addons that can be used by AKL (launchers, scrapers etc.)" +msgstr "viewqueries" + +msgctxt "#42021" +msgid "Manage existing/available tags for ROMs" +msgstr "viewqueries" + +msgctxt "#42022" +msgid "Execute several [COLOR orange]Utilities[/COLOR]." +msgstr "viewqueries" + +msgctxt "#42023" +msgid "Exports all AKL categories and collections into an XML configuration file.\nYou can later reimport this XML file." +msgstr "viewqueries" + +msgctxt "#42024" +msgid "Check all collections for missing launchers or scanners, missing artwork, wrong platform names, asset path existence, etc." +msgstr "viewqueries" + +msgctxt "#42025" +msgid "Scans existing [COLOR=orange]ROMs artwork images[/COLOR] in ROM Collections and verifies that the images have correct extension and size is greater than 0. You can delete corrupted images to be rescraped later." +msgstr "viewqueries" + +msgctxt "#42026" +msgid "Scans all ROM collections and finds [COLOR orange]redundant ROMs artwork[/COLOR]. You may delete these unneeded images." +msgstr "viewqueries" + +msgctxt "#42027" +msgid "Display the auto-detected No-Intro/Redump DATs that will be used for the ROM audit. You have to configure the DAT directories in [COLOR orange]AKL addon settings[/COLOR], [COLOR=orange]ROM Audit[/COLOR] tab." +msgstr "viewqueries" + +msgctxt "#42028" +msgid "Shows a report of all ROM collections with number of ROMs." +msgstr "viewqueries" + +msgctxt "#42029" +msgid "Shows a report of all audited ROM collections, with Have, Miss and Unknown statistics." +msgstr "viewqueries" + +msgctxt "#42030" +msgid "Shows a report of all audited ROM Launchers, with Have, Miss and Unknown statistics. Only No-Intro platforms (cartridge-based) are reported." +msgstr "viewqueries" + +msgctxt "#42031" +msgid "Shows a report of all audited ROM Launchers, with Have, Miss and Unknown statistics. Only Redump platforms (optical-based) are reported." +msgstr "viewqueries" \ No newline at end of file diff --git a/resources/lib/commands/addon_commands.py b/resources/lib/commands/addon_commands.py index c238ed8c..69230fae 100644 --- a/resources/lib/commands/addon_commands.py +++ b/resources/lib/commands/addon_commands.py @@ -40,16 +40,16 @@ @AppMediator.register('SCAN_FOR_ADDONS') def cmd_scan_addons(args): - kodi.notify('Scanning for AKL supported addons') + kodi.notify(kodi.translate(40998)) addon_count = _check_installed_addons() - msg = 'No AKL addons found. Search and install default plugin addons for AKL?' + msg = kodi.translate(40963) if addon_count == 0 and kodi.dialog_yesno(msg): xbmc.executebuiltin('InstallAddon(script.akl.defaults)', True) addon_count = _check_installed_addons() logger.info(f'cmd_scan_addons(): Processed {addon_count} addons') - kodi.notify(f'Scan completed. Found {addon_count} addons') + kodi.notify(kodi.translate(40999).format(addon_count)) @AppMediator.register('SHOW_ADDONS') @@ -60,7 +60,7 @@ def cmd_show_addons(args): addons = repository.find_all() options = collections.OrderedDict() - options["cmd_SCAN_FOR_ADDONS"] = "> Scan for new or updated addons" + options["cmd_SCAN_FOR_ADDONS"] = kodi.translate(42008) for addon in addons: logger.info(f"Installed Addon {addon.get_addon_id()} v{addon.get_version()} {addon.get_addon_type()}") if addon.get_addon_id() in options: @@ -68,7 +68,7 @@ def cmd_show_addons(args): name = f"{addon.get_name()} v{addon.get_version()}" options[addon.get_addon_id()] = name - s = 'Addons' + s = kodi.translate(41080) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: return @@ -87,13 +87,13 @@ def cmd_addon_details(args): addon_id:str = args['addon_id'] if 'addon_id' in args else None options = collections.OrderedDict() - options["UPDATE"] = "Refresh/update addon" - options["METADATA"] = "Supported metadata" - options["ASSETS"] = "Supported assets" - options["SETTINGS"] = "Plugin settings" + options["UPDATE"] = kodi.translate(42009) + options["METADATA"] = kodi.translate(42010) + options["ASSETS"] = kodi.translate(42011) + options["SETTINGS"] = kodi.translate(42012) addon = xbmcaddon.Addon(addon_id) - title = f"Addon: {addon.getAddonInfo('name')}" + title = kodi.translate(41081).format(addon.getAddonInfo('name')) selected_option = kodi.OrdDictionaryDialog().select(title, options) if selected_option is None: @@ -138,14 +138,14 @@ def cmd_addon_details(args): }) repository.update_addon(ael_addon) uow.commit() - kodi.notify("Updated addon") + kodi.notify(kodi.translate(40962)) cmd_addon_details(args) return if selected_option == "METADATA": supported_metadata_str = addon.getSetting('akl.scraper.supported_metadata') - options = { m: constants.METADATA_DESCRIPTIONS[m] for m in supported_metadata_str.split('|') } - kodi.OrdDictionaryDialog().select(f"Supported metadata: {addon.getAddonInfo('name')}", options) + options = { m: kodi.translate(constants.METADATA_DESCRIPTIONS[m]) for m in supported_metadata_str.split('|') } + kodi.OrdDictionaryDialog().select(kodi.translate(41082).format(addon.getAddonInfo('name')), options) cmd_addon_details(args) return @@ -153,7 +153,7 @@ def cmd_addon_details(args): supported_assets_str = addon.getSetting('akl.scraper.supported_assets') assets = g_assetFactory.get_asset_list_by_IDs(supported_assets_str.split('|')) options = { a.id: a.name for a in assets } - kodi.OrdDictionaryDialog().select(f"Supported assets: {addon.getAddonInfo('name')}", options) + kodi.OrdDictionaryDialog().select(kodi.translate(41083).format(addon.getAddonInfo('name')), options) cmd_addon_details(args) return diff --git a/resources/lib/commands/api_commands.py b/resources/lib/commands/api_commands.py index 0383bc83..56acce08 100644 --- a/resources/lib/commands/api_commands.py +++ b/resources/lib/commands/api_commands.py @@ -61,7 +61,7 @@ def cmd_set_launcher_args(args) -> bool: launcher.set_settings(launcher_settings) if 'romcollection' in launcher_settings \ - and kodi.dialog_yesno('Do you want to overwrite collection metadata properties with values from the launcher?'): + and kodi.dialog_yesno(kodi.translate(41050)): romcollection.import_data_dic(launcher_settings['romcollection']) metadata_updated = True @@ -81,7 +81,7 @@ def cmd_set_launcher_args(args) -> bool: rom_repository.update_rom(rom) uow.commit() - kodi.notify(f'Configured launcher {addon.get_name()}') + kodi.notify(kodi.translate(41005).format(addon.get_name())) return True @@ -111,9 +111,9 @@ def cmd_set_scanner_settings(args) -> bool: romcollection_repository.update_romcollection(romcollection) uow.commit() - kodi.notify('Configured ROM scanner {}'.format(addon.get_name())) + kodi.notify(kodi.translate(41006).format(addon.get_name())) - if kodi.dialog_yesno('Scan for ROMs now?'): + if kodi.dialog_yesno(kodi.translate(41051)): AppMediator.async_cmd('SCAN_ROMS', {'romcollection_id': romcollection_id}) else: AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) @@ -147,7 +147,7 @@ def cmd_store_scanned_roms(args) -> bool: romcollection_repository.add_rom_to_romcollection(romcollection.get_id(), rom_obj.get_id()) uow.commit() - kodi.notify('Stored scanned ROMS in ROMs Collection {}'.format(romcollection.get_name())) + kodi.notify(kodi.translate(41007).format(romcollection.get_name())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) @@ -173,7 +173,7 @@ def cmd_remove_roms(args) -> bool: rom_repository.delete_rom(rom_id) uow.commit() - kodi.notify('Removed ROMS from ROMs Collection {}'.format(romcollection.get_name())) + kodi.notify(kodi.translate(41010).format(romcollection.get_name())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) @@ -236,7 +236,7 @@ def cmd_store_scraped_roms(args) -> bool: rom_repository.update_rom(rom_obj) uow.commit() - kodi.notify('Stored scraped ROMS in ROMs Collection {}'.format(romcollection.get_name())) + kodi.notify(kodi.translate(41008).format(romcollection.get_name())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) if metadata_is_updated: AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') @@ -288,7 +288,7 @@ def cmd_store_scraped_single_rom(args) -> bool: rom_repository.update_rom(rom) uow.commit() - kodi.notify('Stored scraped ROM {}'.format(rom.get_name())) + kodi.notify(kodi.translate(41009).format(rom.get_name())) for collection_id in rom_collection_ids: AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': collection_id}) diff --git a/resources/lib/commands/category_commands.py b/resources/lib/commands/category_commands.py index ea6be45e..a09a1d70 100644 --- a/resources/lib/commands/category_commands.py +++ b/resources/lib/commands/category_commands.py @@ -44,13 +44,14 @@ def cmd_add_category(args): if grand_parent_category is not None: options_dialog = kodi.ListDialog() - selected_option = options_dialog.select('Add category in?',[parent_category.get_name(), grand_parent_category.get_name()]) + selected_option = options_dialog.select(kodi.translate(41084),[parent_category.get_name(), grand_parent_category.get_name()]) if selected_option > 0: parent_category = grand_parent_category # --- Get new Category name --- - name = kodi.dialog_keyboard('New Category Name') - if name is None: return + name = kodi.dialog_keyboard(kodi.translate(41085)) + if name is None: + return category = Category() category.set_name(name) @@ -59,7 +60,7 @@ def cmd_add_category(args): repository.insert_category(category, parent_category) uow.commit() - kodi.notify('Category {0} created'.format(category.get_name())) + kodi.notify(kodi.translate(41036).format(category.get_name())) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) @@ -80,12 +81,12 @@ def cmd_edit_category(args): category = repository.find_category(category_id) options = collections.OrderedDict() - options['CATEGORY_EDIT_METADATA'] = kodi.translate(40853) - options['CATEGORY_EDIT_ASSETS'] = kodi.translate(40854) + options['CATEGORY_EDIT_METADATA'] = kodi.translate(40853) + options['CATEGORY_EDIT_ASSETS'] = kodi.translate(40854) options['CATEGORY_EDIT_DEFAULT_ASSETS'] = kodi.translate(40859) - options['CATEGORY_STATUS'] = f'{kodi.translate(40859)} {category.get_finished_str()}' - options['EXPORT_CATEGORY_XML'] = kodi.translate(40861) - options['DELETE_CATEGORY'] = kodi.translate(40862) + options['CATEGORY_STATUS'] = f'{kodi.translate(40859)} {category.get_finished_str_code()}' + options['EXPORT_CATEGORY_XML'] = kodi.translate(40861) + options['DELETE_CATEGORY'] = kodi.translate(40862) s = f'{kodi.translate(40950)} "{category.get_name}"' selected_option = kodi.OrdDictionaryDialog().select(s, options) @@ -112,21 +113,21 @@ def cmd_edit_metadata_category(args): category = repository.find_category(category_id) NFO_FileName = category.get_NFO_name() - NFO_found_str = 'NFO found' if NFO_FileName.exists() else 'NFO not found' + NFO_found_str = kodi.translate(42019) if NFO_FileName.exists() else kodi.translate(42020) plot_str = text.limit_string(category.get_plot(), constants.PLOT_STR_MAXSIZE) options = collections.OrderedDict() - options['CATEGORY_EDIT_METADATA_TITLE'] = "Edit Title: '{}'".format(category.get_name()) - options['CATEGORY_EDIT_METADATA_RELEASEYEAR'] = "Edit Release Year: '{}'".format(category.get_releaseyear()) - options['CATEGORY_EDIT_METADATA_GENRE'] = "Edit Genre: '{}'".format(category.get_genre()) - options['CATEGORY_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(category.get_developer()) - options['CATEGORY_EDIT_METADATA_RATING'] = "Edit Rating: '{}'".format(category.get_rating()) - options['CATEGORY_EDIT_METADATA_PLOT'] = "Edit Plot: '{}'".format(plot_str) - options['CATEGORY_IMPORT_NFO_FILE'] = 'Import NFO file (default, {})'.format(NFO_found_str) - options['CATEGORY_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' - options['CATEGORY_SAVE_NFO_FILE'] = 'Save NFO file (default location)' + options['CATEGORY_EDIT_METADATA_TITLE'] = kodi.translate(40863).format(category.get_name()) + options['CATEGORY_EDIT_METADATA_RELEASEYEAR'] = kodi.translate(40865).format(category.get_releaseyear()) + options['CATEGORY_EDIT_METADATA_GENRE'] = kodi.translate(40867).format(category.get_genre()) + options['CATEGORY_EDIT_METADATA_DEVELOPER'] = kodi.translate(40868).format(category.get_developer()) + options['CATEGORY_EDIT_METADATA_RATING'] = kodi.translate(40869).format(category.get_rating()) + options['CATEGORY_EDIT_METADATA_PLOT'] = kodi.translate(40870).format(plot_str) + options['CATEGORY_IMPORT_NFO_FILE'] = kodi.translate(40876).format(NFO_found_str) + options['CATEGORY_IMPORT_NFO_FILE_BROWSE'] = kodi.translate(40877) + options['CATEGORY_SAVE_NFO_FILE'] = kodi.translate(40878) - s = 'Edit Category "{}" metadata'.format(category.get_name()) + s = kodi.translate(41086).format(category.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -203,7 +204,7 @@ def cmd_category_status(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) category.change_finished_status() - kodi.dialog_OK('Category "{}" status is now {}'.format(category.get_name(), category.get_finished_str())) + kodi.dialog_OK(kodi.translate(41146).format(category.get_name(), kodi.translate(category.get_finished_str_code()))) repository.update_category(category) uow.commit() @@ -223,13 +224,12 @@ def cmd_category_delete(args): category = repository.find_category(category_id) category_name = category.get_name() + del_q = kodi.translate(41066).format(category_name) if category.has_items(): - question = (f'Category "{category_name}" has {category.num_categories()} sub-categories and ' - f'{category.num_romcollections()} romcollections. Deleting it will also delete related items. ' - f'Are you sure you want to delete "{category_name}"?') + question = kodi.translate(41067).format(category_name, category.num_categories(), category.num_romcollections()) \ + + kodi.translate(41066).format(category_name) else: - question = (f'Category "{category_name}" has no categories or romcollections. ' - f'Are you sure you want to delete "{category_name}"?') + question = kodi.translate(41068).format(category_name) + kodi.translate(41066).format(category_name) ret = kodi.dialog_yesno(question) if not ret: return @@ -238,7 +238,7 @@ def cmd_category_delete(args): repository.delete_category(category_id) uow.commit() - kodi.notify(f'Deleted category {category_name}') + kodi.notify(kodi.translate(41037).format(category_name)) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.async_cmd('CLEANUP_VIEWS') AppMediator.sync_cmd('EDIT_CATEGORY', args) @@ -257,7 +257,7 @@ def cmd_category_metadata_title(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) - if editors.edit_field_by_str(category, 'Title', category.get_name, category.set_name): + if editors.edit_field_by_str(category, kodi.translate(40812), category.get_name, category.set_name): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) @@ -272,7 +272,7 @@ def cmd_category_metadata_releaseyear(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) - if editors.edit_field_by_str(category, 'Release Year', category.get_releaseyear, category.set_releaseyear): + if editors.edit_field_by_str(category, kodi.translate(40803), category.get_releaseyear, category.set_releaseyear): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) @@ -287,7 +287,7 @@ def cmd_category_metadata_genre(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) - if editors.edit_field_by_str(category, 'Genre', category.get_genre, category.set_genre): + if editors.edit_field_by_str(category, kodi.translate(40801), category.get_genre, category.set_genre): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) @@ -302,7 +302,7 @@ def cmd_category_metadata_developer(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) - if editors.edit_field_by_str(category, 'Developer', category.get_developer, category.set_developer): + if editors.edit_field_by_str(category, kodi.translate(40802), category.get_developer, category.set_developer): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) @@ -332,7 +332,7 @@ def cmd_category_metadata_plot(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) - if editors.edit_field_by_str(category, 'Plot', category.get_plot, category.set_plot): + if editors.edit_field_by_str(category, kodi.translate(40811), category.get_plot, category.set_plot): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) @@ -351,7 +351,7 @@ def cmd_category_import_nfo_file(args): if category.import_NFO_file(NFO_file): repository.update_category(category) uow.commit() - kodi.notify('Imported Category NFO file {0}'.format(NFO_file.getPath())) + kodi.notify(kodi.translate(41038).format(NFO_file.getPath())) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) @@ -361,11 +361,13 @@ def cmd_category_import_nfo_file(args): def cmd_category_browse_import_nfo_file(args): category_id = args['category_id'] if 'category_id' in args else None - NFO_file = kodi.browse(text='Select NFO description file', mask='.nfo') + NFO_file = kodi.browse(text=kodi.translate(41143), mask='.nfo') logger.debug('cmd_category_browse_import_nfo_file() Dialog().browse returned "{0}"'.format(NFO_file)) - if not NFO_file: return + if not NFO_file: + return NFO_FileName = io.FileName(NFO_file) - if not NFO_FileName.exists(): return + if not NFO_FileName.exists(): + return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -375,7 +377,7 @@ def cmd_category_browse_import_nfo_file(args): if category.import_NFO_file(NFO_FileName): repository.update_category(category) uow.commit() - kodi.notify('Imported Category NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify(kodi.translate(41038).format(NFO_FileName.getPath())) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) @@ -395,11 +397,11 @@ def cmd_category_save_nfo_file(args): try: category.export_to_NFO_file(NFO_FileName) except: - kodi.notify_warn('Exception writing NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify_warn(kodi.translate(41042).format(NFO_FileName.getPath())) logger.error("cmd_category_save_nfo_file() Exception writing'{0}'".format(NFO_FileName.getPath())) else: logger.debug("cmd_category_save_nfo_file() Created '{0}'".format(NFO_FileName.getPath())) - kodi.notify('Exported Category NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify(kodi.translate(41039).format(NFO_FileName.getPath())) AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) @@ -420,7 +422,7 @@ def cmd_category_export_xml(args): logger.debug('cmd_export_category_xml() l_fn_str "{0}"'.format(category_fn_str)) # --- Ask user for a path to export the launcher configuration --- - dir_path = kodi.browse(type=0, text='Select directory to export XML') + dir_path = kodi.browse(type=0, text=kodi.translate(41144)) if not dir_path: AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) return @@ -428,9 +430,9 @@ def cmd_category_export_xml(args): # --- If XML exists then warn user about overwriting it --- export_FN = io.FileName(dir_path).pjoin(category_fn_str) if export_FN.exists(): - ret = kodi.dialog_yesno('Overwrite file {0}?'.format(export_FN.getPath())) + ret = kodi.dialog_yesno(kodi.translate(41052).format(export_FN.getPath())) if not ret: - kodi.notify_warn('Export of Category XML cancelled') + kodi.notify_warn(kodi.translate(41040)) AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) return @@ -441,8 +443,8 @@ def cmd_category_export_xml(args): try: category.export_to_file(export_FN) except constants.AddonError as E: - kodi.notify_warn('{0}'.format(E)) + kodi.notify_warn(str(E)) else: - kodi.notify('Exported Category "{0}" XML config'.format(category.get_name())) + kodi.notify(kodi.translate(41041).format(category.get_name())) AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) \ No newline at end of file diff --git a/resources/lib/commands/chk_commands.py b/resources/lib/commands/chk_commands.py index fb0ecddd..621ce836 100644 --- a/resources/lib/commands/chk_commands.py +++ b/resources/lib/commands/chk_commands.py @@ -130,7 +130,7 @@ def cmd_check_ROM_artwork_integrity(args): ['Launcher', 'ROMs', 'Images', 'Missing', 'Problematic'], ] pdialog = kodi.ProgressDialog() - d_msg = 'Checking ROM artwork integrity...' + d_msg = kodi.translate(41152) pdialog.startProgress(d_msg, len(romcollections)) total_images = 0 missing_images = 0 @@ -292,11 +292,11 @@ def cmd_delete_redundant_rom_artwork(args): options[collection] = collection.get_name() dialog = kodi.MultiSelectDialog() - selected_collections:typing.List[ROMCollection] = dialog.select('Collections to process', options, preselected=romcollections) + selected_collections:typing.List[ROMCollection] = dialog.select(kodi.translate(41087), options, preselected=romcollections) main_slist = [] detailed_slist = [] - d_msg = 'Checking ROM artwork integrity...' + d_msg = kodi.translate(41152) pdialog.startProgress(d_msg, len(selected_collections)) all_unique_paths = [] @@ -411,7 +411,7 @@ def cmd_delete_redundant_rom_artwork(args): pdialog.endProgress() kodi.display_text_window_mono('ROM redundant artwork report', output_table) - do_delete = kodi.dialog_yesno(f'Delete {len(files_to_be_removed)} files marked as redundant?\nWarning! This will actually delete the files!\m Backup filesnow if needed.') + do_delete = kodi.dialog_yesno(kodi.translate(41071).format(len(files_to_be_removed))) if not do_delete: return @@ -422,4 +422,4 @@ def cmd_delete_redundant_rom_artwork(args): file_to_delete.unlink() pdialog.endProgress() - kodi.notify('Cleaned up redundant files') + kodi.notify(kodi.translate(41011)) diff --git a/resources/lib/commands/mediator.py b/resources/lib/commands/mediator.py index 13a9d465..d30b6cad 100644 --- a/resources/lib/commands/mediator.py +++ b/resources/lib/commands/mediator.py @@ -34,7 +34,7 @@ def sync_cmd(cls, command='undefined', args=None): return a_command(args) except Exception as ex: logger.fatal('Failure processing command "{}"'.format(command), exc_info=ex) - kodi.notify_error('Failure processing command "{}"'.format(command)) + kodi.notify_error(kodi.translate(41043).format(command)) @classmethod def async_cmd(cls, command='undefined', args=None): diff --git a/resources/lib/commands/misc_commands.py b/resources/lib/commands/misc_commands.py index f7009a9d..9ea2e8d4 100644 --- a/resources/lib/commands/misc_commands.py +++ b/resources/lib/commands/misc_commands.py @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) @AppMediator.register('IMPORT_LAUNCHERS') def cmd_execute_import_launchers(args): - file_list = kodi.browse(text='Select XML category/launcher configuration file',mask='.xml', multiple=True) + file_list = kodi.browse(text=kodi.translate(41145),mask='.xml', multiple=True) uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -73,7 +73,7 @@ def cmd_execute_import_launchers(args): if category_to_import.get_id() in existing_category_ids: # >> Category exists (by name). Overwrite? logger.debug('Category found. Edit existing category.') - if kodi.dialog_yesno(f'Category "{category_to_import.get_name()}" found in AKL database. Overwrite?'): + if kodi.dialog_yesno(kodi.translate(41072).format(category_to_import.get_name())): categories_to_update.append(category_to_import) else: categories_to_insert.append(category_to_import) @@ -83,7 +83,7 @@ def cmd_execute_import_launchers(args): if launcher_to_import.get_id() in existing_romcollection_ids: # >> Romset exists (by name). Overwrite? logger.debug('ROMCollection found. Edit existing ROMCollection.') - if kodi.dialog_yesno(f'ROMCollection "{launcher_to_import.get_name()}" found in AKL database. Overwrite?'): + if kodi.dialog_yesno(kodi.translate(41073).format(launcher_to_import.get_name())): romcollections_to_update.append(launcher_to_import) else: romcollections_to_insert.append(launcher_to_import) @@ -107,7 +107,7 @@ def cmd_execute_import_launchers(args): uow.commit() AppMediator.async_cmd('RENDER_VIEWS') - kodi.notify('Finished importing Categories/Launchers') + kodi.notify(kodi.translate(41012)) # Export AKL launcher configuration. # Export all Categories and Launchers. @@ -116,15 +116,16 @@ def cmd_export_to_xml(args): logger.debug('_command_exec_utils_export_launchers() Exporting Category/Launcher XML configuration') # --- Ask path to export XML configuration --- - dir_path = kodi.dialog_get_directory('Select XML export directory') - if not dir_path: return + dir_path = kodi.dialog_get_directory(kodi.translate(41144)) + if not dir_path: + return # --- If XML exists then warn user about overwriting it --- export_FN = io.FileName(dir_path).pjoin('AKL_configuration.xml') if export_FN.exists(): - ret = kodi.dialog_yesno('AKL_configuration.xml found in the selected directory. Overwrite?') + ret = kodi.dialog_yesno(kodi.translate(41054)) if not ret: - kodi.notify_warn('Category/Launcher XML exporting cancelled') + kodi.notify_warn(kodi.translate(41013)) return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -207,13 +208,13 @@ def cmd_export_to_xml(args): parsed_xml = minidom.parseString(result_xml) export_FN.saveStrToFile(parsed_xml.toprettyxml(indent=" ")) except constants.AddonError as ex: - kodi.notify_warn('{}'.format(ex)) + kodi.notify_warn(str(ex)) else: - kodi.notify('Exported AKL Categories and Collections to XML configuration') + kodi.notify(kodi.translate(41014)) @AppMediator.register('RESET_DATABASE') def cmd_execute_reset_db(args): - if not kodi.dialog_yesno('Are you sure you want to reset the database?'): + if not kodi.dialog_yesno(kodi.translate(41053)): return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -222,7 +223,7 @@ def cmd_execute_reset_db(args): AppMediator.async_cmd('CLEANUP_VIEWS') AppMediator.async_cmd('RENDER_VIEWS') AppMediator.async_cmd('SCAN_FOR_ADDONS') - kodi.notify('Finished resetting the database') + kodi.notify(kodi.translate(41015)) @AppMediator.register('RUN_DB_MIGRATIONS') def cmd_execute_migrations(args): @@ -241,7 +242,7 @@ def cmd_execute_migrations(args): options[migration_file.getPath()] = f"{migration_file.getBase()} [{state}]" dialog = kodi.OrdDictionaryDialog() - selected_file = dialog.select(f"Select migrations to execute (Current version {db_version})", options) + selected_file = dialog.select(kodi.translate(41088).format(db_version), options) if selected_file is None: return @@ -255,19 +256,19 @@ def cmd_execute_migrations(args): version_to_store = db_version dialog = kodi.ListDialog() - selected_index = dialog.select(f"Migration {migration_file.getBaseNoExt()}",[ - "Run migration", - "Mark as executed without running" + selected_index = dialog.select(kodi.translate(41089).format(migration_file.getBaseNoExt()),[ + kodi.translate(41090), + kodi.translate(41091) ]) if not selected_index: return if selected_index == 0: - if not kodi.dialog_yesno(f"Run migration {migration_file.getBaseNoExt()}?"): + if not kodi.dialog_yesno(kodi.translate(41055).format(migration_file.getBaseNoExt())): return uow.migrate_database([migration_file], version_to_store, selected_index==1) - kodi.notify('Done running migrations on the database') + kodi.notify(kodi.translate(41016)) @AppMediator.register('CHECK_DUPLICATE_ASSET_DIRS') def cmd_check_duplicate_asset_dirs(args): @@ -282,8 +283,7 @@ def cmd_check_duplicate_asset_dirs(args): duplicated_name_list = romcollection.get_duplicated_asset_dirs() if duplicated_name_list: duplicated_asset_srt = ', '.join(duplicated_name_list) - kodi.dialog_OK('Duplicated asset directories: {0}. '.format(duplicated_asset_srt) + - 'AKL will refuse to add/edit ROMs if there are duplicate asset directories.') + kodi.dialog_OK(kodi.translate(41147).format(duplicated_asset_srt)) def _apply_addon_launcher_for_legacy_launcher(collection: ROMCollection, available_addons: typing.Dict[str, AelAddon]): launcher_type = collection.get_custom_attribute('type') diff --git a/resources/lib/commands/rom_commands.py b/resources/lib/commands/rom_commands.py index 8cf3718d..9cde3d9e 100644 --- a/resources/lib/commands/rom_commands.py +++ b/resources/lib/commands/rom_commands.py @@ -42,7 +42,7 @@ def cmd_edit_rom(args): if rom_id is None: logger.warning('cmd_edit_rom(): No ROM id supplied.') - kodi.notify_warn("Invalid parameters supplied.") + kodi.notify_warn(kodi.translate(40951)) return selected_option = None @@ -52,17 +52,19 @@ def cmd_edit_rom(args): rom = repository.find_rom(rom_id) options = collections.OrderedDict() - options['ROM_EDIT_METADATA'] = kodi.translate(40853) - options['ROM_EDIT_ASSETS'] = kodi.translate(40854) + options['ROM_EDIT_METADATA'] = kodi.translate(40853) + options['ROM_EDIT_ASSETS'] = kodi.translate(40854) options['ROM_EDIT_DEFAULT_ASSETS'] = kodi.translate(40859) - options['EDIT_ROM_STATUS'] = f'ROM status: {rom.get_finished_str()}' + options['EDIT_ROM_STATUS'] = kodi.translate(42013).format( + kodi.translate(rom.get_finished_str_code())) if rom.has_launchers(): - options['EDIT_ROM_LAUNCHERS'] = 'Manage associated launchers' - else: options['ADD_ROM_LAUNCHER'] = 'Add new launcher to ROM' - options['DELETE_ROM'] = 'Delete ROM' - options['SCRAPE_ROM'] = kodi.translate(40855) + options['EDIT_ROM_LAUNCHERS'] = kodi.translate(42016) + else: + options['ADD_ROM_LAUNCHER'] = kodi.translate(42017) + options['DELETE_ROM'] = kodi.translate(42018) + options['SCRAPE_ROM'] = kodi.translate(40855) - s = f'Edit ROM "{rom.get_name()}"' + s = kodi.translate(41092).format(rom.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -77,39 +79,39 @@ def cmd_edit_rom(args): @AppMediator.register('ROM_EDIT_METADATA') def cmd_rom_metadata(args): rom_id: str = args['rom_id'] if 'rom_id' in args else None - selected_option = None + selected_option = None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) plot_str = text.limit_string(rom.get_plot(), constants.PLOT_STR_MAXSIZE) - rating = rom.get_rating() if rom.get_rating() != -1 else 'not rated' + rating = rom.get_rating() if rom.get_rating() != -1 else kodi.translate(42021) NFO_FileName = rom.get_nfo_file() - NFO_found_str = 'NFO found' if NFO_FileName and NFO_FileName.exists() else 'NFO not found' + NFO_found_str = kodi.translate(42019) if NFO_FileName and NFO_FileName.exists() else kodi.translate(42020) options = collections.OrderedDict() - options['ROM_EDIT_METADATA_TITLE'] = f"{kodi.translate(40863)}: '{rom.get_name()}'" - options['ROM_EDIT_METADATA_PLATFORM'] = f"{kodi.translate(40864)}: {rom.get_platform()}" - options['ROM_EDIT_METADATA_RELEASEYEAR'] = f"{kodi.translate(40865)}: {rom.get_releaseyear()}" - options['ROM_EDIT_METADATA_GENRE'] = "Edit Genre: '{}'".format(rom.get_genre()) - options['ROM_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(rom.get_developer()) - options['ROM_EDIT_METADATA_NPLAYERS'] = "Edit NPlayers: '{}'".format(rom.get_number_of_players()) - options['ROM_EDIT_METADATA_NPLAYERS_ONL'] = "Edit NPlayers online: '{}'".format(rom.get_number_of_players_online()) - options['ROM_EDIT_METADATA_ESRB'] = "Edit ESRB rating: '{}'".format(rom.get_esrb_rating()) - options['ROM_EDIT_METADATA_PEGI'] = "Edit PEGI rating: '{}'".format(rom.get_pegi_rating()) - options['ROM_EDIT_METADATA_RATING'] = "Edit Rating: '{}'".format(rating) - options['ROM_EDIT_METADATA_PLOT'] = "Edit Plot: '{}'".format(plot_str) - options['ROM_EDIT_METADATA_TAGS'] = "Edit Tags" - options['ROM_EDIT_METADATA_BOXSIZE'] = "Edit Box Size: '{}'".format(rom.get_box_sizing()) - options['ROM_LOAD_PLOT'] = "Load Plot from TXT file ..." - options['ROM_IMPORT_NFO_FILE_DEFAULT'] = 'Import NFO file (default {})'.format(NFO_found_str) - options['ROM_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' - options['ROM_SAVE_NFO_FILE_DEFAULT'] = 'Save NFO file (default location)' - options['SCRAPE_ROM_METADATA'] = 'Scrape Metadata' - - s = 'Edit ROM "{0}" metadata'.format(rom.get_name()) + options['ROM_EDIT_METADATA_TITLE'] = kodi.translate(40863).format(rom.get_name) + options['ROM_EDIT_METADATA_PLATFORM'] = kodi.translate(40864).format(rom.get_platform()) + options['ROM_EDIT_METADATA_RELEASEYEAR'] = kodi.translate(40865).format(rom.get_releaseyear()) + options['ROM_EDIT_METADATA_GENRE'] = kodi.translate(40867).format(rom.get_genre()) + options['ROM_EDIT_METADATA_DEVELOPER'] = kodi.translate(40868).format(rom.get_developer()) + options['ROM_EDIT_METADATA_NPLAYERS'] = kodi.translate(40871).format(rom.get_number_of_players()) + options['ROM_EDIT_METADATA_NPLAYERS_ONL'] = kodi.translate(40872).format(rom.get_number_of_players_online()) + options['ROM_EDIT_METADATA_ESRB'] = kodi.translate(40874).format(rom.get_esrb_rating()) + options['ROM_EDIT_METADATA_PEGI'] = kodi.translate(40873).format(rom.get_pegi_rating()) + options['ROM_EDIT_METADATA_RATING'] = kodi.translate(40869).format(rating) + options['ROM_EDIT_METADATA_PLOT'] = kodi.translate(40870).format(plot_str) + options['ROM_EDIT_METADATA_TAGS'] = kodi.translate(40866) + options['ROM_EDIT_METADATA_BOXSIZE'] = kodi.translate(40875).format(rom.get_box_sizing()) + options['ROM_LOAD_PLOT'] = kodi.translate(40879) + options['ROM_IMPORT_NFO_FILE_DEFAULT'] = kodi.translate(40876).format(NFO_found_str) + options['ROM_IMPORT_NFO_FILE_BROWSE'] = kodi.translate(40877) + options['ROM_SAVE_NFO_FILE_DEFAULT'] = kodi.translate(40878) + options['SCRAPE_ROM_METADATA'] = kodi.translate(40880) + + s = kodi.translate(41093).format(rom.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Return recursively to parent menu. @@ -201,7 +203,7 @@ def cmd_rom_delete(args): rom = roms_repository.find_rom(rom_id) - question = f'Are you sure you want to delete "{rom.get_name()}"?\nThis will delete the ROM from all views.' + question = kodi.translate(41056).format(rom.get_name()) ret = kodi.dialog_yesno(question) if not ret: AppMediator.sync_cmd('EDIT_ROM', args) @@ -220,7 +222,7 @@ def cmd_rom_delete(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) AppMediator.async_cmd('RENDER_VIRTUAL_VIEWS') - kodi.notify(f'Deleted ROM {rom.get_name()}') + kodi.notify(kodi.translate(41024).format(rom.get_name())) # --- Atomic commands --- @AppMediator.register('ROM_EDIT_METADATA_TITLE') @@ -231,7 +233,7 @@ def cmd_rom_metadata_title(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_str(rom, 'Title', rom.get_name, rom.set_name): + if editors.edit_field_by_str(rom, kodi.translate(40812), rom.get_name, rom.set_name): repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -247,7 +249,7 @@ def cmd_rom_metadata_platform(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_list(rom, 'Platform', platforms.AKL_platform_list, + if editors.edit_field_by_list(rom, kodi.translate(40807), platforms.AKL_platform_list, rom.get_platform, rom.set_platform): repository.update_rom(rom) uow.commit() @@ -263,7 +265,7 @@ def cmd_rom_metadata_esrb(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_list(rom, 'ESRB rating', constants.ESRB_LIST, + if editors.edit_field_by_list(rom, kodi.translate(40804), constants.ESRB_LIST, rom.get_esrb_rating, rom.set_esrb_rating): repository.update_rom(rom) uow.commit() @@ -280,7 +282,7 @@ def cmd_rom_metadata_pegi(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_list(rom, 'PEGI rating', constants.PEGI_LIST, + if editors.edit_field_by_list(rom, kodi.translate(40805), constants.PEGI_LIST, rom.get_pegi_rating, rom.set_pegi_rating): repository.update_rom(rom) uow.commit() @@ -297,7 +299,7 @@ def cmd_rom_metadata_releaseyear(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_str(rom, 'Release Year', rom.get_releaseyear, rom.set_releaseyear): + if editors.edit_field_by_str(rom, kodi.translate(40803), rom.get_releaseyear, rom.set_releaseyear): repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -313,7 +315,7 @@ def cmd_rom_metadata_genre(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_str(rom, 'Genre', rom.get_genre, rom.set_genre): + if editors.edit_field_by_str(rom, kodi.translate(40801), rom.get_genre, rom.set_genre): repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -329,7 +331,7 @@ def cmd_rom_metadata_developer(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_str(rom, 'Developer', rom.get_developer, rom.set_developer): + if editors.edit_field_by_str(rom, kodi.translate(40802), rom.get_developer, rom.set_developer): repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -346,8 +348,11 @@ def cmd_rom_metadata_nplayers(args): rom = repository.find_rom(rom_id) default_options = list(constants.NPLAYERS_LIST.keys()) - menu_list = ['Not set', 'Manual entry'] + default_options - selected_option = kodi.ListDialog().select('Edit ROM NPlayers', menu_list) + menu_list = [ + kodi.translate(42001), + kodi.translate(42022) + ] + default_options + selected_option = kodi.ListDialog().select(kodi.translate(41094), menu_list) if selected_option is None or selected_option < 0: AppMediator.sync_cmd('ROM_EDIT_METADATA', args) @@ -358,7 +363,7 @@ def cmd_rom_metadata_nplayers(args): if selected_option == 1: # >> Manual entry. Open a text entry dialog. - if not editors.edit_field_by_int(rom, 'NPlayers', rom.get_number_of_players, rom.set_number_of_players): + if not editors.edit_field_by_int(rom, kodi.translate(40808), rom.get_number_of_players, rom.set_number_of_players): AppMediator.sync_cmd('ROM_EDIT_METADATA', args) return @@ -371,7 +376,7 @@ def cmd_rom_metadata_nplayers(args): uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_NPLAYERS_ID}) - kodi.notify('Changed ROM NPlayers') + kodi.notify(kodi.translate(41025)) AppMediator.sync_cmd('ROM_EDIT_METADATA', args) @@ -384,8 +389,11 @@ def cmd_rom_metadata_nplayers_online(args): rom = repository.find_rom(rom_id) default_options = list(constants.NPLAYERS_LIST.keys()) - menu_list = ['Not set', 'Manual entry'] + default_options - selected_option = kodi.ListDialog().select('Edit ROM NPlayers online', menu_list) + menu_list = [ + kodi.translate(42001), + kodi.translate(42022) + ] + default_options + selected_option = kodi.ListDialog().select(kodi.translate(41095), menu_list) if selected_option is None or selected_option < 0: AppMediator.sync_cmd('ROM_EDIT_METADATA', args) @@ -396,7 +404,7 @@ def cmd_rom_metadata_nplayers_online(args): if selected_option == 1: # >> Manual entry. Open a number entry dialog. - if not editors.edit_field_by_int(rom, 'NPlayers', rom.get_number_of_players, rom.set_number_of_players): + if not editors.edit_field_by_int(rom, kodi.translate(40809), rom.get_number_of_players, rom.set_number_of_players): AppMediator.sync_cmd('ROM_EDIT_METADATA', args) return @@ -409,7 +417,7 @@ def cmd_rom_metadata_nplayers_online(args): uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_NPLAYERS_ID}) - kodi.notify('Changed ROM NPlayers Online') + kodi.notify(kodi.translate(41026)) AppMediator.sync_cmd('ROM_EDIT_METADATA', args) @@ -438,7 +446,7 @@ def cmd_rom_metadata_plot(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_str(rom, 'Plot', rom.get_plot, rom.set_plot): + if editors.edit_field_by_str(rom, kodi.translate(40811), rom.get_plot, rom.set_plot): repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -456,14 +464,14 @@ def cmd_rom_metadata_tags(args): did_remove_tag = False options = collections.OrderedDict() - options['ROM_ADD_METADATA_TAGS'] = "[Add tag]" - options['ROM_CLEAR_METADATA_TAGS'] = "[Clear all tags]" + options['ROM_ADD_METADATA_TAGS'] = kodi.translate(42023) + options['ROM_CLEAR_METADATA_TAGS'] = kodi.translate(42024) for tag in rom.get_tags(): options[tag] = tag while selected_option is not None: - s = 'Edit Tags' + s = kodi.translate(40866) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: continue @@ -472,17 +480,17 @@ def cmd_rom_metadata_tags(args): selected_option == 'ROM_CLEAR_METADATA_TAGS': break - if not kodi.dialog_yesno(f'Remove tag "{selected_option}"?'): + if not kodi.dialog_yesno(kodi.translate(41057).format(selected_option)): continue did_remove_tag = True logger.debug(f'cmd_rom_metadata_remove_tag() Remove tag {options[selected_option]}') - kodi.notify(f'Removing tag "{selected_option}"') + kodi.notify(kodi.translate(41027).format(selected_option)) del options[selected_option] rom.remove_tag(selected_option) if did_remove_tag: - kodi.notify('Updating ROM with removed tags') + kodi.notify(kodi.translate(41028)) repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -506,7 +514,7 @@ def cmd_rom_metadata_add_tag(args): available_tags = repository.find_all_tags() options = collections.OrderedDict() - options['MANUAL'] = '[Manual insert tag]' + options['MANUAL'] = kodi.translate(42025) if available_tags is not None and len(available_tags) > 0: options.update({value:key for key, value in available_tags.items()}) @@ -514,26 +522,26 @@ def cmd_rom_metadata_add_tag(args): did_add_tag = False while selected_option is not None: - selected_option = kodi.OrdDictionaryDialog().select('Tag to add', options) + selected_option = kodi.OrdDictionaryDialog().select(kodi.translate(41096), options) if selected_option is None: break if selected_option == 'MANUAL': - tag = kodi.dialog_keyboard('Tag') + tag = kodi.dialog_keyboard(kodi.translate(40814)) if tag is not None: did_add_tag = True logger.debug(f'cmd_rom_metadata_add_tag() Adding tag "{tag}"') - kodi.notify(f'Adding tag "{tag}') + kodi.notify(kodi.translate(41029).format(tag)) rom.add_tag(tag) else: tag = options[selected_option] did_add_tag = True logger.debug(f'cmd_rom_metadata_add_tag() Adding tag "{tag}"') - kodi.notify(f'Adding tag "{tag}') + kodi.notify(kodi.translate(41029).format(tag)) rom.add_tag(tag) if did_add_tag: - kodi.notify('Updating ROM with added tags') + kodi.notify(kodi.translate(41030)) repository.update_rom(rom) uow.commit() AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) @@ -547,11 +555,11 @@ def cmd_rom_metadata_clear_tags(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if kodi.dialog_yesno(f'Clear all tags from ROM "{rom.get_name()}"?'): + if kodi.dialog_yesno(kodi.translate(41058).format(rom.get_name())): rom.clear_tags() repository.update_rom(rom) uow.commit() - kodi.notify(f'Removed all tags from ROM "{rom.get_name()}') + kodi.notify(kodi.translate(41031).format(rom.get_name())) AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.sync_cmd('ROM_EDIT_METADATA_TAGS', args) @@ -563,7 +571,7 @@ def cmd_rom_metadata_boxsize(args): repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) - if editors.edit_field_by_list(rom, 'Default box size', constants.BOX_SIZES, + if editors.edit_field_by_list(rom, kodi.translate(40816), constants.BOX_SIZES, rom.get_box_sizing, rom.set_box_sizing): repository.update_rom(rom) uow.commit() @@ -574,7 +582,7 @@ def cmd_rom_metadata_boxsize(args): def cmd_rom_load_plot(args): rom_id = args['rom_id'] if 'rom_id' in args else None - plot_file = kodi.browse(text='Select description file (TXT|DAT)', mask='.txt|.dat') + plot_file = kodi.browse(text=kodi.translate(41157), mask='.txt|.dat') logger.debug('cmd_rom_load_plot() Dialog().browse returned "{0}"'.format(plot_file)) if not plot_file: return plot_FileName = io.FileName(plot_file) @@ -590,7 +598,7 @@ def cmd_rom_load_plot(args): repository.update_rom(rom) uow.commit() - kodi.notify('Imported ROM Plot') + kodi.notify(kodi.translate(41032)) AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.sync_cmd('ROM_EDIT_METADATA', args) @@ -606,13 +614,13 @@ def cmd_rom_import_nfo_file(args): NFO_file = rom.get_nfo_file() if not NFO_file: - kodi.dialog_OK('No NFO file available') + kodi.dialog_OK(kodi.translate(41148)) return if rom.update_with_nfo_file(NFO_file): repository.update_rom(rom) uow.commit() - kodi.notify('Imported ROMCollection NFO file {0}'.format(NFO_file.getPath())) + kodi.notify(kodi.translate(41033).format(NFO_file.getPath())) AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') @@ -622,7 +630,7 @@ def cmd_rom_import_nfo_file(args): def cmd_rom_browse_import_nfo_file(args): rom_id = args['rom_id'] if 'rom_id' in args else None - NFO_file = kodi.browse(text='Select NFO description file', mask='.nfo') + NFO_file = kodi.browse(text=kodi.translate(41143), mask='.nfo') logger.debug('cmd_rom_browse_import_nfo_file() Dialog().browse returned "{0}"'.format(NFO_file)) if not NFO_file: return NFO_FileName = io.FileName(NFO_file) @@ -636,7 +644,7 @@ def cmd_rom_browse_import_nfo_file(args): if rom.update_with_nfo_file(NFO_FileName): repository.update_rom(rom) uow.commit() - kodi.notify('Imported ROMCollection NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify(kodi.translate(41033).format(NFO_FileName.getPath())) AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') @@ -656,11 +664,11 @@ def cmd_rom_save_nfo_file(args): try: rom.export_to_NFO_file(NFO_FileName) except: - kodi.notify_warn('Exception writing NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify_warn(kodi.translate(41042).format(NFO_FileName.getPath())) logger.error("cmd_rom_save_nfo_file() Exception writing'{0}'".format(NFO_FileName.getPath())) else: logger.debug("cmd_rom_save_nfo_file() Created '{0}'".format(NFO_FileName.getPath())) - kodi.notify('Exported ROMCollection NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify(kodi.translate(41034).format(NFO_FileName.getPath())) AppMediator.sync_cmd('ROM_EDIT_METADATA', args) @@ -673,34 +681,34 @@ def cmd_manage_rom_tags(args): available_tags = repository.find_all_tags() options = collections.OrderedDict() - options['ADD_TAG'] = "[Add tag]" + options['ADD_TAG'] = kodi.translate(42023) if available_tags is not None and len(available_tags) > 0: options.update({value: key for key, value in available_tags.items()}) selected_option = 'ADD_TAG' did_tag_change = False while selected_option is not None: - s = 'Manage Tags' + s = kodi.translate(41097) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: continue if selected_option == 'ADD_TAG': - tag = kodi.dialog_keyboard('Tag') + tag = kodi.dialog_keyboard(kodi.translate(40814)) if tag is not None: did_tag_change = True logger.debug(f'cmd_manage_rom_tags() Adding tag "{tag}"') - kodi.notify(f'Adding tag "{tag}') + kodi.notify(kodi.translate(41029).format(tag)) tag_id = repository.insert_tag(tag) options[tag_id] = tag continue - if not kodi.dialog_yesno(f'Remove tag "{options[selected_option]}"?'): + if not kodi.dialog_yesno(kodi.translate(41057).format(options[selected_option])): continue did_tag_change = True logger.debug(f'cmd_manage_rom_tags() Remove tag {options[selected_option]}') - kodi.notify(f'Removing tag "{options[selected_option]}"') + kodi.notify(kodi.translate(41027).format(options[selected_option])) del options[selected_option] repository.delete_tag(selected_option) @@ -725,7 +733,7 @@ def cmd_add_rom(args): if grand_parent_category is not None: options_dialog = kodi.ListDialog() - selected_option = options_dialog.select('Add ROM in?',[parent_category.get_name(), grand_parent_category.get_name()]) + selected_option = options_dialog.select(kodi.translate(41098),[parent_category.get_name(), grand_parent_category.get_name()]) if selected_option > 0: parent_category = grand_parent_category @@ -738,12 +746,12 @@ def cmd_add_rom(args): path = io.FileName(file_path) rom_name = path.getBaseNoExt() - rom_name = kodi.dialog_keyboard("Name", rom_name) + rom_name = kodi.dialog_keyboard(kodi.translate(40815), rom_name) if rom_name is None: return dialog = kodi.ListDialog() - selected_idx = dialog.select('Select the platform', platforms.AKL_platform_list) + selected_idx = dialog.select(kodi.translate(41099), platforms.AKL_platform_list) platform = platforms.AKL_platform_list[selected_idx] rom_obj = ROM() @@ -768,5 +776,5 @@ def cmd_add_rom(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_TITLE_ID}) - kodi.notify(f"Created new standalone ROM '{rom_name}'") + kodi.notify(kodi.translate(41035).format(rom_name)) kodi.refresh_container() \ No newline at end of file diff --git a/resources/lib/commands/rom_launcher_commands.py b/resources/lib/commands/rom_launcher_commands.py index da23000a..ca4d1f88 100644 --- a/resources/lib/commands/rom_launcher_commands.py +++ b/resources/lib/commands/rom_launcher_commands.py @@ -50,12 +50,12 @@ def cmd_manage_romcollection_launchers(args): default_launcher_name = default_launcher.get_name() if default_launcher is not None else 'None' options = collections.OrderedDict() - options['ADD_LAUNCHER'] = 'Add new launcher' - options['EDIT_LAUNCHER'] = 'Edit launcher' - options['REMOVE_LAUNCHER'] = 'Remove launcher' - options['SET_DEFAULT_LAUNCHER'] = 'Set default launcher: "{}"'.format(default_launcher_name) + options['ADD_LAUNCHER'] = kodi.translate(42026) + options['EDIT_LAUNCHER'] = kodi.translate(42027) + options['REMOVE_LAUNCHER'] = kodi.translate(42028) + options['SET_DEFAULT_LAUNCHER'] = kodi.translate(42029).format(default_launcher_name) - s = 'Manage Launchers for "{}"'.format(romcollection.get_name()) + s = kodi.translate(41100).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -80,15 +80,15 @@ def cmd_manage_rom_launchers(args): launchers = rom.get_launchers() default_launcher = next((l for l in launchers if l.is_default()), launchers[0]) if len(launchers) > 0 else None - default_launcher_name = default_launcher.get_name() if default_launcher is not None else 'None' + default_launcher_name = default_launcher.get_name() if default_launcher is not None else kodi.translate(20010) options = collections.OrderedDict() - options['ADD_ROM_LAUNCHER'] = 'Add new launcher' - options['EDIT_ROM_LAUNCHER'] = 'Edit launcher' - options['REMOVE_ROM_LAUNCHER'] = 'Remove launcher' - options['SET_DEFAULT_ROM_LAUNCHER'] = f'Set default launcher: "{default_launcher_name}"' + options['ADD_ROM_LAUNCHER'] = kodi.translate(42026) + options['EDIT_ROM_LAUNCHER'] = kodi.translate(42027) + options['REMOVE_ROM_LAUNCHER'] = kodi.translate(42028) + options['SET_DEFAULT_ROM_LAUNCHER'] = kodi.translate(42029).format(default_launcher_name) - s = f'Manage Launchers for "{rom.get_name()}"' + s = kodi.translate(41100).format(rom.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -117,7 +117,7 @@ def cmd_add_rom_launchers(args): for addon in addons: options[addon] = addon.get_name() - s = 'Choose launcher to associate' + s = kodi.translate(41101) selected_option:AelAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -148,7 +148,7 @@ def cmd_add_romcollection_launchers(args): for addon in addons: options[addon] = addon.get_name() - s = 'Choose launcher to associate' + s = kodi.translate(41101) selected_option:AelAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -174,7 +174,7 @@ def cmd_edit_romcollection_launchers(args): launchers = romcollection.get_launchers() if len(launchers) == 0: - kodi.notify('No launchers configured for this romcollection!') + kodi.notify(kodi.translate(41003)) AppMediator.async_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return @@ -182,7 +182,7 @@ def cmd_edit_romcollection_launchers(args): for launcher in launchers: options[launcher] = launcher.get_name() - s = 'Choose launcher to edit' + s = kodi.translate(41102) selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -206,7 +206,7 @@ def cmd_edit_rom_launcher(args): launchers = rom.get_launchers() if len(launchers) == 0: - kodi.notify('No launchers configured for this ROM!') + kodi.notify(kodi.translate(41002)) AppMediator.async_cmd('EDIT_ROM_LAUNCHERS', args) return @@ -214,7 +214,7 @@ def cmd_edit_rom_launcher(args): for launcher in launchers: options[launcher] = launcher.get_name() - s = 'Choose launcher to edit' + s = kodi.translate(41102) selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -238,7 +238,7 @@ def cmd_remove_romcollection_launchers(args): launchers = romcollection.get_launchers() if len(launchers) == 0: - kodi.notify('No launchers configured for this romcollection!') + kodi.notify(kodi.translate(41003)) AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return @@ -246,7 +246,7 @@ def cmd_remove_romcollection_launchers(args): for launcher in launchers: options[launcher] = launcher.get_name() - s = 'Choose launcher to remove' + s = kodi.translate(41103) selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -257,7 +257,7 @@ def cmd_remove_romcollection_launchers(args): # >> Execute subcommand. May be atomic, maybe a submenu. logger.debug('REMOVE_LAUNCHER: cmd_remove_romcollection_launchers() Selected {}'.format(selected_option.get_id())) - if not kodi.dialog_yesno('Are you sure to delete launcher "{}"'.format(selected_option.get_name())): + if not kodi.dialog_yesno(kodi.translate(41059).format(selected_option.get_name())): logger.debug('REMOVE_LAUNCHER: cmd_remove_romcollection_launchers() Cancelled operation.') AppMediator.async_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return @@ -266,7 +266,7 @@ def cmd_remove_romcollection_launchers(args): logger.info(f'Removed launcher#{selected_option.get_id()}') uow.commit() - kodi.notify(f'Removed launcher "{selected_option.get_name()}"') + kodi.notify(kodi.translate(41004).format(selected_option.get_name())) AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) @AppMediator.register('REMOVE_ROM_LAUNCHER') @@ -280,7 +280,7 @@ def cmd_remove_rom_launchers(args): launchers = rom.get_launchers() if len(launchers) == 0: - kodi.notify('No launchers configured for this ROM!') + kodi.notify(kodi.translate(41002)) AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) return @@ -288,7 +288,7 @@ def cmd_remove_rom_launchers(args): for launcher in launchers: options[launcher] = launcher.get_name() - s = 'Choose launcher to remove' + s = kodi.translate(41103) selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -299,7 +299,7 @@ def cmd_remove_rom_launchers(args): # >> Execute subcommand. May be atomic, maybe a submenu. logger.debug(f'Selected {selected_option.get_id()}') - if not kodi.dialog_yesno(f'Are you sure to delete launcher "{selected_option.get_name()}"'): + if not kodi.dialog_yesno(kodi.translate(41059).format(selected_option.get_name())): logger.debug('Cancelled operation.') AppMediator.async_cmd('EDIT_ROM_LAUNCHERS', args) return @@ -308,7 +308,7 @@ def cmd_remove_rom_launchers(args): logger.info(f'Removed launcher#{selected_option.get_id()}') uow.commit() - kodi.notify(f'Removed launcher "{selected_option.get_name()}"') + kodi.notify(kodi.translate(41004).format(selected_option.get_name())) AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) @AppMediator.register('SET_DEFAULT_LAUNCHER') @@ -322,7 +322,7 @@ def cmd_set_default_romcollection_launchers(args): launchers = romcollection.get_launchers() if len(launchers) == 0: - kodi.notify('No launchers configured for this romcollection!') + kodi.notify(kodi.translate(41003)) AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return @@ -330,7 +330,7 @@ def cmd_set_default_romcollection_launchers(args): for launcher in launchers: options[launcher.get_id()] = launcher.get_name() - s = 'Choose launcher to set as default' + s = kodi.translate(41104) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -358,7 +358,7 @@ def cmd_set_default_rom_launchers(args): launchers = rom.get_launchers() if len(launchers) == 0: - kodi.notify('No launchers configured for this ROM!') + kodi.notify(kodi.translate(41002)) AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) return @@ -366,7 +366,7 @@ def cmd_set_default_rom_launchers(args): for launcher in launchers: options[launcher.get_id()] = launcher.get_name() - s = 'Choose launcher to set as default' + s = kodi.translate(41104) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -406,7 +406,7 @@ def cmd_execute_rom_with_launcher(args): if launchers is None or len(launchers) == 0: logger.warning(f'No launcher configured for ROM {rom.get_name()}') if not settings.getSettingAsBool('fallback_to_retroplayer'): - kodi.notify_warn('No launcher configured.') + kodi.notify_warn(kodi.translate(41001)) return logger.info('Automatic fallback to Retroplayer as launcher applied.') @@ -423,7 +423,7 @@ def cmd_execute_rom_with_launcher(args): if launcher.is_default(): preselected = launcher dialog = kodi.OrdDictionaryDialog() - selected_launcher = dialog.select('Choose launcher', launcher_options,preselect=preselected) + selected_launcher = dialog.select(kodi.translate(41105), launcher_options,preselect=preselected) if selected_launcher is None: return diff --git a/resources/lib/commands/rom_scanner_commands.py b/resources/lib/commands/rom_scanner_commands.py index b9e57347..19982896 100644 --- a/resources/lib/commands/rom_scanner_commands.py +++ b/resources/lib/commands/rom_scanner_commands.py @@ -47,11 +47,11 @@ def cmd_manage_romcollection_scanners(args): romcollection = repository.find_romcollection(romcollection_id) options = collections.OrderedDict() - options['ADD_SCANNER'] = 'Add new scanner' - options['EDIT_SCANNER'] = 'Edit scanner' - options['REMOVE_SCANNER'] = 'Remove scanner' + options['ADD_SCANNER'] = kodi.translate(42080) + options['EDIT_SCANNER'] = kodi.translate(42081) + options['REMOVE_SCANNER'] = kodi.translate(42082) - s = 'Manage ROM scanners for "{}"'.format(romcollection.get_name()) + s = kodi.translate(41106).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -80,7 +80,7 @@ def cmd_add_romcollection_scanner(args): for addon in addons: options[addon] = addon.get_name() - s = 'Choose scanner to associate' + s = kodi.translate(41107) selected_option:AelAddon = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -93,7 +93,7 @@ def cmd_add_romcollection_scanner(args): logger.debug('ADD_SCANNER: cmd_add_romcollection_scanner() Selected {}'.format(selected_option.get_id())) scanner_addon = ROMCollectionScanner(selected_option, {}) - kodi.notify('Preparing scanner') + kodi.notify(kodi.translate(40980)) kodi.run_script( selected_option.get_addon_id(), scanner_addon.get_configure_command(romcollection)) @@ -110,7 +110,7 @@ def cmd_edit_romcollection_scanners(args): scanners = romcollection.get_scanners() if len(scanners) == 0: - kodi.notify('No scanners configured for this romcollection!') + kodi.notify(kodi.translate(40982)) AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) return @@ -118,7 +118,7 @@ def cmd_edit_romcollection_scanners(args): for scanner in scanners: options[scanner] = scanner.get_name() - s = 'Choose scanner to edit' + s = kodi.translate(41108) selected_option:ROMCollectionScanner = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -130,7 +130,7 @@ def cmd_edit_romcollection_scanners(args): # >> Execute subcommand. May be atomic, maybe a submenu. logger.debug('EDIT_SCANNER: cmd_edit_romcollection_scanners() Selected {}'.format(selected_option.get_id())) - kodi.notify('Preparing scanner') + kodi.notify(kodi.translate(40980)) kodi.run_script( selected_option.addon.get_addon_id(), selected_option.get_configure_command(romcollection)) @@ -146,7 +146,7 @@ def cmd_remove_romcollection_scanner(args): scanners = romcollection.get_scanners() if len(scanners) == 0: - kodi.notify('No scanners configured for this romcollection!') + kodi.notify(kodi.translate(40982)) AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) return @@ -154,7 +154,7 @@ def cmd_remove_romcollection_scanner(args): for scanner in scanners: options[scanner] = scanner.get_name() - s = 'Choose scanner to remove' + s = kodi.translate(41109) selected_option:ROMCollectionScanner = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: @@ -165,7 +165,7 @@ def cmd_remove_romcollection_scanner(args): # >> Execute subcommand. May be atomic, maybe a submenu. logger.debug('REMOVE_SCANNER: cmd_remove_romcollection_scanner() Selected {}'.format(selected_option.get_id())) - if not kodi.dialog_yesno('Are you sure to delete ROM scanner "{}"'.format(selected_option.get_name())): + if not kodi.dialog_yesno(kodi.translate(41060).format(selected_option.get_name())): logger.debug('REMOVE_SCANNER: cmd_remove_romcollection_scanner() Cancelled operation.') AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) return @@ -190,7 +190,7 @@ def cmd_execute_rom_scanner(args): scanners = romcollection.get_scanners() if scanners is None or len(scanners) == 0: - kodi.notify_warn('No ROM scanners configured.') + kodi.notify_warn(kodi.translate(41000)) return selected_scanner = scanners[0] @@ -199,7 +199,7 @@ def cmd_execute_rom_scanner(args): for scanner in scanners: scanner_options[scanner] = scanner.get_name() dialog = kodi.OrdDictionaryDialog() - selected_scanner = dialog.select('Choose ROM scanner', scanner_options) + selected_scanner = dialog.select(kodi.translate(41110), scanner_options) if selected_scanner is None: # >> Exits context menu @@ -208,7 +208,7 @@ def cmd_execute_rom_scanner(args): return logger.info('SCAN_ROMS: selected scanner "{}"'.format(selected_scanner.get_name())) - kodi.notify('Preparing scanner') + kodi.notify(kodi.translate(40980)) kodi.run_script( selected_scanner.addon.get_addon_id(), selected_scanner.get_scan_command(romcollection)) \ No newline at end of file diff --git a/resources/lib/commands/rom_scraper_commands.py b/resources/lib/commands/rom_scraper_commands.py index 46211811..6ece2257 100644 --- a/resources/lib/commands/rom_scraper_commands.py +++ b/resources/lib/commands/rom_scraper_commands.py @@ -45,7 +45,7 @@ def cmd_scrape_romcollection(args): scraper_settings:ScraperSettings = ScraperSettings.from_addon_settings() - dialog_title = f'Scrape collection "{collection.get_name()}" ROMs' + dialog_title = kodi.translate(41124).format(collection.get_name()) selected_addon = _select_scraper(uow, dialog_title, scraper_settings) if selected_addon is None: # >> Exits context menu @@ -76,7 +76,7 @@ def cmd_scrape_rom(args): scraper_settings:ScraperSettings = ScraperSettings.from_addon_settings() - dialog_title = f'Scrape ROM "{rom.get_name()}"' + dialog_title = kodi.translate(41123).format(rom.get_name()) selected_addon = _select_scraper(uow, dialog_title, scraper_settings) if selected_addon is None: # >> Exits context menu @@ -103,30 +103,30 @@ def cmd_scrape_roms_in_romcollection(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - addon_repository = AelAddonRepository(uow) + addon_repository = AelAddonRepository(uow) collection_repository = ROMCollectionRepository(uow) - collection = collection_repository.find_romcollection(romcollection_id) - addon = addon_repository.find(scraper_id) - selected_addon = ScraperAddon(addon, scraper_settings) + collection = collection_repository.find_romcollection(romcollection_id) + addon = addon_repository.find(scraper_id) + selected_addon = ScraperAddon(addon, scraper_settings) - assets_to_scrape = g_assetFactory.get_asset_list_by_IDs(scraper_settings.asset_IDs_to_scrape) - metadata_to_scrape = [constants.METADATA_DESCRIPTIONS[meta_id] for meta_id in scraper_settings.metadata_IDs_to_scrape] + assets_to_scrape = g_assetFactory.get_asset_list_by_IDs(scraper_settings.asset_IDs_to_scrape) + metadata_to_scrape = [constants.METADATA_DESCRIPTIONS[meta_id] for meta_id in scraper_settings.metadata_IDs_to_scrape] options = collections.OrderedDict() - options['SCRAPER_METADATA_POLICY'] = 'Metadata scan policy: "{}"'.format(kodi.translate(scraper_settings.scrape_metadata_policy)) - options['SCRAPER_ASSET_POLICY'] = 'Asset scan policy: "{}"'.format(kodi.translate(scraper_settings.scrape_assets_policy)) - options['SCRAPER_SEARCH_TERM_MODE'] = f'Search term mode: "{kodi.translate(scraper_settings.search_term_mode)}"' - options['SCRAPER_GAME_SELECTION_MODE'] = 'Game selection mode: "{}"'.format(kodi.translate(scraper_settings.game_selection_mode)) - options['SCRAPER_ASSET_SELECTION_MODE'] = 'Asset selection mode: "{}"'.format(kodi.translate(scraper_settings.asset_selection_mode)) - options['SCRAPER_META_TO_SCRAPE'] = 'Metadata to scrape: "{}"'.format(', '.join(metadata_to_scrape)) - options['SCRAPER_ASSETS_TO_SCRAPE'] = 'Assets to scrape: "{}"'.format(', '.join([a.plural for a in assets_to_scrape])) - options['SCRAPER_OVERWRITE_META_MODE'] = 'Overwrite existing metadata: "{}"'.format('Yes' if scraper_settings.overwrite_existing_meta else 'No') - options['SCRAPER_OVERWRITE_ASSETS_MODE'] = 'Overwrite existing assets/files: "{}"'.format('Yes' if scraper_settings.overwrite_existing_assets else 'No') - options['SCRAPER_IGNORE_TITLES_MODE'] = 'Ignore scraped titles: "{}"'.format('Yes' if scraper_settings.ignore_scrap_title else 'No') - options['SCRAPE'] = 'Scrape' + options['SCRAPER_METADATA_POLICY'] = kodi.translate(41115).format(kodi.translate(scraper_settings.scrape_metadata_policy)) + options['SCRAPER_ASSET_POLICY'] = kodi.translate(41116).format(kodi.translate(scraper_settings.scrape_assets_policy)) + options['SCRAPER_SEARCH_TERM_MODE'] = kodi.translate(41117).format(kodi.translate(scraper_settings.search_term_mode)) + options['SCRAPER_GAME_SELECTION_MODE'] = kodi.translate(41118).format(kodi.translate(scraper_settings.game_selection_mode)) + options['SCRAPER_ASSET_SELECTION_MODE'] = kodi.translate(41119).format(kodi.translate(scraper_settings.asset_selection_mode)) + options['SCRAPER_META_TO_SCRAPE'] = kodi.translate(42030).format(', '.join(metadata_to_scrape)) + options['SCRAPER_ASSETS_TO_SCRAPE'] = kodi.translate(42031).format(', '.join([a.plural for a in assets_to_scrape])) + options['SCRAPER_OVERWRITE_META_MODE'] = kodi.translate(42032).format(kodi.translate(42035) if scraper_settings.overwrite_existing_meta else kodi.translate(42036)) + options['SCRAPER_OVERWRITE_ASSETS_MODE'] = kodi.translate(42033).format(kodi.translate(42035) if scraper_settings.overwrite_existing_assets else kodi.translate(42036)) + options['SCRAPER_IGNORE_TITLES_MODE'] = kodi.translate(42034).format(kodi.translate(42035) if scraper_settings.ignore_scrap_title else kodi.translate(42036)) + options['SCRAPE'] = kodi.translate(40881) - dialog_title = f'Scrape collection "{collection.get_name()}" ROMs with "{selected_addon.get_name()}"' + dialog_title = kodi.translate(41111).format(collection.get_name(), selected_addon.get_name()) selected_option = kodi.OrdDictionaryDialog().select(dialog_title, options, preselect='SCRAPE') if selected_option is None: logger.debug('cmd_scrape_roms_in_romcollection() Selected None. Closing context menu') @@ -142,7 +142,7 @@ def cmd_scrape_roms_in_romcollection(args): _check_collection_unset_asset_dirs(collection, scraper_settings) selected_addon.set_scraper_settings(scraper_settings) - kodi.notify('Preparing scraper') + kodi.notify(kodi.translate(40979)) kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command_for_collection(collection)) @@ -167,19 +167,19 @@ def cmd_scrape_rom_with_settings(args): metadata_to_scrape = [constants.METADATA_DESCRIPTIONS[meta_id] for meta_id in scraper_settings.metadata_IDs_to_scrape] options = collections.OrderedDict() - options['SCRAPER_METADATA_POLICY'] = 'Metadata scan policy: "{}"'.format(kodi.translate(scraper_settings.scrape_metadata_policy)) - options['SCRAPER_ASSET_POLICY'] = 'Asset scan policy: "{}"'.format(kodi.translate(scraper_settings.scrape_assets_policy)) - options['SCRAPER_SEARCH_TERM_MODE'] = f'Search term mode: "{kodi.translate(scraper_settings.search_term_mode)}"' - options['SCRAPER_GAME_SELECTION_MODE'] = 'Game selection mode: "{}"'.format(kodi.translate(scraper_settings.game_selection_mode)) - options['SCRAPER_ASSET_SELECTION_MODE'] = 'Asset selection mode: "{}"'.format(kodi.translate(scraper_settings.asset_selection_mode)) - options['SCRAPER_META_TO_SCRAPE'] = 'Metadata to scrape: "{}"'.format(', '.join(metadata_to_scrape)) - options['SCRAPER_ASSETS_TO_SCRAPE'] = 'Assets to scrape: "{}"'.format(', '.join([a.plural for a in assets_to_scrape])) - options['SCRAPER_OVERWRITE_META_MODE'] = 'Overwrite existing metadata: "{}"'.format('Yes' if scraper_settings.overwrite_existing_meta else 'No') - options['SCRAPER_OVERWRITE_ASSETS_MODE'] = 'Overwrite existing assets/files: "{}"'.format('Yes' if scraper_settings.overwrite_existing_assets else 'No') - options['SCRAPER_IGNORE_TITLES_MODE'] = 'Ignore scraped titles: "{}"'.format('Yes' if scraper_settings.ignore_scrap_title else 'No') - options['SCRAPE'] = 'Scrape' + options['SCRAPER_METADATA_POLICY'] = kodi.translate(41115).format(kodi.translate(scraper_settings.scrape_metadata_policy)) + options['SCRAPER_ASSET_POLICY'] = kodi.translate(41116).format(kodi.translate(scraper_settings.scrape_assets_policy)) + options['SCRAPER_SEARCH_TERM_MODE'] = kodi.translate(41117).format(kodi.translate(scraper_settings.search_term_mode)) + options['SCRAPER_GAME_SELECTION_MODE'] = kodi.translate(41118).format(kodi.translate(scraper_settings.game_selection_mode)) + options['SCRAPER_ASSET_SELECTION_MODE'] = kodi.translate(41119).format(kodi.translate(scraper_settings.asset_selection_mode)) + options['SCRAPER_META_TO_SCRAPE'] = kodi.translate(42030).format(', '.join(metadata_to_scrape)) + options['SCRAPER_ASSETS_TO_SCRAPE'] = kodi.translate(42031).format(', '.join([a.plural for a in assets_to_scrape])) + options['SCRAPER_OVERWRITE_META_MODE'] = kodi.translate(42032).format(kodi.translate(42035) if scraper_settings.overwrite_existing_meta else kodi.translate(42036)) + options['SCRAPER_OVERWRITE_ASSETS_MODE'] = kodi.translate(42033).format(kodi.translate(42035) if scraper_settings.overwrite_existing_assets else kodi.translate(42036)) + options['SCRAPER_IGNORE_TITLES_MODE'] = kodi.translate(42034).format(kodi.translate(42035) if scraper_settings.ignore_scrap_title else kodi.translate(42036)) + options['SCRAPE'] = kodi.translate(40881) - s = f'Scrape ROM "{rom.get_name()}" with "{selected_addon.get_name()}' + s = kodi.translate(41112).format(rom.get_name(),selected_addon.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect='SCRAPE') if selected_option is None: logger.debug('cmd_scrape_rom_with_settings() Selected None. Closing context menu') @@ -194,7 +194,7 @@ def cmd_scrape_rom_with_settings(args): # >> Execute scraper selected_addon.set_scraper_settings(scraper_settings) - kodi.notify('Preparing scraper') + kodi.notify(kodi.translate(40979)) kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) @@ -215,7 +215,7 @@ def cmd_scrape_rom_metadata(args): scraper_settings.game_selection_mode = constants.SCRAPE_MANUAL scraper_settings.overwrite_existing_meta = True - selected_addon = _select_scraper(uow, 'Scrape ROM metadata', scraper_settings) + selected_addon = _select_scraper(uow, kodi.translate(41122), scraper_settings) if selected_addon is None: # >> Exits context menu logger.debug('SCRAPE_ROM_METADATA: Selected None. Closing context menu') @@ -230,14 +230,14 @@ def cmd_scrape_rom_metadata(args): if selected_addon.is_metadata_supported(metadata_id): options[metadata_id] = constants.METADATA_DESCRIPTIONS[metadata_id] - selected_options = kodi.MultiSelectDialog().select('Metadata to scrape', options, preselected=scraper_settings.metadata_IDs_to_scrape) + selected_options = kodi.MultiSelectDialog().select(kodi.translate(41113), options, preselected=scraper_settings.metadata_IDs_to_scrape) if selected_options is not None: scraper_settings.metadata_IDs_to_scrape = selected_options # >> Execute scraper selected_addon.set_scraper_settings(scraper_settings) - kodi.notify('Preparing scraper') + kodi.notify(kodi.translate(40979)) kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) @@ -263,7 +263,7 @@ def cmd_scrape_rom_asset(args): scraper_settings.asset_IDs_to_scrape = [asset_id] scraper_settings.overwrite_existing_assets = True - selected_addon = _select_scraper(uow, 'Scrape ROM {} asset'.format(asset_to_scrape.name), scraper_settings) + selected_addon = _select_scraper(uow, kodi.translate(41121).format(asset_to_scrape.name), scraper_settings) if selected_addon is None: # >> Exits context menu logger.debug('SCRAPE_ROM_ASSET: cmd_scrape_rom_asset() Selected None. Closing context menu') @@ -273,7 +273,7 @@ def cmd_scrape_rom_asset(args): # >> Execute scraper logger.debug('SCRAPE_ROM_ASSET: Selected scraper#{}'.format(selected_addon.get_name())) - kodi.notify('Preparing scraper') + kodi.notify(kodi.translate(40979)) kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) @@ -293,7 +293,7 @@ def cmd_scrape_rom_assets(args): scraper_settings.search_term_mode = constants.SCRAPE_MANUAL scraper_settings.asset_selection_mode = constants.SCRAPE_MANUAL - selected_addon = _select_scraper(uow, 'Scrape ROM assets', scraper_settings) + selected_addon = _select_scraper(uow, kodi.translate(41120), scraper_settings) if selected_addon is None: # >> Exits context menu logger.debug('SCRAPE_ROM_ASSETS: Selected None. Closing context menu') @@ -307,18 +307,18 @@ def cmd_scrape_rom_assets(args): options = collections.OrderedDict() for asset_option in asset_options: if selected_addon.is_asset_supported(asset_option.id): - options[asset_option.id] = asset_option.name + options[asset_option.id] = kodi.translate(asset_option.name_id) selected_options = kodi.MultiSelectDialog().select( - 'Assets to scrape', options, preselected=scraper_settings.asset_IDs_to_scrape) + kodi.translate(41114), options, preselected=scraper_settings.asset_IDs_to_scrape) if selected_options is not None: scraper_settings.asset_IDs_to_scrape = selected_options - scraper_settings.overwrite_existing = kodi.dialog_yesno('Overwrite existing assets settings?') + scraper_settings.overwrite_existing_assets = kodi.dialog_yesno(kodi.translate(41061)) selected_addon.set_scraper_settings(scraper_settings) - kodi.notify('Preparing scraper') + kodi.notify(kodi.translate(40979)) # >> Execute scraper kodi.run_script( selected_addon.addon.get_addon_id(), @@ -332,12 +332,12 @@ def cmd_configure_scraper_metadata_policy(args): scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() - options[constants.SCRAPE_ACTION_NONE] = kodi.translate(constants.SCRAPE_ACTION_NONE) - options[constants.SCRAPE_POLICY_TITLE_ONLY] = kodi.translate(constants.SCRAPE_POLICY_TITLE_ONLY) + options[constants.SCRAPE_ACTION_NONE] = kodi.translate(constants.SCRAPE_ACTION_NONE) + options[constants.SCRAPE_POLICY_TITLE_ONLY] = kodi.translate(constants.SCRAPE_POLICY_TITLE_ONLY) options[constants.SCRAPE_POLICY_LOCAL_AND_SCRAPE] = kodi.translate(constants.SCRAPE_POLICY_LOCAL_AND_SCRAPE) - options[constants.SCRAPE_POLICY_SCRAPE_ONLY] = kodi.translate(constants.SCRAPE_POLICY_SCRAPE_ONLY) + options[constants.SCRAPE_POLICY_SCRAPE_ONLY] = kodi.translate(constants.SCRAPE_POLICY_SCRAPE_ONLY) - s = 'Metadata scan policy "{}"'.format(kodi.translate(scraper_settings.scrape_metadata_policy)) + s = kodi.translate(41115).format(kodi.translate(scraper_settings.scrape_metadata_policy)) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect=scraper_settings.scrape_metadata_policy) if selected_option is None: @@ -355,12 +355,12 @@ def cmd_configure_scraper_asset_policy(args): scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() - options[constants.SCRAPE_ACTION_NONE] = kodi.translate(constants.SCRAPE_ACTION_NONE) - options[constants.SCRAPE_POLICY_LOCAL_ONLY] = kodi.translate(constants.SCRAPE_POLICY_LOCAL_ONLY) + options[constants.SCRAPE_ACTION_NONE] = kodi.translate(constants.SCRAPE_ACTION_NONE) + options[constants.SCRAPE_POLICY_LOCAL_ONLY] = kodi.translate(constants.SCRAPE_POLICY_LOCAL_ONLY) options[constants.SCRAPE_POLICY_LOCAL_AND_SCRAPE] = kodi.translate(constants.SCRAPE_POLICY_LOCAL_AND_SCRAPE) - options[constants.SCRAPE_POLICY_SCRAPE_ONLY] = kodi.translate(constants.SCRAPE_POLICY_SCRAPE_ONLY) + options[constants.SCRAPE_POLICY_SCRAPE_ONLY] = kodi.translate(constants.SCRAPE_POLICY_SCRAPE_ONLY) - s = 'Asset scan policy "{}"'.format(kodi.translate(scraper_settings.scrape_assets_policy)) + s = kodi.translate(41116).format(kodi.translate(scraper_settings.scrape_assets_policy)) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect=scraper_settings.scrape_assets_policy) if selected_option is None: @@ -379,7 +379,7 @@ def cmd_configure_scraper_search_term_mode(args): options = collections.OrderedDict() options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) options[constants.SCRAPE_AUTOMATIC] = kodi.translate(constants.SCRAPE_AUTOMATIC) - s = 'Game search term mode "{}"'.format(kodi.translate(scraper_settings.search_term_mode)) + s = kodi.translate(41117).format(kodi.translate(scraper_settings.search_term_mode)) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect=scraper_settings.search_term_mode) if selected_option is None: @@ -399,7 +399,7 @@ def cmd_configure_scraper_game_selection_mode(args): options = collections.OrderedDict() options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) options[constants.SCRAPE_AUTOMATIC] = kodi.translate(constants.SCRAPE_AUTOMATIC) - s = 'Game selection mode "{}"'.format(kodi.translate(scraper_settings.game_selection_mode)) + s = kodi.translate(41118).format(kodi.translate(scraper_settings.game_selection_mode)) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect=scraper_settings.game_selection_mode) if selected_option is None: @@ -417,9 +417,9 @@ def cmd_configure_scraper_asset_selection_mode(args): scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() - options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) + options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) options[constants.SCRAPE_AUTOMATIC] = kodi.translate(constants.SCRAPE_AUTOMATIC) - s = 'Game selection mode "{}"'.format(kodi.translate(scraper_settings.asset_selection_mode)) + s = kodi.translate(41119).format(kodi.translate(scraper_settings.asset_selection_mode)) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect=scraper_settings.asset_selection_mode) if selected_option is None: @@ -442,7 +442,7 @@ def cmd_configure_scraper_metadata_to_scrape(args): if scraper_supported_metadata is None or metadata_id in scraper_supported_metadata: options[metadata_id] = constants.METADATA_DESCRIPTIONS[metadata_id] - selected_options = kodi.MultiSelectDialog().select('Metadata to scrape', options, preselected=scraper_settings.metadata_IDs_to_scrape) + selected_options = kodi.MultiSelectDialog().select(kodi.translate(41113), options, preselected=scraper_settings.metadata_IDs_to_scrape) if selected_options is None: AppMediator.sync_cmd(args['ret_cmd'], args) @@ -463,9 +463,9 @@ def cmd_configure_scraper_assets_to_scrape(args): options = collections.OrderedDict() for asset_option in asset_options: if supported_assets is None or asset_option.id in supported_assets: - options[asset_option.id] = asset_option.name + options[asset_option.id] = kodi.translate(asset_option.name_id) - selected_options = kodi.MultiSelectDialog().select('Assets to scrape', options, preselected=scraper_settings.asset_IDs_to_scrape) + selected_options = kodi.MultiSelectDialog().select(kodi.translate(41114), options, preselected=scraper_settings.asset_IDs_to_scrape) if selected_options is None: AppMediator.sync_cmd(args['ret_cmd'], args) @@ -533,8 +533,7 @@ def _check_collection_unset_asset_dirs(romcollection: ROMCollection, scraper_set scraper_settings.asset_IDs_to_scrape = enabled_asset_list if unconfigured_name_list: unconfigured_asset_srt = ', '.join(unconfigured_name_list) - msg = 'Assets directories not set: {0}. '.format(unconfigured_asset_srt) - msg = msg + 'Asset scanner will be disabled for this/those.' + msg = kodi.translate(41149).format(unconfigured_asset_srt) logger.debug(msg) kodi.dialog_OK(msg) return False diff --git a/resources/lib/commands/romcollection_commands.py b/resources/lib/commands/romcollection_commands.py index 453132ee..6666d496 100644 --- a/resources/lib/commands/romcollection_commands.py +++ b/resources/lib/commands/romcollection_commands.py @@ -44,7 +44,7 @@ def cmd_add_collection(args): if grand_parent_category is not None: options_dialog = kodi.ListDialog() - selected_option = options_dialog.select('Add ROM collection in?', [ + selected_option = options_dialog.select(kodi.translate(41125), [ parent_category.get_name(), grand_parent_category.get_name() ]) @@ -53,10 +53,10 @@ def cmd_add_collection(args): if selected_option > 0: parent_category = grand_parent_category - wizard = kodi.WizardDialog_Selection(None, 'platform', 'Select the platform', platforms.AKL_platform_list) + wizard = kodi.WizardDialog_Selection(None, 'platform', kodi.translate(41099), platforms.AKL_platform_list) wizard = kodi.WizardDialog_Dummy(wizard, 'm_name', '', _get_name_from_platform) - wizard = kodi.WizardDialog_Keyboard(wizard, 'm_name', 'Set the title of the launcher') - wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', 'Select asset/artwork directory', 0, '') + wizard = kodi.WizardDialog_Keyboard(wizard, 'm_name', kodi.translate(42037)) + wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', kodi.translate(42038), 0, '') romcollection = ROMCollection() entity_data = romcollection.get_data_dic() @@ -79,7 +79,7 @@ def cmd_add_collection(args): romcollection_repository.insert_romcollection(romcollection, parent_category) uow.commit() - kodi.notify('ROM Collection {0} created'.format(romcollection.get_name())) + kodi.notify(kodi.translate(41017).format(romcollection.get_name())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) @@ -100,7 +100,7 @@ def cmd_edit_romcollection(args): if romcollection_id is None: logger.warning('cmd_edit_romcollection(): No romcollection id supplied.') - kodi.notify_warn("Invalid parameters supplied.") + kodi.notify_warn(kodi.translate(40951)) return selected_option = None @@ -112,23 +112,23 @@ def cmd_edit_romcollection(args): cat_repository = CategoryRepository(uow) parent_id = romcollection.get_parent_id() category = cat_repository.find_category(romcollection.get_parent_id()) if parent_id is not None else None - category_name = 'None' if category is None else category.get_name() + category_name = kodi.translate(20010) if category is None else category.get_name() options = collections.OrderedDict() options['ROMCOLLECTION_EDIT_METADATA'] = kodi.translate(40853) options['ROMCOLLECTION_EDIT_ASSETS'] = kodi.translate(40854) options['ROMCOLLECTION_EDIT_DEFAULT_ASSETS'] = kodi.translate(40859) if romcollection.has_launchers(): - options['EDIT_ROMCOLLECTION_LAUNCHERS'] = 'Manage associated launchers' + options['EDIT_ROMCOLLECTION_LAUNCHERS'] = kodi.translate(42016) else: - options['ADD_LAUNCHER'] = 'Add new launcher' - options['ROMCOLLECTION_MANAGE_ROMS'] = 'Manage ROMs ...' - options['EDIT_ROMCOLLECTION_CATEGORY'] = f"Change Category: '{category_name}'" - options['EDIT_ROMCOLLECTION_STATUS'] = f'ROM Collection status: {romcollection.get_finished_str()}' - options['EXPORT_ROMCOLLECTION'] = 'Export ROM Collection XML configuration ...' - options['DELETE_ROMCOLLECTION'] = 'Delete ROM Collection' - - s = 'Select action for ROM Collection "{}"'.format(romcollection.get_name()) + options['ADD_LAUNCHER'] = kodi.translate(42026) + options['ROMCOLLECTION_MANAGE_ROMS'] = kodi.translate(42039) + options['EDIT_ROMCOLLECTION_CATEGORY'] = kodi.translate(42040).format(category_name) + options['EDIT_ROMCOLLECTION_STATUS'] = kodi.translate(42041).format(kodi.translate(romcollection.get_finished_str_code())) + options['EXPORT_ROMCOLLECTION'] = kodi.translate(42042) + options['DELETE_ROMCOLLECTION'] = kodi.translate(42043) + + s = kodi.translate(41126).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -153,24 +153,24 @@ def cmd_romcollection_metadata(args): romcollection = repository.find_romcollection(romcollection_id) plot_str = text.limit_string(romcollection.get_plot(), constants.PLOT_STR_MAXSIZE) - rating = romcollection.get_rating() if romcollection.get_rating() != -1 else 'not rated' + rating = romcollection.get_rating() if romcollection.get_rating() != -1 else kodi.translate(42021) NFO_FileName = romcollection.get_NFO_name() - NFO_found_str = 'NFO found' if NFO_FileName.exists() else 'NFO not found' + NFO_found_str = kodi.translate(42019) if NFO_FileName.exists() else kodi.translate(42020) options = collections.OrderedDict() - options['ROMCOLLECTION_EDIT_METADATA_TITLE'] = "Edit Title: '{0}'".format(romcollection.get_name()) - options['ROMCOLLECTION_EDIT_METADATA_PLATFORM'] = "Edit Platform: {}".format(romcollection.get_platform()) - options['ROMCOLLECTION_EDIT_METADATA_RELEASEYEAR'] = "Edit Release Year: {}".format(romcollection.get_releaseyear()) - options['ROMCOLLECTION_EDIT_METADATA_GENRE'] = "Edit Genre: '{0}'".format(romcollection.get_genre()) - options['ROMCOLLECTION_EDIT_METADATA_DEVELOPER'] = "Edit Developer: '{}'".format(romcollection.get_developer()) - options['ROMCOLLECTION_EDIT_METADATA_RATING'] = "Edit Rating: '{0}'".format(rating) - options['ROMCOLLECTION_EDIT_METADATA_PLOT'] = "Edit Plot: '{0}'".format(plot_str) - options['ROMCOLLECTION_EDIT_METADATA_BOXSIZE'] = "Edit Box Size: '{}'".format(romcollection.get_box_sizing()) - options['ROMCOLLECTION_IMPORT_NFO_FILE_DEFAULT'] = 'Import NFO file (default {0})'.format(NFO_found_str) - options['ROMCOLLECTION_IMPORT_NFO_FILE_BROWSE'] = 'Import NFO file (browse NFO file) ...' - options['ROMCOLLECTION_SAVE_NFO_FILE_DEFAULT'] = 'Save NFO file (default location)' - - s = 'Edit Launcher "{0}" metadata'.format(romcollection.get_name()) + options['ROMCOLLECTION_EDIT_METADATA_TITLE'] = kodi.translate(40863).format(romcollection.get_name()) + options['ROMCOLLECTION_EDIT_METADATA_PLATFORM'] = kodi.translate(40864).format(romcollection.get_platform()) + options['ROMCOLLECTION_EDIT_METADATA_RELEASEYEAR'] = kodi.translate(40865).format(romcollection.get_releaseyear()) + options['ROMCOLLECTION_EDIT_METADATA_GENRE'] = kodi.translate(40867).format(romcollection.get_genre()) + options['ROMCOLLECTION_EDIT_METADATA_DEVELOPER'] = kodi.translate(40868).format(romcollection.get_developer()) + options['ROMCOLLECTION_EDIT_METADATA_RATING'] = kodi.translate(40869).format(rating) + options['ROMCOLLECTION_EDIT_METADATA_PLOT'] = kodi.translate(40870).format(plot_str) + options['ROMCOLLECTION_EDIT_METADATA_BOXSIZE'] = kodi.translate(40875).format(romcollection.get_box_sizing()) + options['ROMCOLLECTION_IMPORT_NFO_FILE_DEFAULT'] = kodi.translate(40876).format(NFO_found_str) + options['ROMCOLLECTION_IMPORT_NFO_FILE_BROWSE'] = kodi.translate(40877) + options['ROMCOLLECTION_SAVE_NFO_FILE_DEFAULT'] = kodi.translate(40878) + + s = kodi.translate(41127).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Return recursively to parent menu. @@ -248,7 +248,7 @@ def cmd_romcollection_status(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) romcollection.change_finished_status() - kodi.dialog_OK('ROMCollection "{}" status is now {}'.format(romcollection.get_name(), romcollection.get_finished_str())) + kodi.dialog_OK(kodi.translate(41150).format(romcollection.get_name(), kodi.translate(romcollection.get_finished_str_code()))) repository.update_romcollection(romcollection) uow.commit() @@ -269,10 +269,10 @@ def cmd_romcollection_delete(args): romcollection_name = romcollection.get_name() if romcollection.num_roms() > 0: - question = 'ROMCollection "{0}" has {1} ROMs. '.format(romcollection_name, romcollection.num_roms()) + \ - 'Are you sure you want to delete it?' + question = kodi.translate(41069).format(romcollection_name, romcollection.num_roms()) + \ + kodi.translate(41066).format(romcollection_name) else: - question = 'Are you sure you want to delete "{}"?'.format(romcollection_name) + question = kodi.translate(41066).format(romcollection_name) ret = kodi.dialog_yesno(question) if not ret: return @@ -281,7 +281,7 @@ def cmd_romcollection_delete(args): repository.delete_romcollection(romcollection.get_id()) uow.commit() - kodi.notify('Deleted romcollection {0}'.format(romcollection_name)) + kodi.notify(kodi.translate(41018).format(romcollection_name)) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.async_cmd('CLEANUP_VIEWS') AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) @@ -296,7 +296,7 @@ def cmd_romcollection_metadata_title(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_str(romcollection, 'Title', romcollection.get_name, romcollection.set_name): + if editors.edit_field_by_str(romcollection, kodi.translate(40812), romcollection.get_name, romcollection.set_name): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) @@ -311,7 +311,7 @@ def cmd_romcollection_metadata_platform(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_list(romcollection, 'Platform', platforms.AKL_platform_list, + if editors.edit_field_by_list(romcollection, kodi.translate(40807), platforms.AKL_platform_list, romcollection.get_platform, romcollection.set_platform): repository.update_romcollection(romcollection) update_roms_too = kodi.dialog_yesno(kodi.translate(40955)) @@ -337,7 +337,7 @@ def cmd_romcollection_metadata_releaseyear(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_str(romcollection, 'Release Year', romcollection.get_releaseyear, romcollection.set_releaseyear): + if editors.edit_field_by_str(romcollection, kodi.translate(40803), romcollection.get_releaseyear, romcollection.set_releaseyear): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) @@ -352,7 +352,7 @@ def cmd_romcollection_metadata_genre(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_str(romcollection, 'Genre', romcollection.get_genre, romcollection.set_genre): + if editors.edit_field_by_str(romcollection, kodi.translate(40801), romcollection.get_genre, romcollection.set_genre): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) @@ -367,7 +367,7 @@ def cmd_romcollection_metadata_developer(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_str(romcollection, 'Developer', romcollection.get_developer, romcollection.set_developer): + if editors.edit_field_by_str(romcollection, kodi.translate(40802), romcollection.get_developer, romcollection.set_developer): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) @@ -397,7 +397,7 @@ def cmd_romcollection_metadata_plot(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_str(romcollection, 'Plot', romcollection.get_plot, romcollection.set_plot): + if editors.edit_field_by_str(romcollection, kodi.translate(40811), romcollection.get_plot, romcollection.set_plot): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) @@ -412,7 +412,7 @@ def cmd_romcollection_metadata_boxsize(args): repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) - if editors.edit_field_by_list(romcollection, 'Default box size', constants.BOX_SIZES, + if editors.edit_field_by_list(romcollection, kodi.translate(40816), constants.BOX_SIZES, romcollection.get_box_sizing, romcollection.set_box_sizing): repository.update_romcollection(romcollection) uow.commit() @@ -433,7 +433,7 @@ def cmd_romcollection_import_nfo_file(args): if romcollection.import_NFO_file(NFO_file): repository.update_romcollection(romcollection) uow.commit() - kodi.notify('Imported ROMCollection NFO file {0}'.format(NFO_file.getPath())) + kodi.notify(kodi.translate(41019).format(NFO_file.getPath())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) @@ -443,7 +443,7 @@ def cmd_romcollection_import_nfo_file(args): def cmd_romcollection_browse_import_nfo_file(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None - NFO_file = kodi.browse(text='Select NFO description file', mask='.nfo') + NFO_file = kodi.browse(text=kodi.translate(41143), mask='.nfo') logger.debug('cmd_romcollection_browse_import_nfo_file() Dialog().browse returned "{0}"'.format(NFO_file)) if not NFO_file: return NFO_FileName = io.FileName(NFO_file) @@ -457,7 +457,7 @@ def cmd_romcollection_browse_import_nfo_file(args): if romcollection.import_NFO_file(NFO_FileName): repository.update_romcollection(romcollection) uow.commit() - kodi.notify('Imported ROMCollection NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify(kodi.translate(41019).format(NFO_FileName.getPath())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) @@ -477,11 +477,11 @@ def cmd_romcollection_save_nfo_file(args): try: romcollection.export_to_NFO_file(NFO_FileName) except: - kodi.notify_warn('Exception writing NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify_warn(kodi.translate(41042).format(NFO_FileName.getPath())) logger.error("cmd_romcollection_save_nfo_file() Exception writing'{0}'".format(NFO_FileName.getPath())) else: logger.debug("cmd_romcollection_save_nfo_file() Created '{0}'".format(NFO_FileName.getPath())) - kodi.notify('Exported ROMCollection NFO file {0}'.format(NFO_FileName.getPath())) + kodi.notify(kodi.translate(41020).format(NFO_FileName.getPath())) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) @@ -504,7 +504,7 @@ def cmd_romcollection_change_category(args): options[root_category] = root_category.get_name() options.update({category:category.get_name() for category in all_categories}) - selected_option = kodi.OrdDictionaryDialog().select('Move to category', options) + selected_option = kodi.OrdDictionaryDialog().select(kodi.translate(41128), options) if selected_option is None: # >> Return recursively to parent menu. logger.debug('cmd_romcollection_change_category(): Selected NONE') @@ -512,7 +512,7 @@ def cmd_romcollection_change_category(args): return selected_category:Category = selected_option - if not kodi.dialog_yesno(f'Move "{romcollection.get_name()}" to category "{selected_category.get_name()}"?'): + if not kodi.dialog_yesno(kodi.translate(41065).format(romcollection.get_name(), selected_category.get_name())): logger.debug('cmd_romcollection_change_category(): Cancelled') AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) return @@ -521,7 +521,7 @@ def cmd_romcollection_change_category(args): repository.update_romcollection_parent_reference(romcollection, selected_category) uow.commit() - kodi.notify('Changed category for collection') + kodi.notify(kodi.translate(41021)) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': selected_category.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': previous_category_id}) @@ -551,9 +551,9 @@ def cmd_romcollection_export_xml(args): # --- If XML exists then warn user about overwriting it --- export_FN = io.FileName(dir_path).pjoin(romcollection_fn_str) if export_FN.exists(): - ret = kodi.dialog_yesno('Overwrite file {0}?'.format(export_FN.getPath())) + ret = kodi.dialog_yesno(kodi.translate(41052).format(export_FN.getPath())) if not ret: - kodi.notify_warn('Export of ROMCollection XML cancelled') + kodi.notify_warn(kodi.translate(41022)) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) return @@ -564,8 +564,8 @@ def cmd_romcollection_export_xml(args): try: romcollection.export_to_file(export_FN) except constants.AddonError as E: - kodi.notify_warn('{0}'.format(E)) + kodi.notify_warn(str(E)) else: - kodi.notify('Exported ROMCollection "{0}" XML config'.format(romcollection.get_name())) + kodi.notify(kodi.translate(41023).format(romcollection.get_name())) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) \ No newline at end of file diff --git a/resources/lib/commands/romcollection_roms_commands.py b/resources/lib/commands/romcollection_roms_commands.py index f01ff06e..3f49402a 100644 --- a/resources/lib/commands/romcollection_roms_commands.py +++ b/resources/lib/commands/romcollection_roms_commands.py @@ -43,7 +43,7 @@ def cmd_manage_roms(args): if romcollection_id is None: logger.warning('cmd_manage_roms(): No romcollection id supplied.') - kodi.notify_warn("Invalid parameters supplied.") + kodi.notify_warn(kodi.translate(40951)) return selected_option = None @@ -55,23 +55,23 @@ def cmd_manage_roms(args): has_roms = romcollection.has_roms() options = collections.OrderedDict() - options['SET_ROMS_DEFAULT_ARTWORK'] = 'Choose ROMs default artwork ...' - options['SET_ROMS_ASSET_DIRS'] = 'Manage ROMs asset directories ...' + options['SET_ROMS_DEFAULT_ARTWORK'] = kodi.translate(42044) + options['SET_ROMS_ASSET_DIRS'] = kodi.translate(42045) if romcollection.has_scanners(): - options['SCAN_ROMS'] = 'Scan for new ROMs' - options['REMOVE_DEAD_ROMS'] = 'Remove dead/missing ROMs' - options['EDIT_ROMCOLLECTION_SCANNERS'] = 'Configure ROM scanners' - else: options['ADD_SCANNER'] = 'Add new ROM scanner' + options['SCAN_ROMS'] = kodi.translate(42046) + options['REMOVE_DEAD_ROMS'] = kodi.translate(42047) + options['EDIT_ROMCOLLECTION_SCANNERS'] = kodi.translate(42048) + else: options['ADD_SCANNER'] = kodi.translate(42049) - options['IMPORT_ROMS'] = 'Import ROMs (files/metadata)' + options['IMPORT_ROMS'] = kodi.translate(42050) if has_roms: - options['EXPORT_ROMS'] = 'Export ROMs metadata to NFO files' - options['SCRAPE_ROMS'] = 'Scrape ROMs' - options['DELETE_ROMS_NFO'] = 'Delete ROMs NFO files' - options['CLEAR_ROMS'] = 'Clear ROMs from ROMCollection' + options['EXPORT_ROMS'] = kodi.translate(42051) + options['SCRAPE_ROMS'] = kodi.translate(42052) + options['DELETE_ROMS_NFO'] = kodi.translate(42053) + options['CLEAR_ROMS'] = kodi.translate(42054) - s = 'Manage ROM Collection "{}" ROMs'.format(romcollection.get_name()) + s = kodi.translate(41128).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -101,10 +101,12 @@ def cmd_set_roms_default_artwork(args): # >> Label is the string 'Choose asset for XXXX (currently YYYYY)' mapped_asset_info = romcollection.get_ROM_asset_mapping(default_asset_info) # --- Append to list of ListItems --- - options[default_asset_info] = 'Choose asset for {0} (currently {1})'.format(default_asset_info.name, mapped_asset_info.name) + options[default_asset_info] = kodi.translate(42055).format( + kodi.translate(default_asset_info.name_id), + kodi.translate(mapped_asset_info.name_id)) dialog = kodi.OrdDictionaryDialog() - selected_asset_info = dialog.select('Edit ROMs default Assets/Artwork', options) + selected_asset_info = dialog.select(kodi.translate(41077).format("ROM"), options) if selected_asset_info is None: # >> Return to parent menu. @@ -121,10 +123,11 @@ def cmd_set_roms_default_artwork(args): options = collections.OrderedDict() for mappable_asset_info in mappable_asset_list: # >> Label is the asset name (Icon, Fanart, etc.) - options[mappable_asset_info] = mappable_asset_info.name + options[mappable_asset_info] = kodi.translate(mappable_asset_info.name_id) dialog = kodi.OrdDictionaryDialog() - dialog_title_str = f'Edit {romcollection.get_object_name()} {selected_asset_info.name} mapped asset' + dialog_title_str = kodi.translate(41078).format(romcollection.get_object_name(), + kodi.translate(selected_asset_info.name_id)) new_selected_asset_info = dialog.select(dialog_title_str, options, mapped_asset_info) if new_selected_asset_info is None: @@ -135,8 +138,10 @@ def cmd_set_roms_default_artwork(args): logger.debug(f'Mapable selected {new_selected_asset_info.name}.') romcollection.set_mapped_ROM_asset(selected_asset_info, new_selected_asset_info) - kodi.notify('{0} {1} mapped to {2}'.format( - romcollection.get_object_name(), selected_asset_info.name, new_selected_asset_info.name + kodi.notify(kodi.translate(40983).format( + romcollection.get_object_name(), + kodi.translate(selected_asset_info.name_id), + kodi.translate(new_selected_asset_info.name_id) )) repository.update_romcollection(romcollection) @@ -159,13 +164,15 @@ def cmd_set_rom_asset_dirs(args): romcollection = repository.find_romcollection(romcollection_id) root_path = romcollection.get_assets_root_path() - list_items[AssetInfo()] = "Change root assets path: '{}'".format(root_path.getPath() if root_path else 'Undefined') + root_path_str = root_path.getPath() if root_path else kodi.translate(41158) + list_items[AssetInfo()] = kodi.translate(42083).format(root_path_str) for asset_info in assets: path = romcollection.get_asset_path(asset_info) - if path: list_items[asset_info] = "Change {} path: '{}'".format(asset_info.plural, path.getPath()) + if path: + list_items[asset_info] = kodi.translate(42084).format(asset_info.plural, path.getPath()) dialog = kodi.OrdDictionaryDialog() - selected_asset: AssetInfo = dialog.select('ROM Asset directories ', list_items) + selected_asset: AssetInfo = dialog.select(kodi.translate(41129), list_items) if selected_asset is None: AppMediator.sync_cmd('ROMCOLLECTION_MANAGE_ROMS', args) @@ -173,17 +180,17 @@ def cmd_set_rom_asset_dirs(args): # rootpath? if selected_asset.id == '': - dir_path = kodi.browse(type=0, text='Select root assets path', preselected_path=root_path.getPath() if root_path else None) + dir_path = kodi.browse(type=0, text=kodi.translate(41159), preselected_path=root_path.getPath() if root_path else None) if not dir_path or (root_path is not None and dir_path == root_path.getPath()): AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) return root_path = io.FileName(dir_path) - apply_to_all = kodi.dialog_yesno('Apply new path to all current asset paths?') + apply_to_all = kodi.dialog_yesno(kodi.translate(41062)) romcollection.set_assets_root_path(root_path, constants.ROM_ASSET_ID_LIST, create_default_subdirectories=apply_to_all) else: selected_asset_path = romcollection.get_asset_path(selected_asset) - dir_path = kodi.browse(type=0, text='Select {} path'.format(selected_asset.plural), preselected_path=selected_asset_path.getPath()) + dir_path = kodi.browse(type=0, text=kodi.translate(41160).format(selected_asset.plural), preselected_path=selected_asset_path.getPath()) if not dir_path or dir_path == selected_asset_path.getPath(): AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) return @@ -195,7 +202,7 @@ def cmd_set_rom_asset_dirs(args): # >> Check for duplicate paths and warn user. AppMediator.async_cmd('CHECK_DUPLICATE_ASSET_DIRS', args) - kodi.notify('Changed rom asset dir for {0} to {1}'.format(selected_asset.name, dir_path)) + kodi.notify(kodi.translate(40984).format(selected_asset.name, dir_path)) AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) @AppMediator.register('IMPORT_ROMS') @@ -210,10 +217,10 @@ def cmd_import_roms(args): romcollection = repository.find_romcollection(romcollection_id) options = collections.OrderedDict() - options['IMPORT_ROMS_NFO'] = 'Import ROMs metadata from NFO files' - options['IMPORT_ROMS_JSON'] = 'Import ROMs data from JSON files' + options['IMPORT_ROMS_NFO'] = kodi.translate(42056) + options['IMPORT_ROMS_JSON'] = kodi.translate(42057) - s = 'Import ROMs in ROMCollection "{}"'.format(romcollection.get_name()) + s = kodi.translate(41130).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -240,7 +247,7 @@ def cmd_import_roms_nfo(args): roms = repository.find_roms_by_romcollection(collection) pDialog = kodi.ProgressDialog() - pDialog.startProgress('Processing NFO files', num_steps=len(roms)) + pDialog.startProgress(kodi.translate(41153), num_steps=len(roms)) num_read_NFO_files = 0 step = 0 @@ -253,18 +260,18 @@ def cmd_import_roms_nfo(args): repository.update_rom(rom) # >> Save ROMs XML file / Launcher/timestamp saved at the end of function - pDialog.updateProgress(len(roms), 'Saving ROM JSON database ...') + pDialog.updateProgress(len(roms), kodi.translate(41154)) uow.commit() pDialog.close() - kodi.notify('Imported {0} NFO files'.format(num_read_NFO_files)) + kodi.notify(kodi.translate(40985).format(num_read_NFO_files)) AppMediator.async_cmd('IMPORT_ROMS', args) # --- Import ROM metadata from json config file --- @AppMediator.register('IMPORT_ROMS_JSON') def cmd_import_roms_json(args): romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - file_list = kodi.browse(text='Select ROMs JSON file',mask='.json',multiple=True) + file_list = kodi.browse(text=kodi.translate(41155),mask='.json',multiple=True) uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -293,12 +300,12 @@ def cmd_import_roms_json(args): if imported_rom.get_id() in existing_rom_ids: # >> ROM exists (by id). Overwrite? logger.debug('ROM found. Edit existing category.') - if kodi.dialog_yesno('ROM "{}" found in AKL database. Overwrite?'.format(imported_rom.get_name())): + if kodi.dialog_yesno(kodi.translate(41063).format(imported_rom.get_name())): roms_to_update.append(imported_rom) elif imported_rom.get_name() in existing_rom_names: # >> ROM exists (by name). Overwrite? logger.debug('ROM found. Edit existing category.') - if kodi.dialog_yesno('ROM "{}" found in AKL database. Overwrite?'.format(imported_rom.get_name())): + if kodi.dialog_yesno(kodi.translate(41063).format(imported_rom.get_name())): roms_to_update.append(imported_rom) else: logger.debug('Add new ROM {}'.format(imported_rom.get_name())) @@ -316,7 +323,7 @@ def cmd_import_roms_json(args): AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) - kodi.notify('Finished importing ROMS') + kodi.notify(kodi.translate(40978)) # --- Empty Launcher ROMs --- @AppMediator.register('CLEAR_ROMS') @@ -334,20 +341,20 @@ def cmd_clear_roms(args): # If collection is empty (no ROMs) do nothing num_roms = len([*roms]) if num_roms == 0: - kodi.dialog_OK('Collection has no ROMs. Nothing to do.') + kodi.dialog_OK(kodi.translate(41151)) return # Confirm user wants to delete ROMs - ret = kodi.dialog_yesno("Collection '{0}' has {1} ROMs. Are you sure you want to clear them " - "from this collection?".format(romcollection.get_name(), num_roms)) - if not ret: return + ret = kodi.dialog_yesno(kodi.translate(41142).format(romcollection.get_name(), num_roms)) + if not ret: + return # --- If there is a No-Intro XML DAT configured remove it --- # TODO fix # romcollection.reset_nointro_xmldata() # Confirm if the user wants to remove the ROMs also when linked to other collections. - delete_completely = kodi.dialog_yesno("Delete the ROMs completely from the AKL database and not collection only?") + delete_completely = kodi.dialog_yesno(kodi.translate(41064)) if not delete_completely: collection_repository.remove_all_roms_in_launcher(romcollection_id) else: @@ -356,4 +363,4 @@ def cmd_clear_roms(args): AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) - kodi.notify('Cleared ROMs from collection') + kodi.notify(kodi.translate(40977)) diff --git a/resources/lib/commands/search_commands.py b/resources/lib/commands/search_commands.py index 1f9de1b5..46f3fae2 100644 --- a/resources/lib/commands/search_commands.py +++ b/resources/lib/commands/search_commands.py @@ -35,20 +35,20 @@ def cmd_search(args): options = collections.OrderedDict() - options['SEARCH_BY_TITLE'] = 'By ROM Title' - options['SEARCH_BY_RELEASEYEAR'] = 'By Release Year' - options['SEARCH_BY_GENRE'] = 'By Genre' - options['SEARCH_BY_DEVELOPER'] = 'By Developer' - options['SEARCH_BY_RATING'] = 'By Rating' + options['SEARCH_BY_TITLE'] = kodi.translate(42058) + options['SEARCH_BY_RELEASEYEAR'] = kodi.translate(42059) + options['SEARCH_BY_GENRE'] = kodi.translate(42060) + options['SEARCH_BY_DEVELOPER'] = kodi.translate(42061) + options['SEARCH_BY_RATING'] = kodi.translate(42062) - selected_option = kodi.OrdDictionaryDialog().select('Search ROMs...',options) + selected_option = kodi.OrdDictionaryDialog().select(kodi.translate(41131),options) AppMediator.sync_cmd(selected_option, args) @AppMediator.register('SEARCH_BY_TITLE') def cmd_search_by_title(args): romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - search_string = kodi.dialog_keyboard('Enter the ROM Title search string...') + search_string = kodi.dialog_keyboard(kodi.translate(41136)) if search_string is None: return None params = { @@ -61,22 +61,22 @@ def cmd_search_by_title(args): @AppMediator.register('SEARCH_BY_GENRE') def cmd_search_by_genre(args): romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - _apply_search_query_by_options(romcollection_id, constants.META_GENRE_ID, 'Select a Genre...') + _apply_search_query_by_options(romcollection_id, constants.META_GENRE_ID, kodi.translate(41133)) @AppMediator.register('SEARCH_BY_RELEASEYEAR') def cmd_search_by_year(args): romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - _apply_search_query_by_options(romcollection_id, constants.META_YEAR_ID, 'Select a Release year...') + _apply_search_query_by_options(romcollection_id, constants.META_YEAR_ID, kodi.translate(41134)) @AppMediator.register('SEARCH_BY_DEVELOPER') def cmd_search_by_developer(args): romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - _apply_search_query_by_options(romcollection_id, constants.META_DEVELOPER_ID, 'Select a Developer...') + _apply_search_query_by_options(romcollection_id, constants.META_DEVELOPER_ID, kodi.translate(41135)) @AppMediator.register('SEARCH_BY_RATING') def cmd_search_by_rating(args): romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - _apply_search_query_by_options(romcollection_id, constants.META_RATING_ID, 'Select a Rating...') + _apply_search_query_by_options(romcollection_id, constants.META_RATING_ID, kodi.translate(41132)) def _apply_search_query_by_options(romcollection_id:str, filter_type:str, dialog_title: str): @@ -90,14 +90,17 @@ def _apply_search_query_by_options(romcollection_id:str, filter_type:str, dialog filter_values = repository.find_all_filter_values_in_romcollection(romcollection, constants.META_YEAR_ID) options = [] - options.append('[ Not Set ]') + options.append(f'[ {kodi.translate(42001)} ]') options.extend(filter_values) selected_index = kodi.ListDialog().select(dialog_title, options) - if selected_index is None: return None + if selected_index is None: + return None - if selected_index == 0: search_string = 'UNDEFINED' - else: search_string = options[selected_index] + if selected_index == 0: + search_string = 'UNDEFINED' + else: + search_string = options[selected_index] params = { 'filter': filter_type, diff --git a/resources/lib/commands/stats_commands.py b/resources/lib/commands/stats_commands.py index 15c4c245..fefcfd1b 100644 --- a/resources/lib/commands/stats_commands.py +++ b/resources/lib/commands/stats_commands.py @@ -60,7 +60,7 @@ def cmd_add_rom_to_favourites(args): rom_id: str = args['rom_id'] if 'rom_id' in args else None if rom_id is None: logger.warning('No rom id supplied.') - kodi.notify_warn("Invalid parameters supplied.") + kodi.notify_warn(kodi.translate(40951)) return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) diff --git a/resources/lib/commands/view_rendering_commands.py b/resources/lib/commands/view_rendering_commands.py index 1d3c3b21..8429a878 100644 --- a/resources/lib/commands/view_rendering_commands.py +++ b/resources/lib/commands/view_rendering_commands.py @@ -34,7 +34,7 @@ @AppMediator.register('RENDER_VIEWS') def cmd_render_views_data(args): - kodi.notify('Rendering all views') + kodi.notify(kodi.translate(40968)) uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -45,13 +45,13 @@ def cmd_render_views_data(args): _render_root_view(categories_repository, romcollections_repository, roms_repository, views_repository, render_sub_views=True) - kodi.notify('All views rendered') + kodi.notify(kodi.translate(40969)) kodi.refresh_container() @AppMediator.register('RENDER_CATEGORY_VIEW') def cmd_render_view_data(args): - kodi.notify('Rendering views') + kodi.notify(kodi.translate(40967)) category_id = args['category_id'] if 'category_id' in args else None render_recursive = args['render_recursive'] if 'render_recursive' in args else False @@ -70,7 +70,7 @@ def cmd_render_view_data(args): do_notification = not settings.getSettingAsBool("display_hide_rendering_notifications") if do_notification: - kodi.notify('Selected views rendered') + kodi.notify(kodi.translate(40966)) kodi.refresh_container() @AppMediator.register('RENDER_VIRTUAL_VIEWS') @@ -101,11 +101,11 @@ def cmd_render_virtual_views(args): vcategory = VirtualCategoryFactory.create(vcategory_id) if do_notification: - kodi.notify(f'Rendering virtual category "{vcategory.get_name()}"') + kodi.notify(kodi.translate(40970).format(vcategory.get_name())) _render_category_view(vcategory, categories_repository, romcollections_repository, roms_repository, views_repository) if do_notification: - kodi.notify('Virtual views rendered') + kodi.notify(kodi.translate(40965)) kodi.refresh_container() @AppMediator.register('RENDER_VCATEGORY_VIEWS') @@ -124,12 +124,12 @@ def cmd_render_vcategory(args): for vcategory_id in constants.VCATEGORIES: vcategory = VirtualCategoryFactory.create(vcategory_id) - if do_notification: - kodi.notify(f'Rendering virtual category "{vcategory.get_name()}"') + if do_notification: + kodi.notify(kodi.translate(40970).format(vcategory.get_name())) _render_category_view(vcategory, categories_repository, romcollections_repository, roms_repository, views_repository) - if do_notification: - kodi.notify(f'{vcategory.get_name()} view rendered') + if do_notification: + kodi.notify(kodi.translate(40971).format(vcategory.get_name())) kodi.refresh_container() @AppMediator.register('RENDER_VCATEGORY_VIEW') @@ -147,26 +147,28 @@ def cmd_render_vcategory(args): vcategory = VirtualCategoryFactory.create(vcategory_id) if vcategory is None: - kodi.notify_warn(f"Cannot find virtual category '{vcategory_id}'") + kodi.notify_warn(kodi.translate(40972).format(vcategory_id)) return # cleanup first views_repository.cleanup_virtual_category_views(vcategory.get_id()) if do_notification: - kodi.notify(f'Rendering virtual category "{vcategory.get_name()}"') + kodi.notify(kodi.translate(40970).format(vcategory.get_name())) _render_category_view(vcategory, categories_repository, romcollections_repository, roms_repository, views_repository) - if do_notification: - kodi.notify(f'{vcategory.get_name()} view rendered') + if do_notification: + kodi.notify(kodi.translate(40971).format(vcategory.get_name())) kodi.refresh_container() @AppMediator.register('RENDER_ROMCOLLECTION_VIEW') def cmd_render_romcollection_view_data(args): - kodi.notify('Rendering romcollection views') romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None do_notification = not settings.getSettingAsBool("display_hide_rendering_notifications") + if do_notification: + kodi.notify(kodi.translate(40974)) + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: romcollections_repository = ROMCollectionRepository(uow) @@ -178,7 +180,7 @@ def cmd_render_romcollection_view_data(args): views_repository.store_view(romcollection.get_id(), romcollection.get_type(), collection_view_data) if do_notification: - kodi.notify('Selected views rendered') + kodi.notify(kodi.translate(40966)) kodi.refresh_container() @@ -195,12 +197,12 @@ def cmd_render_vcollection(args): vcollection = VirtualCollectionFactory.create(vcollection_id) if do_notification: - kodi.notify(f'Rendering virtual collection "{vcollection.get_name()}"') + kodi.notify(kodi.translate(40973).format(vcollection.get_name())) collection_view_data = _render_romcollection_view(vcollection, roms_repository) views_repository.store_view(vcollection.get_id(), vcollection.get_type(), collection_view_data) - if do_notification: - kodi.notify(f'{vcollection.get_name()} view rendered') + if do_notification: + kodi.notify(kodi.translate(40971).format(vcollection.get_name())) kodi.refresh_container() @@ -217,7 +219,7 @@ def cmd_render_rom_views(args): rom_obj = roms_repository.find_rom(rom_id) if do_notification: - kodi.notify(f'Rendering all views containing ROM#{rom_obj.get_rom_identifier()}') + kodi.notify(kodi.translate(40975).format(rom_obj.get_rom_identifier())) romcollections = romcollections_repository.find_romcollections_by_rom(rom_id) for romcollection in romcollections: @@ -230,7 +232,7 @@ def cmd_render_rom_views(args): views_repository.store_view(vcollection.get_id(), vcollection.get_type(), collection_view_data) if do_notification: - kodi.notify('Views rendered') + kodi.notify(kodi.translate(40964)) kodi.refresh_container() @AppMediator.register('CLEANUP_VIEWS') @@ -360,7 +362,7 @@ def _render_category_view(category_obj: Category, categories_repository: Categor view_items.append(rendered_item) except Exception: logger.exception(f"Exception while rendering list item ROM Collection '{romcollection.get_name()}'") - kodi.notify_error(f"Failed to process ROM collection {romcollection.get_name()}") + kodi.notify_error(kodi.translate(40976).format(romcollection.get_name())) if render_sub_views and not category_obj.get_type() == constants.OBJ_CATEGORY_VIRTUAL: collection_view_data = _render_romcollection_view(romcollection, roms_repository) views_repository.store_view(romcollection.get_id(), romcollection.get_type(), collection_view_data) diff --git a/resources/lib/domain.py b/resources/lib/domain.py index 85ae7fbd..3ed2e775 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -55,17 +55,17 @@ def _is_empty_or_default(input: any, default: any): # Returns an object with all the information # ------------------------------------------------------------------------------------------------- class AssetInfo(object): - id = '' - key = '' - - name = '' - description = name - plural = '' - fname_infix = '' # Used only when searching assets when importing XML - kind_str = '' - exts = [] - exts_dialog = '' - path_key = '' + id = '' + key = '' + name_id = '' + name = '' + description = name + plural = '' + fname_infix = '' # Used only when searching assets when importing XML + kind_str = '' + exts = [] + exts_dialog = '' + path_key = '' def get_description(self): if self.description == '': return self.name @@ -328,31 +328,6 @@ def is_mapped(self): return True -# legacy -# |----- LauncherABC (abstract class) -# | -# |----- StandaloneLauncher (Standalone launcher) -# | -# |----- ROMLauncherABC (abstract class) -# | -# |----- CollectionLauncher (ROM Collection launcher) -# | -# |----- VirtualLauncher (Browse by ... launcher) -# | -# |----- StandardRomLauncher (Standard launcher) -# | -# |----- LnkLauncher -# | -# |----- RetroplayerLauncher -# | -# |----- RetroarchLauncher -# | -# |----- SteamLauncher -# | -# |----- NvidiaGameStreamLauncher -# -# ------------------------------------------------------------------------------------------------- - class ROMAddon(EntityABC): __metaclass__ = abc.ABCMeta @@ -362,7 +337,8 @@ def __init__(self, addon: AelAddon, entity_data: dict): def get_name(self): secondary_name = self.get_secondary_name() - if secondary_name: return '{} ({})'.format(self.addon.get_name(), secondary_name) + if secondary_name: + return '{} ({})'.format(self.addon.get_name(), secondary_name) return self.addon.get_name() def get_secondary_name(self): @@ -454,14 +430,17 @@ def launch(self, rom: ROM): rom_file_path = rom.get_scanned_data_element_as_file('file') if rom_file_path is None: logger.warning(f'Cannot launch ROM {rom.get_rom_identifier()}. No path provided.') - kodi.notify_warn('Cannot launch ROM') + kodi.notify_warn(kodi.translate(40957)) return # >> How to fill gameclient = string (game.libretro.fceumm) ??? game_info = { - 'title' : rom.get_name(), 'platform' : rom.get_platform(), - 'genres' : [rom.get_genre()], 'developer' : rom.get_developer(), - 'overview' : rom.get_plot(), 'year' : rom.get_releaseyear() + 'title' : rom.get_name(), + 'platform': rom.get_platform(), + 'genres' : [rom.get_genre()], + 'developer': rom.get_developer(), + 'overview': rom.get_plot(), + 'year': rom.get_releaseyear() } logger.info(f'launch() name "{rom.get_name()}"') logger.info(f'launch() path "{rom_file_path.getPath()}"') @@ -479,7 +458,7 @@ def configure(self, romcollection: ROMCollection): } is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, globals.WEBSERVER_PORT, post_data) if not is_stored: - kodi.notify_error('Failed to store launchers settings') + kodi.notify_error(kodi.translate(40958)) def configure_for_rom(self, rom: ROM): post_data = { @@ -490,7 +469,7 @@ def configure_for_rom(self, rom: ROM): } is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, globals.WEBSERVER_PORT, post_data) if not is_stored: - kodi.notify_error('Failed to store launchers settings') + kodi.notify_error(kodi.translate(40958)) class ROMCollectionScanner(ROMAddon): @@ -530,13 +509,16 @@ def settings_are_applicable(self) -> bool: if settings.scrape_metadata_policy != constants.SCRAPE_ACTION_NONE: supported_metadata_types = self.get_supported_metadata() - if len(supported_metadata_types) > 0: return True + if len(supported_metadata_types) > 0: + return True if settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE: supported_asset_types = self.get_supported_assets() - if len(supported_asset_types) == 0: return False + if len(supported_asset_types) == 0: + return False asset_overlap = list(set(supported_asset_types) & set(settings.asset_IDs_to_scrape)) - if len(asset_overlap) > 0: return True + if len(asset_overlap) > 0: + return True return False @@ -551,13 +533,15 @@ def is_asset_supported(self, asset_id) -> bool: def get_supported_metadata(self) -> typing.List[str]: extra_settings = self.addon.get_extra_settings() supported_types = extra_settings['supported_metadata'] if 'supported_metadata' in extra_settings else None - if supported_types is None: return None + if supported_types is None: + return None return supported_types.split('|') def get_supported_assets(self) -> typing.List[str]: extra_settings = self.addon.get_extra_settings() supported_types = extra_settings['supported_assets'] if 'supported_assets' in extra_settings else None - if supported_types is None: return None + if supported_types is None: + return None return supported_types.split('|') def get_scraper_settings(self) -> ScraperSettings: @@ -575,7 +559,7 @@ def get_scrape_command(self, rom: ROM)-> dict: '--server_port': globals.WEBSERVER_PORT, '--akl_addon_id': self.addon.get_id(), '--rom_id': rom.get_id(), - '--settings': io.parse_to_json_arg(self.get_settings()) + '--settings': io.parse_to_json_arg(self.get_settings()) } def get_scrape_command_for_collection(self, collection: ROMCollection) -> dict: @@ -586,7 +570,7 @@ def get_scrape_command_for_collection(self, collection: ROMCollection) -> dict: '--server_port': globals.WEBSERVER_PORT, '--akl_addon_id': self.addon.get_id(), '--romcollection_id': collection.get_id(), - '--settings': io.parse_to_json_arg(self.get_settings()) + '--settings': io.parse_to_json_arg(self.get_settings()) } # ------------------------------------------------------------------------------------------------- @@ -649,7 +633,7 @@ def get_metadata_id(self): return self.entity_data['metadata_id'] def get_name(self): - return self.entity_data['m_name'] if 'm_name' in self.entity_data else 'Unknown' + return self.entity_data['m_name'] if 'm_name' in self.entity_data else kodi.translate(41156) def set_name(self, name): self.entity_data['m_name'] = name @@ -723,9 +707,9 @@ def set_trailer(self, trailer_str): def is_finished(self): return 'finished' in self.entity_data and self.entity_data['finished'] - def get_finished_str(self): + def get_finished_str_code(self): finished = self.is_finished() - finished_display = 'Finished' if finished == True else 'Unfinished' + finished_display = 42014 if finished == True else 42015 return finished_display @@ -766,8 +750,10 @@ def get_available_assets(self) -> typing.List[Asset]: # Gets the asset path (str) of the given assetinfo type. # def get_asset_str(self, asset_info=None, asset_id=None, fallback = '') -> str: - if asset_info is None and asset_id is None: return None - if asset_info is not None: asset_id = asset_info.id + if asset_info is None and asset_id is None: + return None + if asset_info is not None: + asset_id = asset_info.id asset = self.get_asset(asset_id) if asset is not None: @@ -777,10 +763,12 @@ def get_asset_str(self, asset_info=None, asset_id=None, fallback = '') -> str: return fallback def get_asset_FN(self, asset_info: AssetInfo) -> io.FileName: - if asset_info is None: return None + if asset_info is None: + return None asset = self.get_asset(asset_info.id) - if asset is None: return None + if asset is None: + return None return asset.get_path_FN() @@ -927,13 +915,13 @@ def __init__(self, super(Category, self).__init__(category_dic, assets, None, asset_mappings) def get_object_name(self): - return 'Category' + return "Category" def get_assets_kind(self): return constants.KIND_ASSET_CATEGORY def get_type(self): - return constants.OBJ_CATEGORY + return constants.OBJ_CATEGORY # 42501 # parent category / romcollection this item belongs to. def get_parent_id(self) -> str: @@ -986,11 +974,11 @@ def import_NFO_file(self, nfo_FileName: io.FileName) -> bool: item_nfo = nfo_FileName.loadFileToStr() item_nfo = item_nfo.replace('\r', '').replace('\n', '') except: - kodi.notify_warn('Exception reading NFO file {0}'.format(nfo_FileName.getPath())) + kodi.notify_warn(kodi.translate(41044).format(nfo_FileName.getPath())) logger.error("Category.import_NFO_file() Exception reading NFO file '{0}'".format(nfo_FileName.getPath())) return False else: - kodi.notify_warn('NFO file not found {0}'.format(nfo_FileName.getBase())) + kodi.notify_warn(kodi.translate(41045).format(nfo_FileName.getBase())) logger.error("Category.import_NFO_file() NFO file not found '{0}'".format(nfo_FileName.getPath())) return False @@ -1065,13 +1053,13 @@ def __str__(self): class VirtualCategory(Category): def get_object_name(self): - return 'Virtual Category' + return "Virtual Category" def get_assets_kind(self): return constants.KIND_ASSET_CATEGORY def get_type(self): - return constants.OBJ_CATEGORY_VIRTUAL + return constants.OBJ_CATEGORY_VIRTUAL # 42502 # ------------------------------------------------------------------------------------------------- # Class representing a collection of ROMs. @@ -1107,9 +1095,11 @@ def __init__(self, super(ROMCollection, self).__init__(entity_data, assets_data, asset_paths, asset_mappings) - def get_object_name(self): return 'ROM Collection' + def get_object_name(self): + return "ROM Collection" - def get_assets_kind(self): return constants.KIND_ASSET_LAUNCHER + def get_assets_kind(self): + return constants.KIND_ASSET_LAUNCHER def get_type(self): return constants.OBJ_ROMCOLLECTION @@ -1168,8 +1158,8 @@ def set_mapped_ROM_asset(self, asset_info: AssetInfo, mapped_to_info: AssetInfo) # Get a list of assets with duplicated paths. Refuse to do anything if duplicated paths found. # def get_duplicated_asset_dirs(self): - duplicated_bool_list = [False] * len(constants.ROM_ASSET_ID_LIST) - duplicated_name_list = [] + duplicated_bool_list = [False] * len(constants.ROM_ASSET_ID_LIST) + duplicated_name_list = [] # >> Check for duplicated asset paths for i, asset_i in enumerate(constants.ROM_ASSET_ID_LIST[:-1]): @@ -1217,14 +1207,17 @@ def get_launcher(self, id:str) -> ROMLauncherAddon: return next((l for l in self.launchers_data if l.get_id() == id), None) def get_default_launcher(self) -> ROMLauncherAddon: - if len(self.launchers_data) == 0: return None + if len(self.launchers_data) == 0: + return None default_launcher = next((l for l in self.launchers_data if l.is_default()), None) - if default_launcher is None: return self.launchers_data[0] + if default_launcher is None: + return self.launchers_data[0] return default_launcher def set_launcher_as_default(self, launcher_id): - if len(self.launchers_data) == 0: return + if len(self.launchers_data) == 0: + return current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) if current_default_launcher: current_default_launcher.set_default(False) @@ -1276,11 +1269,11 @@ def import_NFO_file(self, nfo_FileName: io.FileName) -> bool: item_nfo = nfo_FileName.loadFileToStr() item_nfo = item_nfo.replace('\r', '').replace('\n', '') except: - kodi.notify_warn('Exception reading NFO file {0}'.format(nfo_FileName.getPath())) + kodi.notify_warn(kodi.translate(41044).format(nfo_FileName.getPath())) logger.error("ROMCollection.import_NFO_file() Exception reading NFO file '{0}'".format(nfo_FileName.getPath())) return False else: - kodi.notify_warn('NFO file not found {0}'.format(nfo_FileName.getBase())) + kodi.notify_warn(kodi.translate(41045).format(nfo_FileName.getBase())) logger.error("ROMCollection.import_NFO_file() NFO file not found '{0}'".format(nfo_FileName.getPath())) return False @@ -1366,7 +1359,7 @@ def __init__(self, super(VirtualCollection, self).__init__(entity_data, assets_data) def get_object_name(self): - return 'Virtual Collection' + return "Virtual Collection" def get_assets_kind(self): return constants.KIND_ASSET_COLLECTION @@ -1418,7 +1411,8 @@ def __init__(self, super(ROM, self).__init__(rom_data, assets_data, asset_paths_data, asset_mappings) - def get_object_name(self): return 'ROM' + def get_object_name(self): + return 'ROM' def get_assets_kind(self): return constants.KIND_ASSET_ROM @@ -1539,8 +1533,10 @@ def add_tag(self, tag:str): self.tags[tag] = '' def remove_tag(self, tag:str): - if self.tags is None: return - if not tag in self.tags: return + if self.tags is None: + return + if not tag in self.tags: + return del self.tags[tag] def clear_tags(self): @@ -1600,7 +1596,8 @@ def add_launcher(self, addon: AelAddon, settings: dict, is_non_blocking = True, }) if is_default: current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) - if current_default_launcher: current_default_launcher.set_default(False) + if current_default_launcher: + current_default_launcher.set_default(False) self.launchers_data.append(launcher) logger.debug(f'Adding addon "{addon.get_addon_id()}" to ROM "{self.get_name()}"') @@ -1612,14 +1609,17 @@ def get_launcher(self, id:str) -> ROMLauncherAddon: return next((l for l in self.launchers_data if l.get_id() == id), None) def get_default_launcher(self) -> ROMLauncherAddon: - if len(self.launchers_data) == 0: return None + if len(self.launchers_data) == 0: + return None default_launcher = next((l for l in self.launchers_data if l.is_default()), None) - if default_launcher is None: return self.launchers_data[0] + if default_launcher is None: + return self.launchers_data[0] return default_launcher def set_launcher_as_default(self, launcher_id): - if len(self.launchers_data) == 0: return + if len(self.launchers_data) == 0: + return current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) if current_default_launcher: current_default_launcher.set_default(False) @@ -1688,7 +1688,7 @@ def update_with_nfo_file(self, nfo_file_path:io.FileName, verbose = True): logger.debug('Rom.update_with_nfo_file() Loading "{0}"'.format(nfo_file_path.getPath())) if not nfo_file_path.exists(): if verbose: - kodi.notify_warn('NFO file not found {0}'.format(nfo_file_path.getPath())) + kodi.notify_warn(kodi.translate(41045).format(nfo_file_path.getPath())) logger.debug("Rom.update_with_nfo_file() NFO file not found '{0}'".format(nfo_file_path.getPath())) return False @@ -1729,7 +1729,7 @@ def update_with_nfo_file(self, nfo_file_path:io.FileName, verbose = True): if len(item_trailer) > 0: self.set_trailer(text.unescape_XML(item_trailer[0])) if verbose: - kodi.notify('Imported {0}'.format(nfo_file_path.getPath())) + kodi.notify(kodi.translate(41046).format(nfo_file_path.getPath())) return True @@ -2085,6 +2085,7 @@ def _load_asset_data(self): # >> These are used very frequently so I think it is better to have a cached list. a = AssetInfo() a.id = constants.ASSET_ICON_ID + a.name_id = 43001 a.name = 'Icon' a.plural = 'Icons' a.fname_infix = 'icon' @@ -2096,6 +2097,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_FANART_ID + a.name_id = 43002 a.name = 'Fanart' a.plural = 'Fanarts' a.fname_infix = 'fanart' @@ -2107,6 +2109,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_BANNER_ID + a.name_id = 43003 a.name = 'Banner' a.description = 'Banner / Marquee' a.plural = 'Banners' @@ -2119,6 +2122,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_POSTER_ID + a.name_id = 43004 a.name = 'Poster' a.plural = 'Posters' a.fname_infix = 'poster' @@ -2130,6 +2134,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_CLEARLOGO_ID + a.name_id = 43005 a.name = 'Clearlogo' a.plural = 'Clearlogos' a.fname_infix = 'clearlogo' @@ -2141,6 +2146,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_CONTROLLER_ID + a.name_id = 43006 a.name = 'Controller' a.plural = 'Controllers' a.fname_infix = 'controller' @@ -2152,6 +2158,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_TRAILER_ID + a.name_id = 43007 a.name = 'Trailer' a.plural = 'Trailers' a.fname_infix = 'trailer' @@ -2163,6 +2170,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_TITLE_ID + a.name_id = 43008 a.name = 'Title' a.plural = 'Titles' a.fname_infix = 'title' @@ -2174,6 +2182,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_SNAP_ID + a.name_id = 43009 a.name = 'Snap' a.plural = 'Snaps' a.fname_infix = 'snap' @@ -2185,6 +2194,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_BOXFRONT_ID + a.name_id = 43010 a.name = 'Boxfront' a.description = 'Boxfront / Cabinet' a.plural = 'Boxfronts' @@ -2197,6 +2207,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_BOXBACK_ID + a.name_id = 43011 a.name = 'Boxback' a.description = 'Boxback / CPanel' a.plural = 'Boxbacks' @@ -2209,6 +2220,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_CARTRIDGE_ID + a.name_id = 43012 a.name = 'Cartridge' a.description = 'Cartridge / PCB' a.plural = 'Cartridges' @@ -2221,6 +2233,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_FLYER_ID + a.name_id = 43013 a.name = 'Flyer' a.plural = 'Flyers' a.fname_infix = 'flyer' @@ -2233,6 +2246,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_MAP_ID + a.name_id = 43014 a.name = 'Map' a.plural = 'Maps' a.fname_infix = 'map' @@ -2244,6 +2258,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_MANUAL_ID + a.name_id = 43015 a.name = 'Manual' a.plural = 'Manuals' a.fname_infix = 'manual' @@ -2255,6 +2270,7 @@ def _load_asset_data(self): a = AssetInfo() a.id = constants.ASSET_3DBOX_ID + a.name_id = 43016 a.name = '3D Box' a.plural = '3D Boxes' a.fname_infix = '3dbox' @@ -2279,20 +2295,32 @@ def create(vcollection_id: str) -> VirtualCollection: if vcollection_id == constants.VCOLLECTION_FAVOURITES_ID: return VirtualCollection(dict(default_entity_data, **{ 'id' : vcollection_id, - 'm_name' : '', - 'plot': 'Browse AKL Favourite ROMs', + 'm_name' : kodi.translate(42063), + 'plot': kodi.translate(42005), 'finished': settings.getSettingAsBool('display_hide_favs') }), [ - Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), - Asset({'id' : '', 'asset_type' : constants.ASSET_ICON_ID, 'filepath' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Favourites_icon.png').getPath()}), - Asset({'id' : '', 'asset_type' : constants.ASSET_POSTER_ID, 'filepath' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Favourites_poster.png').getPath()}), + Asset({ + 'id' : '', + 'asset_type' : constants.ASSET_FANART_ID, + 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath() + }), + Asset({ + 'id' : '', + 'asset_type' : constants.ASSET_ICON_ID, + 'filepath' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Favourites_icon.png').getPath() + }), + Asset({ + 'id' : '', + 'asset_type' : constants.ASSET_POSTER_ID, + 'filepath' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Favourites_poster.png').getPath() + }), ]) if vcollection_id == constants.VCOLLECTION_RECENT_ID: return VirtualCollection(dict(default_entity_data, **{ 'id' : vcollection_id, - 'm_name' : '[Recently played ROMs]', - 'plot': 'Browse the ROMs you played recently', + 'm_name' : kodi.translate(42064), + 'plot': kodi.translate(42006), 'finished': settings.getSettingAsBool('display_hide_recent') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2303,8 +2331,8 @@ def create(vcollection_id: str) -> VirtualCollection: if vcollection_id == constants.VCOLLECTION_MOST_PLAYED_ID: return VirtualCollection(dict(default_entity_data, **{ 'id' : vcollection_id, - 'm_name' : '[Most played ROMs]', - 'plot': 'Browse the ROMs you play most', + 'm_name' : kodi.translate(42065), + 'plot': kodi.translate(42007), 'finished': settings.getSettingAsBool('display_hide_mostplayed') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2322,7 +2350,7 @@ def create_by_category(vcategory_id:str, collection_value:str) -> VirtualCollect 'id' : f'{vcategory_id}_{collection_value}', 'parent_id': vcategory_id, 'm_name' : collection_value, - 'plot': f"Browse ROMs filtered on '{collection_value}'", + 'plot': kodi.translate(42008).format(collection_value), 'collection_value': collection_value, 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ @@ -2338,8 +2366,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_ROOT_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by...', - 'plot': 'Browse the ROMs by specifics', + 'm_name' : kodi.translate(42066), + 'plot': kodi.translate(42009), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2350,8 +2378,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_TITLE_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by Title', - 'plot': 'Browse the ROMs by title', + 'm_name' : kodi.translate(42067), + 'plot': kodi.translate(42010), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2362,8 +2390,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_YEARS_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by Year', - 'plot': 'Browse the ROMs by year', + 'm_name' : kodi.translate(42068), + 'plot': kodi.translate(42011), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2374,8 +2402,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_GENRE_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by Genre', - 'plot': 'Browse the ROMs by genre', + 'm_name' : kodi.translate(42069), + 'plot': kodi.translate(42012), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2386,8 +2414,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_DEVELOPER_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by Developer', - 'plot': 'Browse the ROMs by developer', + 'm_name' : kodi.translate(42070), + 'plot': kodi.translate(42013), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2398,8 +2426,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_NPLAYERS_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by Number of Players', - 'plot': 'Browse the ROMs by number of players', + 'm_name' : kodi.translate(42071), + 'plot': kodi.translate(42014), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2410,8 +2438,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_ESRB_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by ESRB Rating', - 'plot': 'Browse the ROMs by ESRB rating', + 'm_name': kodi.translate(42072), + 'plot': kodi.translate(42015), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2422,8 +2450,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_PEGI_ID: return VirtualCategory({ 'id' : vcategory_id, - 'm_name' : 'Browse by PEGI Rating', - 'plot': 'Browse the ROMs by PEGI rating', + 'm_name': kodi.translate(42073), + 'plot': kodi.translate(42016), 'finished': settings.getSettingAsBool('display_hide_vcategories') }, [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2434,8 +2462,8 @@ def create(vcategory_id: str) -> VirtualCategory: if vcategory_id == constants.VCATEGORY_RATING_ID: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, - 'm_name' : 'Browse by Rating', - 'plot': 'Browse the ROMs by rating', + 'm_name': kodi.translate(42074), + 'plot': kodi.translate(42017), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), diff --git a/resources/lib/editors.py b/resources/lib/editors.py index ce05b6bc..ac8e60c9 100644 --- a/resources/lib/editors.py +++ b/resources/lib/editors.py @@ -40,15 +40,16 @@ def edit_field_by_str(obj_instance: MetaDataItemABC, metadata_name, get_method, set_method) -> bool: object_name = obj_instance.get_object_name() old_value = get_method() - s = 'Edit {0} "{1}" {2}'.format(object_name, old_value, metadata_name) + s = kodi.translate(41137).format(object_name, old_value, metadata_name) new_value = kodi.dialog_keyboard(s, old_value) - if new_value is None: return False + if new_value is None: + return False if old_value == new_value: - kodi.notify('{0} {1} not changed'.format(object_name, metadata_name)) + kodi.notify(kodi.translate(40987).format(object_name, metadata_name)) return False set_method(new_value) - kodi.notify('{0} {1} is now {2}'.format(object_name, metadata_name, new_value)) + kodi.notify(kodi.translate(40986).format(object_name, metadata_name, new_value)) return True # Edits an object field which is an integer. @@ -59,15 +60,15 @@ def edit_field_by_str(obj_instance: MetaDataItemABC, metadata_name, get_method, def edit_field_by_int(obj_instance: MetaDataItemABC, metadata_name, get_method, set_method) -> bool: object_name = obj_instance.get_object_name() old_value = get_method() - s = 'Edit {0} "{1}" {2}'.format(object_name, old_value, metadata_name) + s = kodi.translate(41137).format(object_name, old_value, metadata_name) new_value = kodi.dialog_numeric(s, old_value) if new_value is None: return False if old_value == new_value: - kodi.notify('{0} {1} not changed'.format(object_name, metadata_name)) + kodi.notify(kodi.translate(40987).format(object_name, metadata_name)) return False set_method(new_value) - kodi.notify('{0} {1} is now {2}'.format(object_name, metadata_name, new_value)) + kodi.notify(kodi.translate(40986).format(object_name, metadata_name, new_value)) return True # @@ -84,17 +85,17 @@ def edit_field_by_list(obj_instance: MetaDataItemABC, metadata_name:str, str_lis preselect_idx = str_list.index(old_value) else: preselect_idx = 0 - dialog_title = 'Edit {0} {1}'.format(object_name, metadata_name) + dialog_title = kodi.translate(41075).format(object_name, metadata_name) selected = kodi.ListDialog().select(dialog_title, str_list, preselect_idx) if selected is None: return new_value = str_list[selected] if old_value == new_value: - kodi.notify('{0} {1} not changed'.format(object_name, metadata_name)) + kodi.notify(kodi.translate(40987).format(object_name, metadata_name)) return False set_method(new_value) - kodi.notify('{0} {1} is now {2}'.format(object_name, metadata_name, new_value)) + kodi.notify(kodi.translate(40986).format(object_name, metadata_name, new_value)) return True # @@ -107,9 +108,18 @@ def edit_field_by_list(obj_instance: MetaDataItemABC, metadata_name:str, str_lis # def edit_rating(obj_instance: MetaDataItemABC, get_method, set_method): options_list = [ - 'Not set', - 'Rating 0', 'Rating 1', 'Rating 2', 'Rating 3', 'Rating 4', 'Rating 5', - 'Rating 6', 'Rating 7', 'Rating 8', 'Rating 9', 'Rating 10' + kodi.translate(42001), + kodi.translate(42002).format('0'), + kodi.translate(42002).format('1'), + kodi.translate(42002).format('2'), + kodi.translate(42002).format('3'), + kodi.translate(42002).format('4'), + kodi.translate(42002).format('5'), + kodi.translate(42002).format('6'), + kodi.translate(42002).format('7'), + kodi.translate(42002).format('8'), + kodi.translate(42002).format('9'), + kodi.translate(42002).format('10') ] object_name = obj_instance.get_object_name() current_rating_str = get_method() @@ -117,11 +127,11 @@ def edit_rating(obj_instance: MetaDataItemABC, get_method, set_method): preselected_value = 0 else: preselected_value = int(current_rating_str) + 1 - sel_value = kodi.ListDialog().select('Select the {0} Rating'.format(object_name), + sel_value = kodi.ListDialog().select(kodi.translate(41079).format(object_name), options_list, preselect_idx = preselected_value) if sel_value is None: return if sel_value == preselected_value: - kodi.notify('{0} Rating not changed'.format(object_name)) + kodi.notify(kodi.translate(40988).format(object_name)) return False if sel_value == 0: @@ -129,11 +139,11 @@ def edit_rating(obj_instance: MetaDataItemABC, get_method, set_method): elif sel_value >= 1 and sel_value <= 11: current_rating_str = '{0}'.format(sel_value - 1) elif sel_value < 0: - kodi.notify('{0} Rating not changed'.format(object_name)) + kodi.notify(kodi.translate(40988).format(object_name)) return False set_method(current_rating_str) - kodi.notify('{0} rating is now {1}'.format(object_name, current_rating_str)) + kodi.notify(kodi.translate(40989).format(object_name, current_rating_str)) return True # @@ -147,8 +157,7 @@ def import_TXT_file(text_file: io.FileName): file_size = statinfo.st_size logger.debug('import_TXT_file() File size is {0}'.format(file_size)) if file_size > 16384: - ret = kodi.dialog_yesno('File "{0}" has {1} bytes and it is very big.'.format(text_file.getPath(), file_size) + - 'Are you sure this is the correct file?') + ret = kodi.dialog_yesno(kodi.translate(41070).format(text_file.getPath(), file_size)) if not ret: return '' # Import file @@ -174,14 +183,17 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - # >> Label1 is the asset name (Icon, Fanart, etc.) # >> Label2 is the asset filename str as in the database or 'Not set' # >> setArt('icon') is the asset picture. - label1_str = 'Edit {0} ...'.format(asset_info_obj.name) + label1_str = kodi.translate(42003).format(asset_info_obj.name) label2_stt = asset_fname_str if asset_fname_str else 'Not set' list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_stt) if asset_fname_str: item_path = io.FileName(asset_fname_str) - if item_path.isVideoFile(): item_img = 'DefaultAddonVideo.png' - elif item_path.isManualFile(): item_img = 'DefaultAddonInfoProvider.png' - else: item_img = asset_fname_str + if item_path.isVideoFile(): + item_img = 'DefaultAddonVideo.png' + elif item_path.isManualFile(): + item_img = 'DefaultAddonInfoProvider.png' + else: + item_img = asset_fname_str else: item_img = 'DefaultAddonNone.png' list_item.setArt({'icon' : item_img}) @@ -190,12 +202,11 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - # if ROM then add scrape option if obj_instance.get_object_name() == 'ROM': - options[SCRAPE_CMD] = 'Scrape ROM assets' + options[SCRAPE_CMD] = kodi.translate(41120) # --- Customize function for each object type --- - dialog_title_str = 'Edit {0} Assets/Artwork'. format(obj_instance.get_object_name()) + dialog_title_str = kodi.translate(41076). format(obj_instance.get_object_name()) dialog = kodi.OrdDictionaryDialog() - # >> Use Krypton Dialog().select(useDetails = True) to display label2 on dialog. selected_option = dialog.select(dialog_title_str, options, preselect = preselected_asset, use_details = True) if selected_option is None: @@ -222,8 +233,8 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: # --- Get asset object information --- assets_directory = obj_instance.get_assets_root_path() if not assets_directory: - if kodi.dialog_yesno("No local assets path configured. Configure now?\n Else we will use addon default directories."): - path_str = kodi.dialog_get_directory(f"Assets root path for entry '{obj_instance.get_name()}'") + if kodi.dialog_yesno(kodi.translate(41047)): + path_str = kodi.dialog_get_directory(kodi.translate(41138).format(obj_instance.get_name())) assets_directory = io.FileName(path_str, True) obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) else: @@ -240,8 +251,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: assets_directory = io.FileName(settings.getSetting('launchers_asset_dir'), isdir = True) obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) else: - kodi.dialog_OK('Unknown obj_instance.get_assets_kind() {}. '.format(obj_instance.get_assets_kind()) + - 'This is a bug, please report it.') + kodi.dialog_OK(kodi.translate(41140).format(obj_instance.get_assets_kind())) return None asset_type_directory = obj_instance.get_asset_path(asset_info, False) @@ -253,19 +263,18 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: if not assets_directory.exists(): logger.error(f'Directory not found "{assets_directory.getPath()}"') - kodi.dialog_OK('Directory to store artwork not found. ' - 'Configure it before you can edit artwork.') + kodi.dialog_OK(kodi.translate(41139)) return None - dialog_title = f'Change {obj_instance.get_name()} {asset_info.name}' + dialog_title = kodi.translate(41074).format(obj_instance.get_name(), asset_info.name) # --- Show image editing options --- options = collections.OrderedDict() - options['LINK_LOCAL'] = f'Link to local {asset_info.name} image' - options['IMPORT_LOCAL'] = f'Import local {asset_info.name} (copy and rename)' - options['UNSET'] = 'Unset artwork/asset' + options['LINK_LOCAL'] = kodi.translate(42004).format(asset_info.name) + options['IMPORT_LOCAL'] = kodi.translate(42005).format(asset_info.name) + options['UNSET'] = kodi.translate(42006) if obj_instance.get_assets_kind() == constants.KIND_ASSET_ROM: - options['SCRAPE_ASSET'] = f'Scrape {asset_info.name}' + options['SCRAPE_ASSET'] = kodi.translate(42007).format(asset_info.name) selected_option = kodi.OrdDictionaryDialog().select(dialog_title, options) @@ -291,7 +300,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: current_image_dir = io.FileName('/') logger.debug(f'edit_asset() Asset initial dir "{current_image_dir.getPath()}"') - title_str = 'Select {0} {1}'.format(obj_instance.get_object_name(), asset_info.name) + title_str = kodi.translate(41141).format(obj_instance.get_object_name(), kodi.translate(asset_info.name_id)) ext_list = asset_info.exts_dialog if asset_info.id == constants.ASSET_MANUAL_ID or asset_info.id == constants.ASSET_TRAILER_ID: new_asset_file = kodi.browse(text=title_str, mask=ext_list, preselected_path=current_image_dir.getPath()) @@ -318,7 +327,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: # kodi_update_image_cache(new_asset_FN) # --- Notify user --- - kodi.notify('{0} {1} has been updated'.format(obj_instance.get_object_name(), asset_info.name)) + kodi.notify(kodi.translate(40990).format(obj_instance.get_object_name(), asset_info.name)) # --- Import an image --- # >> Copy and rename a local image into asset directory @@ -328,7 +337,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: logger.info("No local asset type path configured. Reverting to root.") current_image_dir = obj_instance.get_assets_root_path() - title_str = f'Select {obj_instance.get_object_name()} {asset_info.name}' + title_str = kodi.translate(41141).format(obj_instance.get_object_name(), kodi.translate(asset_info.name_id)) ext_list = asset_info.exts_dialog if asset_info.id == constants.ASSET_MANUAL_ID or asset_info.id == constants.ASSET_TRAILER_ID: new_asset_file_str = kodi.browse(text=title_str, mask=ext_list, preselected_path=current_image_dir.getPath()) @@ -349,7 +358,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: logger.debug(f'edit_asset() dest_asset_file "{dest_asset_file.getPath()}"') if new_asset_file.getPath() == dest_asset_file.getPath(): logger.info('edit_asset() new_asset_file and dest_asset_file are the same. Returning.') - kodi.notify_warn('new_asset_file and dest_asset_file are the same. Returning') + kodi.notify_warn(kodi.translate(40991)) return None # --- Kodi image cache --- @@ -372,7 +381,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: new_asset_file.copy(dest_asset_file) except constants.AddonError: logger.error('edit_asset() AddonException exception copying image') - kodi.notify_warn('AddonException exception copying image') + kodi.notify_warn(kodi.translate(40992)) return None # --- Update object --- @@ -412,13 +421,13 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: logger.exception("Failed to delete cache") # --- Notify user --- - kodi.notify(f'{obj_instance.get_object_name()} {asset_info.name} has been updated') + kodi.notify(kodi.translate(40994).format(obj_instance.get_object_name(), asset_info.name)) # --- Unset asset --- elif selected_option == 'UNSET': obj_instance.set_asset(asset_info, io.FileName('')) logger.info('edit_asset() Unset {0} {1}'.format(obj_instance.get_object_name(), asset_info.name)) - kodi.notify('{0} {1} has been unset'.format(obj_instance.get_object_name(), asset_info.name)) + kodi.notify(kodi.translate(40993).format(obj_instance.get_object_name(), asset_info.name)) return selected_option @@ -432,7 +441,7 @@ def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_ logger.debug('edit_object_default_assets() preselected_asset_id {0}'.format(preselected_asset_id)) pre_select_idx = 0 - dialog_title_str = f'Edit {obj_instance.get_object_name()} default Assets/Artwork' + dialog_title_str = kodi.translate(41077).format(obj_instance.get_object_name()) # --- Build Dialog.select() list --- default_assets_list = obj_instance.get_mappable_asset_list() @@ -446,14 +455,19 @@ def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_ # >> icon is the fname string of the current mapped asset. mapped_asset_info = obj_instance.get_asset_mapping(default_asset_info) mapped_asset_str = obj_instance.get_asset_str(mapped_asset_info) - label1_str = f'Choose asset for {default_asset_info.name} (currently {mapped_asset_info.name})' + label1_str = kodi.translate(42055).format( + kodi.translate(default_asset_info.name_id), + kodi.translate(mapped_asset_info.name_id)) label2_str = mapped_asset_str list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_str) if mapped_asset_str: item_path = io.FileName(mapped_asset_str) - if item_path.isVideoFile(): item_img = 'DefaultAddonVideo.png' - elif item_path.isManualFile(): item_img = 'DefaultAddonInfoProvider.png' - else: item_img = mapped_asset_str + if item_path.isVideoFile(): + item_img = 'DefaultAddonVideo.png' + elif item_path.isManualFile(): + item_img = 'DefaultAddonInfoProvider.png' + else: + item_img = mapped_asset_str else: item_img = 'DefaultAddonNone.png' list_item.setArt({'icon' : item_img}) @@ -494,13 +508,16 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> # >> setArt('icon') is the asset picture. mapped_asset_str = obj_instance.get_asset_str(mappable_asset_info) label1_str = mappable_asset_info.name - label2_stt = mapped_asset_str if mapped_asset_str else 'Not set' - list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_stt) + label2_str = mapped_asset_str if mapped_asset_str else kodi.translate(42001) + list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_str) if mapped_asset_str: item_path = io.FileName(mapped_asset_str) - if item_path.isVideoFile(): item_img = 'DefaultAddonVideo.png' - elif item_path.isManualFile(): item_img = 'DefaultAddonInfoProvider.png' - else: item_img = mapped_asset_str + if item_path.isVideoFile(): + item_img = 'DefaultAddonVideo.png' + elif item_path.isManualFile(): + item_img = 'DefaultAddonInfoProvider.png' + else: + item_img = mapped_asset_str else: item_img = 'DefaultAddonNone.png' list_item.setArt({'icon' : item_img}) @@ -511,7 +528,7 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> secondary_pre_select_idx = counter counter += 1 - dialog_title_str = 'Edit {0} {1} mapped asset'.format(obj_instance.get_object_name(), asset_info.name) + dialog_title_str = kodi.translate(41078).format(obj_instance.get_object_name(), asset_info.name) secondary_selected_option = xbmcgui.Dialog().select( dialog_title_str, list = list_items, useDetails = True, preselect = secondary_pre_select_idx) logger.debug('edit_default_asset() Mapable select() returned {0}'.format(secondary_selected_option)) @@ -523,7 +540,7 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> new_selected_asset_info = asset_info_list[secondary_selected_option] logger.debug(f'edit_default_asset() Mapable selected {new_selected_asset_info.name}.') obj_instance.set_mapped_asset(asset_info, new_selected_asset_info) - kodi.notify('{0} {1} mapped to {2}'.format( + kodi.notify(kodi.translate(40983).format( obj_instance.get_object_name(), asset_info.name, new_selected_asset_info.name )) return True \ No newline at end of file diff --git a/resources/lib/globals.py b/resources/lib/globals.py index ff6f7232..e422a5fc 100644 --- a/resources/lib/globals.py +++ b/resources/lib/globals.py @@ -24,14 +24,14 @@ from akl.utils import io # --- Addon object (used to access settings) --- -addon = xbmcaddon.Addon() -addon_id = addon.getAddonInfo('id') -addon_name = addon.getAddonInfo('name') -addon_version = addon.getAddonInfo('version') -addon_author = addon.getAddonInfo('author') -addon_profile = addon.getAddonInfo('profile') -addon_type = addon.getAddonInfo('type') -addon_path = addon.getAddonInfo('path') +addon = xbmcaddon.Addon() +addon_id = addon.getAddonInfo('id') +addon_name = addon.getAddonInfo('name') +addon_version = addon.getAddonInfo('version') +addon_author = addon.getAddonInfo('author') +addon_profile = addon.getAddonInfo('profile') +addon_type = addon.getAddonInfo('type') +addon_path = addon.getAddonInfo('path') # --- Addon paths and constant definition --- # _PATH is a filename | _DIR is a directory. @@ -87,17 +87,26 @@ def __init__(self, addon_id): def build(self): # --- Addon data paths creation --- - if not self.ADDON_DATA_DIR.exists(): self.ADDON_DATA_DIR.makedirs() - if not self.SCRAPER_CACHE_DIR.exists(): self.SCRAPER_CACHE_DIR.makedirs() - if not self.REPORTS_DIR.exists(): self.REPORTS_DIR.makedirs() + if not self.ADDON_DATA_DIR.exists(): + self.ADDON_DATA_DIR.makedirs() + if not self.SCRAPER_CACHE_DIR.exists(): + self.SCRAPER_CACHE_DIR.makedirs() + if not self.REPORTS_DIR.exists(): + self.REPORTS_DIR.makedirs() - if not self.DEFAULT_CAT_ASSET_DIR.exists(): self.DEFAULT_CAT_ASSET_DIR.makedirs() - if not self.DEFAULT_COL_ASSET_DIR.exists(): self.DEFAULT_COL_ASSET_DIR.makedirs() - if not self.DEFAULT_LAUN_ASSET_DIR.exists(): self.DEFAULT_LAUN_ASSET_DIR.makedirs() - if not self.DEFAULT_FAV_ASSET_DIR.exists(): self.DEFAULT_FAV_ASSET_DIR.makedirs() + if not self.DEFAULT_CAT_ASSET_DIR.exists(): + self.DEFAULT_CAT_ASSET_DIR.makedirs() + if not self.DEFAULT_COL_ASSET_DIR.exists(): + self.DEFAULT_COL_ASSET_DIR.makedirs() + if not self.DEFAULT_LAUN_ASSET_DIR.exists(): + self.DEFAULT_LAUN_ASSET_DIR.makedirs() + if not self.DEFAULT_FAV_ASSET_DIR.exists(): + self.DEFAULT_FAV_ASSET_DIR.makedirs() - if not self.GENERATED_VIEWS_DIR.exists(): self.GENERATED_VIEWS_DIR.makedirs() - if not self.VIEWS_DIR.exists(): self.VIEWS_DIR.makedirs() + if not self.GENERATED_VIEWS_DIR.exists(): + self.GENERATED_VIEWS_DIR.makedirs() + if not self.VIEWS_DIR.exists(): + self.VIEWS_DIR.makedirs() return self diff --git a/resources/lib/services.py b/resources/lib/services.py index 24f680fd..89b9be69 100644 --- a/resources/lib/services.py +++ b/resources/lib/services.py @@ -42,10 +42,14 @@ def run(self): # --- Some debug stuff for development --- logger.info('------------ Called Advanced Kodi Launcher : Service ------------') logger.debug(f'sys.platform "{sys.platform}"') - if io.is_android(): logger.debug('OS "Android"') - if io.is_windows(): logger.debug('OS "Windows"') - if io.is_osx(): logger.debug('OS "OSX"') - if io.is_linux(): logger.debug('OS "Linux"') + if io.is_android(): + logger.debug('OS "Android"') + if io.is_windows(): + logger.debug('OS "Windows"') + if io.is_osx(): + logger.debug('OS "OSX"') + if io.is_linux(): + logger.debug('OS "Linux"') logger.debug('Python version "' + sys.version.replace('\n', '') + '"') logger.debug(f'addon.id "{globals.addon_id}"') logger.debug(f'addon.version "{globals.addon_version}"') @@ -99,7 +103,7 @@ def shutdown(self): logger.debug("AKL service stopped") def _initial_setup(self, uow:UnitOfWork): - kodi.notify('Creating new AKL database') + kodi.notify(kodi.translate(40981)) uow.create_empty_database(globals.g_PATHS.DATABASE_SCHEMA_PATH) logger.info("Database created.") diff --git a/resources/lib/viewqueries.py b/resources/lib/viewqueries.py index 7042ab2a..5fd9b10a 100644 --- a/resources/lib/viewqueries.py +++ b/resources/lib/viewqueries.py @@ -54,13 +54,13 @@ def qry_get_root_items(): 'obj_type': constants.OBJ_CATEGORY, 'items': [] } - kodi.notify('Building initial views') + kodi.notify(kodi.translate(40959)) AppMediator.async_cmd('RENDER_VIEWS') listitem_fanart = globals.g_PATHS.FANART_FILE_PATH.getPath() if not settings.getSettingAsBool('display_hide_utilities'): - listitem_name = 'Utilities' + listitem_name = kodi.translate(40897) container['items'].append({ 'name': listitem_name, 'url': globals.router.url_for_path('utilities'), @@ -68,7 +68,7 @@ def qry_get_root_items(): 'type': 'video', 'info': { 'title': listitem_name, - 'plot': 'Execute several [COLOR orange]Utilities[/COLOR].', + 'plot': kodi.translate(42001), 'overlay': 4 }, 'art': { @@ -80,7 +80,7 @@ def qry_get_root_items(): }) if not settings.getSettingAsBool('display_hide_g_reports'): - listitem_name = 'Global Reports' + listitem_name = kodi.translate(40898) container['items'].append({ 'name': listitem_name, 'url': globals.router.url_for_path('globalreports'), #SHOW_GLOBALREPORTS_VLAUNCHERS' @@ -88,7 +88,7 @@ def qry_get_root_items(): 'type': 'video', 'info': { 'title': listitem_name, - 'plot': 'Generate and view [COLOR orange]Global Reports[/COLOR].', + 'plot': kodi.translate(42002), 'overlay': 4 }, 'art': { @@ -219,12 +219,12 @@ def qry_get_view_assets(rom_id: str): 'id': asset_id, 'is_folder': False, 'type': 'pictures', - 'name': asset_info.name, + 'name': kodi.translate(asset_info.name_id), 'name2': asset.get_path() if asset else None, 'url': asset.get_path() if asset else globals.router.url_for_path( f'/execute/command/rom_edit_assets/?rom_id={rom_id}&selected_asset={asset_id}'), 'info': { - 'title': asset_info.name, + 'title': kodi.translate(asset_info.name_id), 'picturepath': asset.get_path() if asset else None, }, 'art': { @@ -282,7 +282,7 @@ def qry_get_utilities_items(): container = { 'id': '', - 'name': 'utilities', + 'name': kodi.translate(40897), 'obj_type': constants.OBJ_NONE, 'items': [] } @@ -292,164 +292,156 @@ def qry_get_utilities_items(): # EXECUTE_UTILS_CHECK_DATABASE -> Substituted by db constraints and migration scripts. container['items'].append({ - 'name': 'Reset database', + 'name': kodi.translate(40899), 'url': globals.router.url_for_path('execute/command/reset_database'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Reset database', - 'plot': 'Reset the AKL database. You will loose all data.', + 'title': kodi.translate(40899), + 'plot': kodi.translate(42003), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Rebuild views', + 'name': kodi.translate(40856), 'url': globals.router.url_for_path('execute/command/render_views'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Rebuild views', - 'plot': 'Rebuild all the container views in the application', + 'title': kodi.translate(40856), + 'plot': kodi.translate(42004), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Rebuild virtual views', + 'name': kodi.translate(40900), 'url': globals.router.url_for_path('execute/command/render_virtual_views'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Rebuild virtual views', - 'plot': 'Rebuild all the virtual categories and collections in the container', + 'title': kodi.translate(40900), + 'plot': kodi.translate(42018), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Scan for plugin-addons', + 'name': kodi.translate(40901), 'url': globals.router.url_for_path('execute/command/scan_for_addons'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Scan for plugin-addons', - 'plot': 'Scan for addons that can be used by AKL (launchers, scrapers etc.)', + 'title': kodi.translate(40901), + 'plot': kodi.translate(42019), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Show plugin-addons', + 'name': kodi.translate(40902), 'url': globals.router.url_for_path('execute/command/show_addons'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Show plugin-addons', - 'plot': 'Shows previously scanned addons that can be used by AKL (launchers, scrapers etc.)', + 'title': kodi.translate(40902), + 'plot': kodi.translate(42020), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Manage ROM tags', + 'name': kodi.translate(40903), 'url': globals.router.url_for_path('execute/command/manage_rom_tags'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Manage ROM tags', - 'plot': 'Manage existing/available tags for ROMs', + 'title': kodi.translate(40903), + 'plot': kodi.translate(42021), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Import category/launcher XML configuration file', + 'name': kodi.translate(40904), 'url': globals.router.url_for_path('execute/command/import_launchers'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Import category/launcher XML configuration file', - 'plot': 'Execute several [COLOR orange]Utilities[/COLOR].', + 'title': kodi.translate(40904), + 'plot': kodi.translate(42022), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Export category/rom collection XML configuration file', + 'name': kodi.translate(40905), 'url': globals.router.url_for_path('execute/command/export_to_legacy_xml'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Export category/rom collection XML configuration file', - 'plot': ( - 'Exports all AKL categories and collections into an XML configuration file. ' - 'You can later reimport this XML file.'), + 'title': kodi.translate(40905), + 'plot': kodi.translate(42023), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Check collections', + 'name': kodi.translate(40906), 'url': globals.router.url_for_path('execute/command/check_collections'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Check collections', - 'plot': ('Check all collections for missing launchers or scanners, missing artwork, ' - 'wrong platform names, asset path existence, etc.'), + 'title': kodi.translate(40906), + 'plot': kodi.translate(42024), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Check ROMs artwork image integrity', + 'name': kodi.translate(40907), 'url': globals.router.url_for_path('execute/command/check_rom_artwork_integrity'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Check ROMs artwork image integrity', - 'plot': ('Scans existing [COLOR=orange]ROMs artwork images[/COLOR] in ROM Collections ' - 'and verifies that the images have correct extension ' - 'and size is greater than 0. You can delete corrupted images to be rescraped later.'), + 'title': kodi.translate(40907), + 'plot': kodi.translate(42025), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Delete ROMs redundant artwork', + 'name': kodi.translate(40908), 'url': globals.router.url_for_path('execute/command/delete_redundant_rom_artwork'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Delete ROMs redundant artwork', - 'plot': ('Scans all ROM collections and finds ' - '[COLOR orange]redundant ROMs artwork[/COLOR]. You may delete these unneeded images.'), + 'title': kodi.translate(40908), + 'plot': kodi.translate(42026), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } }) container['items'].append({ - 'name': 'Show detected No-Intro/Redump DATs', + 'name': kodi.translate(40909), 'url': globals.router.url_for_path('execute/command/EXECUTE_UTILS_SHOW_DETECTED_DATS'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Show detected No-Intro/Redump DATs', - 'plot': ('Display the auto-detected No-Intro/Redump DATs that will be used for the ' - 'ROM audit. You have to configure the DAT directories in ' - '[COLOR orange]AKL addon settings[/COLOR], [COLOR=orange]ROM Audit[/COLOR] tab.'), + 'title': kodi.translate(40909), + 'plot': kodi.translate(42027), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, @@ -477,13 +469,13 @@ def qry_get_globalreport_items(): # --- Global ROM statistics --- container['items'].append({ - 'name': 'Global ROM statistics', + 'name': kodi.translate(40910), 'url': globals.router.url_for_path('execute/command/global_rom_stats'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Global ROM statistics', - 'plot': 'Shows a report of all ROM collections with number of ROMs.', + 'title': kodi.translate(40910), + 'plot': kodi.translate(42028), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, @@ -492,14 +484,13 @@ def qry_get_globalreport_items(): # --- Global ROM Audit statistics --- container['items'].append({ - 'name': 'Global ROM Audit statistics (All)', + 'name': kodi.translate(40911), 'url': globals.router.url_for_path('execute/command/EXECUTE_GLOBAL_AUDIT_STATS_ALL'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Global ROM Audit statistics (All)', - 'plot': ('Shows a report of all audited ROM collections, with Have, Miss and Unknown ' - 'statistics.'), + 'title': kodi.translate(40911), + 'plot': kodi.translate(42029), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, @@ -507,14 +498,13 @@ def qry_get_globalreport_items(): }) container['items'].append({ - 'name': 'Global ROM Audit statistics (No-Intro only)', + 'name': kodi.translate(40912), 'url': globals.router.url_for_path('execute/command/EXECUTE_GLOBAL_AUDIT_STATS_NOINTRO'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Global ROM Audit statistics (No-Intro only)', - 'plot': ('Shows a report of all audited ROM Launchers, with Have, Miss and Unknown ' - 'statistics. Only No-Intro platforms (cartridge-based) are reported.'), + 'title': kodi.translate(40912), + 'plot': kodi.translate(42030), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, @@ -522,14 +512,13 @@ def qry_get_globalreport_items(): }) container['items'].append({ - 'name': 'Global ROM Audit statistics (Redump only)', + 'name': kodi.translate(40913), 'url': globals.router.url_for_path('execute/command/EXECUTE_GLOBAL_AUDIT_STATS_REDUMP'), 'is_folder': False, 'type': 'video', 'info': { - 'title': 'Global ROM Audit statistics (Redump only)', - 'plot': ('Shows a report of all audited ROM Launchers, with Have, Miss and Unknown ' - 'statistics. Only Redump platforms (optical-based) are reported.'), + 'title': kodi.translate(40913), + 'plot': kodi.translate(42031), 'overlay': 4 }, 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, @@ -558,23 +547,23 @@ def qry_container_context_menu_items(container_data) -> typing.List[typing.Tuple commands = [] if is_category: - commands.append(('Rebuild {} view'.format(container_name), + commands.append((kodi.translate(40893).format(container_name), _context_menu_url_for('execute/command/render_category_view',{'category_id':container_id}))) if is_romcollection: - commands.append(('Search ROM in collection', _context_menu_url_for(f'/collection/{container_id}/search'))) - commands.append(('Rebuild {} view'.format(container_name), + commands.append((kodi.translate(40894), _context_menu_url_for(f'/collection/{container_id}/search'))) + commands.append((kodi.translate(40893).format(container_name), _context_menu_url_for('execute/command/render_romcollection_view', {'romcollection_id':container_id}))) if is_virtual_category and not is_root: - commands.append(('Rebuild {} view'.format(container_name), + commands.append((kodi.translate(40893).format(container_name), _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':container_id}))) if is_virtual_collection: - commands.append(('Rebuild {} view'.format(container_name), + commands.append((kodi.translate(40893).format(container_name), _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':container_parentid}))) commands.append((kodi.translate(40856), _context_menu_url_for('execute/command/render_views'))) - commands.append(('Open Kodi file manager', 'ActivateWindow(filemanager)')) - commands.append(('AKL addon settings', 'Addon.OpenSettings({0})'.format(globals.addon_id))) + commands.append((kodi.translate(40895), 'ActivateWindow(filemanager)')) + commands.append((kodi.translate(40896), 'Addon.OpenSettings({0})'.format(globals.addon_id))) return commands @@ -605,29 +594,29 @@ def qry_listitem_context_menu_items(list_item_data, container_data)-> typing.Lis commands = [] if is_rom: - commands.append(('View ROM', _context_menu_url_for(f'/rom/view/{item_id}'))) - commands.append(('Edit ROM', _context_menu_url_for(f'/rom/edit/{item_id}'))) - commands.append(('Link ROM in other collection', _context_menu_url_for('/execute/command/link_rom',{'rom_id':item_id}))) - commands.append(('Add ROM to AKL Favourites', _context_menu_url_for('/execute/command/add_rom_to_favourites',{'rom_id':item_id}))) + commands.append((kodi.translate(40882), _context_menu_url_for(f'/rom/view/{item_id}'))) + commands.append((kodi.translate(40883), _context_menu_url_for(f'/rom/edit/{item_id}'))) + commands.append((kodi.translate(40884), _context_menu_url_for('/execute/command/link_rom',{'rom_id':item_id}))) + commands.append((kodi.translate(40885), _context_menu_url_for('/execute/command/add_rom_to_favourites',{'rom_id':item_id}))) if is_category: - commands.append(('View Category', _context_menu_url_for(f'/categories/view/{item_id}'))) - commands.append(('Edit Category', _context_menu_url_for(f'/categories/edit/{item_id}'))) - commands.append(('Add new Category',_context_menu_url_for(f'/categories/add/{item_id}/in/{container_id}'))) - commands.append(('Add new ROM Collection', _context_menu_url_for(f'/romcollection/add/{item_id}/in/{container_id}'))) - commands.append(( 'Add new ROM (Standalone)', _context_menu_url_for(f'/categories/addrom/{item_id}/in/{container_id}'))) + commands.append((kodi.translate(40886), _context_menu_url_for(f'/categories/view/{item_id}'))) + commands.append((kodi.translate(40887), _context_menu_url_for(f'/categories/edit/{item_id}'))) + commands.append((kodi.translate(40888),_context_menu_url_for(f'/categories/add/{item_id}/in/{container_id}'))) + commands.append((kodi.translate(40889), _context_menu_url_for(f'/romcollection/add/{item_id}/in/{container_id}'))) + commands.append((kodi.translate(40890), _context_menu_url_for(f'/categories/addrom/{item_id}/in/{container_id}'))) if is_romcollection: - commands.append(('View ROM Collection', _context_menu_url_for(f'/romcollection/view/{item_id}'))) - commands.append(('Edit ROM Collection', _context_menu_url_for(f'/romcollection/edit/{item_id}'))) + commands.append((kodi.translate(40891), _context_menu_url_for(f'/romcollection/view/{item_id}'))) + commands.append((kodi.translate(40892), _context_menu_url_for(f'/romcollection/edit/{item_id}'))) if not is_category and container_is_category: - commands.append(('Add new Category',_context_menu_url_for(f'/categories/add/{container_id}'))) - commands.append(('Add new ROM Collection', _context_menu_url_for(f'/romcollection/add/{container_id}'))) - commands.append(('Add new ROM (Standalone)', _context_menu_url_for(f'/categories/addrom/{container_id}'))) + commands.append((kodi.translate(40888),_context_menu_url_for(f'/categories/add/{container_id}'))) + commands.append((kodi.translate(40889), _context_menu_url_for(f'/romcollection/add/{container_id}'))) + commands.append((kodi.translate(40890), _context_menu_url_for(f'/categories/addrom/{container_id}'))) if is_virtual_category: - commands.append((f'Rebuild {item_name} view', _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':item_id}))) + commands.append((kodi.translate(40893).format(item_name), _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':item_id}))) return commands diff --git a/resources/lib/views.py b/resources/lib/views.py index a1e37da1..f4493acd 100644 --- a/resources/lib/views.py +++ b/resources/lib/views.py @@ -77,7 +77,7 @@ def run_plugin(addon_argv): router.run(argv) except Exception as e: logger.error('Exception while executing route', exc_info=e) - kodi.notify_error('Failed to execute route or command') + kodi.notify_error(kodi.translate(40960)) logger.debug('Advanced Kodi Launcher run_plugin() exit') @@ -106,12 +106,12 @@ def vw_route_render_collection(view_id: str): filter = vw_create_filter(filter_type, filter_term) if container is None: - kodi.notify('Current view is not rendered correctly. Re-render views first.') + kodi.notify(kodi.translate(40961)) elif len(container['items']) == 0: if container_type == constants.OBJ_CATEGORY: - kodi.notify('Category {} has no items. Add romcollections or categories first.'.format(container['name'])) + kodi.notify(kodi.translate(40995).format(container['name'])) if container_type == constants.OBJ_ROMCOLLECTION or container_type == constants.OBJ_COLLECTION_VIRTUAL: - kodi.notify('Collection {} has no items. Add ROMs'.format(container['name'])) + kodi.notify(kodi.translate(40996).format(container['name'])) else: _render_list_items(container, container_context_items, filter) @@ -136,13 +136,13 @@ def vw_route_render_virtual_view(view_id: str): filter = vw_create_filter(filter_type, filter_term) if container is None: - kodi.notify('Current view is not rendered correctly. Re-render views first.') + kodi.notify(kodi.translate(40961)) elif len(container['items']) == 0: if container_type == constants.OBJ_CATEGORY_VIRTUAL: - if kodi.dialog_yesno(f"Virtual category '{container['name']}'' has no items. Regenerate the views now?"): + if kodi.dialog_yesno(kodi.translate(41048).format(container['name'])): AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': container['id']}) if container_type == constants.OBJ_COLLECTION_VIRTUAL: - if kodi.dialog_yesno(f"Virtual collection {container['name']} has no items. Regenerate the views now?"): + if kodi.dialog_yesno(kodi.translate(41049).format(container['name'])): AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': container['parent_id']}) else: _render_list_items(container, container_context_items, filter) @@ -269,7 +269,7 @@ def vw_view_rom_metadata(rom_id): def vw_view_rom_metadata(rom_id): field = router.args['field'][0] if 'field' in router.args else None if not field: - kodi.notify_warn("No field specified") + kodi.notify_warn(kodi.translate(40997)) return container = viewqueries.qry_get_view_scanned_data(rom_id) requested_item = next((i for i in container['items'] if i['name'] == field), None) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index f4879708..31677e87 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -55,7 +55,7 @@ def is_alive(self): s.connect((WebService.HOST, WebService.PORT)) s.sendall("") except Exception as error: - logger.fatal('Exception in webservice.is_alive {}'.format(str(error))) + logger.fatal(f'Exception in webservice.is_alive {str(error)}') if 'Errno 61' in str(error) or 'WinError 10061' in str(error): alive = False finally: @@ -72,8 +72,8 @@ def stop(self, check_alive = False): ''' Called when the thread needs to stop ''' try: - logger.info("Stopping AKL webservice({}:{})".format(WebService.HOST, WebService.PORT)) - conn = HTTPConnection("{}:{}".format(WebService.HOST, WebService.PORT)) + logger.info(f"Stopping AKL webservice({WebService.HOST}:{WebService.PORT})") + conn = HTTPConnection(f"{WebService.HOST}:{WebService.PORT}") conn.request("QUIT", "/") conn.getresponse() except Exception as error: diff --git a/service.py b/service.py index 8d5096c2..65b9e636 100644 --- a/service.py +++ b/service.py @@ -16,6 +16,6 @@ AppService().run() except Exception as ex: logger.fatal('Exception in plugin', exc_info=ex) - kodi.notify_error("General failure") + kodi.notify_error(kodi.translate(40956)) logger.debug("'%s' shutting down." % addon_id) diff --git a/tests/addon_commands_test.py b/tests/addon_commands_test.py index c3c31dbd..7c9133af 100644 --- a/tests/addon_commands_test.py +++ b/tests/addon_commands_test.py @@ -57,12 +57,14 @@ def repository_update(entity): def repository_empty(): return [] + @patch('akl.utils.kodi.translate') @patch('xbmcaddon.Addon', side_effect = mocked_addons) @patch('resources.lib.repositories.AelAddonRepository.insert_addon', side_effect = repository_save) @patch('resources.lib.repositories.AelAddonRepository.update_addon', side_effect = repository_update) @patch('resources.lib.repositories.AelAddonRepository.find_all', side_effect = repository_empty) @patch('akl.utils.kodi.jsonrpc_query') - def test_saving_new_addons(self, jsonrpc_response_mock, find_all_mock, repo_update_mock, repo_save_mock, addon_mock): + def test_saving_new_addons(self, jsonrpc_response_mock, find_all_mock, repo_update_mock, + repo_save_mock, addon_mock, translate_mock): # arrange Test_cmd_scan_addons.mocked_addons_collection = { 'plugin.mock.A': tests.fakes.FakeAddon({ 'version': '1.2.3', 'akl.enabled': 'true', 'akl.plugin_types': 'SCANNER', 'akl.scanner.friendlyname': 'MockA' }), From e692670876d90ae382e7a4b7a483efbc4cfd4854 Mon Sep 17 00:00:00 2001 From: chrisism Date: Wed, 31 Jan 2024 17:08:59 +0100 Subject: [PATCH 09/71] Made webservice port customizable --- changelog.md | 1 + .../resource.language.en_gb/strings.po | 4 ++ resources/lib/domain.py | 26 +++---- resources/lib/globals.py | 67 ++++++++++--------- resources/lib/webservice.py | 21 +++--- resources/settings.xml | 7 ++ 6 files changed, 73 insertions(+), 53 deletions(-) diff --git a/changelog.md b/changelog.md index 9aa220fc..7dc5c479 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - Changed database migrations system - Added 'edit platform' to ROMs - Changed platform can be applied to all ROMs in a collection +- AKL webservice port number can be changed through settings ## Previous - Custom skin view for View ROM diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index d8447d9b..57a678d1 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -211,6 +211,10 @@ msgctxt "#40613" msgid "Automatic fallback to Retroplayer" msgstr "settings.xml" +msgctxt "#40614" +msgid "AKL webserver port to use (restart required)" +msgstr "settings.xml" + ############################ # Scraping settings ############################ diff --git a/resources/lib/domain.py b/resources/lib/domain.py index 3ed2e775..a3e38ef4 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -22,7 +22,7 @@ import abc import typing import logging -import re +import re import time import datetime import json @@ -377,7 +377,7 @@ def get_launch_command(self, rom: ROM) -> dict: '--cmd': 'launch', '--type': constants.AddonType.LAUNCHER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--akl_addon_id': self.get_id(), '--rom_id': rom.get_id() } @@ -387,7 +387,7 @@ def get_configure_command(self, romcollection: ROMCollection) -> dict: '--cmd': 'configure', '--type': constants.AddonType.LAUNCHER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--romcollection_id': romcollection.get_id(), '--akl_addon_id': self.get_id() } @@ -397,7 +397,7 @@ def get_configure_command_for_rom(self, rom: ROM) -> dict: '--cmd': 'configure', '--type': constants.AddonType.LAUNCHER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--rom_id': rom.get_id(), '--akl_addon_id': self.get_id() } @@ -407,12 +407,12 @@ def launch(self, rom: ROM): self.addon.get_addon_id(), self.get_launch_command(rom)) - def configure(self, romcollection:ROMCollection): + def configure(self, romcollection: ROMCollection): kodi.run_script( self.addon.get_addon_id(), self.get_configure_command(romcollection)) - def configure_for_rom(self, rom:ROM): + def configure_for_rom(self, rom: ROM): kodi.run_script( self.addon.get_addon_id(), self.get_configure_command_for_rom(rom)) @@ -456,7 +456,7 @@ def configure(self, romcollection: ROMCollection): 'addon_id': self.addon.get_addon_id(), 'settings': {} } - is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, globals.WEBSERVER_PORT, post_data) + is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, settings.getSettingAsInt('webserver_port'), post_data) if not is_stored: kodi.notify_error(kodi.translate(40958)) @@ -467,7 +467,7 @@ def configure_for_rom(self, rom: ROM): 'addon_id': self.addon.get_addon_id(), 'settings': {} } - is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, globals.WEBSERVER_PORT, post_data) + is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, settings.getSettingAsInt('webserver_port'), post_data) if not is_stored: kodi.notify_error(kodi.translate(40958)) @@ -481,7 +481,7 @@ def get_scan_command(self, rom_collection: ROMCollection) -> dict: '--cmd': 'scan', '--type': constants.AddonType.SCANNER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--romcollection_id': rom_collection.get_id(), '--akl_addon_id': self.get_id() } @@ -491,9 +491,9 @@ def get_configure_command(self, romcollection: ROMCollection) -> dict: '--cmd': 'configure', '--type': constants.AddonType.SCANNER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--romcollection_id': romcollection.get_id(), - '--akl_addon_id': self.get_id() + '--akl_addon_id': self.get_id() } class ScraperAddon(ROMAddon): @@ -556,7 +556,7 @@ def get_scrape_command(self, rom: ROM)-> dict: '--cmd': 'scrape', '--type': constants.AddonType.SCRAPER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--akl_addon_id': self.addon.get_id(), '--rom_id': rom.get_id(), '--settings': io.parse_to_json_arg(self.get_settings()) @@ -567,7 +567,7 @@ def get_scrape_command_for_collection(self, collection: ROMCollection) -> dict: '--cmd': 'scrape', '--type': constants.AddonType.SCRAPER.name, '--server_host': globals.WEBSERVER_HOST, - '--server_port': globals.WEBSERVER_PORT, + '--server_port': settings.getSettingAsInt('webserver_port'), '--akl_addon_id': self.addon.get_id(), '--romcollection_id': collection.get_id(), '--settings': io.parse_to_json_arg(self.get_settings()) diff --git a/resources/lib/globals.py b/resources/lib/globals.py index e422a5fc..8d219fa8 100644 --- a/resources/lib/globals.py +++ b/resources/lib/globals.py @@ -33,34 +33,35 @@ addon_type = addon.getAddonInfo('type') addon_path = addon.getAddonInfo('path') + # --- Addon paths and constant definition --- # _PATH is a filename | _DIR is a directory. class AKL_Paths(object): def __init__(self, addon_id): # --- Base paths --- - self.HOME_DIR = io.FileName('special://home') - self.PROFILE_DIR = io.FileName('special://profile') - self.ADDONS_DATA_DIR = io.FileName('special://profile/addon_data') - #self.DATABASE_DIR = io.FileName('special://database') + self.HOME_DIR = io.FileName('special://home') + self.PROFILE_DIR = io.FileName('special://profile') + self.ADDONS_DATA_DIR = io.FileName('special://profile/addon_data') + # self.DATABASE_DIR = io.FileName('special://database') - self.ADDON_DATA_DIR = self.ADDONS_DATA_DIR.pjoin(addon_id) - self.ADDONS_CODE_DIR = self.HOME_DIR.pjoin('addons', True) - self.ADDON_CODE_DIR = self.ADDONS_CODE_DIR.pjoin(addon_id) - self.ICON_FILE_PATH = self.ADDON_CODE_DIR.pjoin('media/icon.png') - self.FANART_FILE_PATH = self.ADDON_CODE_DIR.pjoin('media/fanart.jpg') - self.DATABASE_SCHEMA_PATH = self.ADDON_CODE_DIR.pjoin('resources/schema.sql') - self.DATABASE_MIGRATIONS_PATH = self.ADDON_CODE_DIR.pjoin('resources/migrations', True) + self.ADDON_DATA_DIR = self.ADDONS_DATA_DIR.pjoin(addon_id) + self.ADDONS_CODE_DIR = self.HOME_DIR.pjoin('addons', True) + self.ADDON_CODE_DIR = self.ADDONS_CODE_DIR.pjoin(addon_id) + self.ICON_FILE_PATH = self.ADDON_CODE_DIR.pjoin('media/icon.png') + self.FANART_FILE_PATH = self.ADDON_CODE_DIR.pjoin('media/fanart.jpg') + self.DATABASE_SCHEMA_PATH = self.ADDON_CODE_DIR.pjoin('resources/schema.sql') + self.DATABASE_MIGRATIONS_PATH = self.ADDON_CODE_DIR.pjoin('resources/migrations', True) # -- Root data file - self.ROOT_PATH = self.ADDON_DATA_DIR.pjoin('root.json') + self.ROOT_PATH = self.ADDON_DATA_DIR.pjoin('root.json') # --- Databases and reports --- - self.DATABASE_FILE_PATH = self.ADDON_DATA_DIR.pjoin('akl.db') + self.DATABASE_FILE_PATH = self.ADDON_DATA_DIR.pjoin('akl.db') # --- datetime peek file for automatic scanning --- - self.SCAN_INDICATOR_FILE = self.ADDON_DATA_DIR.pjoin('auto_scan.txt') + self.SCAN_INDICATOR_FILE = self.ADDON_DATA_DIR.pjoin('auto_scan.txt') # --- Offline scraper databases --- - self.GAMEDB_INFO_DIR = self.ADDON_CODE_DIR.pjoin('data-AOS') - self.GAMEDB_JSON_BASE_NOEXT = 'AOS_GameDB_info' + self.GAMEDB_INFO_DIR = self.ADDON_CODE_DIR.pjoin('data-AOS') + self.GAMEDB_JSON_BASE_NOEXT = 'AOS_GameDB_info' # self.LAUNCHBOX_INFO_DIR = self.ADDON_CODE_DIR.pjoin('LaunchBox') # self.LAUNCHBOX_JSON_BASE_NOEXT = 'LaunchBox_info' @@ -68,22 +69,22 @@ def __init__(self, addon_id): self.SCRAPER_CACHE_DIR = self.ADDON_DATA_DIR.pjoin('ScraperCache') # --- Artwork and NFO for Categories and Launchers --- - self.DEFAULT_CAT_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-categories') - self.DEFAULT_COL_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-collections') - self.DEFAULT_LAUN_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-launchers') - self.DEFAULT_FAV_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-favourites') + self.DEFAULT_CAT_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-categories') + self.DEFAULT_COL_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-collections') + self.DEFAULT_LAUN_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-launchers') + self.DEFAULT_FAV_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-favourites') # --- Rendered views (normal and virtuals/generated) --- - self.GENERATED_VIEWS_DIR = self.ADDON_DATA_DIR.pjoin('db_generated_views') - self.VIEWS_DIR = self.ADDON_DATA_DIR.pjoin('db_views') + self.GENERATED_VIEWS_DIR = self.ADDON_DATA_DIR.pjoin('db_generated_views') + self.VIEWS_DIR = self.ADDON_DATA_DIR.pjoin('db_views') # Reports - self.REPORTS_DIR = self.ADDON_DATA_DIR.pjoin('reports') - self.BIOS_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_BIOS.txt') - self.COLLECTIONS_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_collections.txt') - self.ROM_SYNC_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_ROM_sync_status.txt') - self.ROM_ART_INTEGRITY_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_ROM_artwork_integrity.txt') - self.ROM_REDUNDANT_FILES_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_ROM_redundant_files.txt') + self.REPORTS_DIR = self.ADDON_DATA_DIR.pjoin('reports') + self.BIOS_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_BIOS.txt') + self.COLLECTIONS_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_collections.txt') + self.ROM_SYNC_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_ROM_sync_status.txt') + self.ROM_ART_INTEGRITY_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_ROM_artwork_integrity.txt') + self.ROM_REDUNDANT_FILES_REPORT_FILE_PATH = self.REPORTS_DIR.pjoin('report_ROM_redundant_files.txt') def build(self): # --- Addon data paths creation --- @@ -109,16 +110,18 @@ def build(self): self.VIEWS_DIR.makedirs() return self - + + router: routing.Plugin = routing.Plugin() g_PATHS: AKL_Paths WEBSERVER_HOST = '127.0.0.1' -WEBSERVER_PORT = 57300 +WEBSERVER_PORT = 5738 + + # # Bootstrap factory object instances. # def g_bootstrap_instances(): - global g_PATHS + global g_PATHS g_PATHS = AKL_Paths(addon_id).build() - \ No newline at end of file diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 31677e87..d5a18cad 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -27,21 +27,26 @@ from http.client import HTTPConnection # AKL modules +from akl import settings from resources.lib import globals, apiqueries from resources.lib.commands import api_commands logger = logging.getLogger(__name__) + ################################################################################################# class WebService(threading.Thread): - HOST = globals.WEBSERVER_HOST - PORT = globals.WEBSERVER_PORT - ''' Run a webservice for api communication. ''' def __init__(self): self.server = None + self.port = settings.getSettingAsInt('webserver_port') + self.host = globals.WEBSERVER_HOST + + if self.port is None or self.port == 0: + self.port = globals.WEBSERVER_PORT + threading.Thread.__init__(self) def is_alive(self): @@ -52,7 +57,7 @@ def is_alive(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - s.connect((WebService.HOST, WebService.PORT)) + s.connect((self.host, self.port)) s.sendall("") except Exception as error: logger.fatal(f'Exception in webservice.is_alive {str(error)}') @@ -72,8 +77,8 @@ def stop(self, check_alive = False): ''' Called when the thread needs to stop ''' try: - logger.info(f"Stopping AKL webservice({WebService.HOST}:{WebService.PORT})") - conn = HTTPConnection(f"{WebService.HOST}:{WebService.PORT}") + logger.info(f"Stopping AKL webservice({self.host}:{self.port})") + conn = HTTPConnection(f"{self.host}:{self.port}") conn.request("QUIT", "/") conn.getresponse() except Exception as error: @@ -87,8 +92,8 @@ def run(self): ''' self.stop(check_alive=True) - logger.info("Startup AKL webservice({}:{})".format(WebService.HOST, WebService.PORT)) - server = AelHttpServer((WebService.HOST, WebService.PORT), RequestHandler) + logger.info("Startup AKL webservice({}:{})".format(self.host, self.port)) + server = AelHttpServer((self.host, self.port), RequestHandler) try: server.serve_forever() diff --git a/resources/settings.xml b/resources/settings.xml index 38578409..5c1f2fbd 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -303,6 +303,13 @@ + + 1 + 5738 + + 40614 + + 1 7 From 1701e5972fe1345848c8b67e2396693b9dd50e78 Mon Sep 17 00:00:00 2001 From: chrisism Date: Thu, 1 Feb 2024 10:24:37 +0100 Subject: [PATCH 10/71] explicit python version in yml --- .build/main.yml | 4 +++- tests/regex_test.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.build/main.yml b/.build/main.yml index 542f5f03..d82a5a57 100644 --- a/.build/main.yml +++ b/.build/main.yml @@ -67,7 +67,9 @@ stages: condition: eq(${{variables.isMaster}}, true) - task: UsePythonVersion@0 - displayName: 'Use Python 3.x' + displayName: 'Use Python 3.9' + inputs: + versionSpec: '3.9' - task: PipAuthenticate@0 displayName: Authenticate with artifact feed diff --git a/tests/regex_test.py b/tests/regex_test.py index 65990e63..eabf54fb 100644 --- a/tests/regex_test.py +++ b/tests/regex_test.py @@ -15,8 +15,8 @@ class Test_Regex(unittest.TestCase): def test_regex_patterns(self): - test_path = "E:\AEL-stuff\AEL-DATs-No-Intro\Atari - 2600 (20191018-075817).dat" - test_patt = ".*Atari - 2600\s\((\d\d\d\d\d\d\d\d)-(\d\d\d\d\d\d)\)\.dat" + test_path = r"E:\AEL-stuff\AEL-DATs-No-Intro\Atari - 2600 (20191018-075817).dat" + test_patt = r".*Atari - 2600\s\((\d\d\d\d\d\d\d\d)-(\d\d\d\d\d\d)\)\.dat" print('Filename "{}"'.format(test_path)) print('Pattern "{}"'.format(test_patt)) @@ -26,8 +26,8 @@ def test_regex_patterns(self): pprint.pprint(result.groups()) def test_fmatch(self): - test_path = 'E:\AEL-stuff\AEL-DATs-No-Intro\Atari - 2600 (20191018-075817).dat' - test_patt = '*Atari - 2600*.dat' + test_path = r'E:\AEL-stuff\AEL-DATs-No-Intro\Atari - 2600 (20191018-075817).dat' + test_patt = r'*Atari - 2600*.dat' print('Filename "{}"'.format(test_path)) print('Pattern "{}"'.format(test_patt)) From 1d42474369b4e4435f1c88a2e89e641442dd1fe8 Mon Sep 17 00:00:00 2001 From: Christian Jungerius Date: Sun, 3 Mar 2024 23:51:38 +0100 Subject: [PATCH 11/71] Feature/separate sources (#34) * Building new library menu item * Updated authors md * Library rendering for views * Syntax cleanup * Cleanup syntax for flake8 * Implemented Library manage roms * Implemented library actions * Cleanup obsolete functions * Implemented API actions * Added cleanup obsolete files * Fixes in queries and context menu * Schema fixes * Removed kind references * Implementing rules editting * Implemented editting rulesets * Rename to sources * Implemented executing rulesets * Moved Add Standalone * Refactored standalone roms * Implemented ROM linking * Adjusted UX flow for launchers * Fixes for webservices and apis * Updated addon requirements * Fixed tests --- AUTHORS.md | 9 +- addon.py | 2 +- addon.xml | 2 +- changelog.md | 2 + media/theme/Libraries_icon.png | Bin 0 -> 33674 bytes media/theme/Libraries_poster.png | Bin 0 -> 74038 bytes requirements.txt | 2 +- .../resource.language.en_gb/strings.po | 376 ++++- resources/lib/apiqueries.py | 108 +- resources/lib/commands/__init__.py | 4 +- resources/lib/commands/addon_commands.py | 2 +- resources/lib/commands/api_commands.py | 279 ++-- resources/lib/commands/category_commands.py | 70 +- resources/lib/commands/chk_commands.py | 2 +- resources/lib/commands/misc_commands.py | 27 +- resources/lib/commands/rom_commands.py | 152 ++- .../lib/commands/rom_launcher_commands.py | 487 +++++-- .../lib/commands/rom_scanner_commands.py | 214 --- .../lib/commands/rom_scraper_commands.py | 179 ++- .../lib/commands/romcollection_commands.py | 145 +- .../commands/romcollection_roms_commands.py | 463 ++++--- resources/lib/commands/source_commands.py | 632 +++++++++ .../lib/commands/view_rendering_commands.py | 192 ++- resources/lib/domain.py | 1209 +++++++++++------ resources/lib/editors.py | 137 +- resources/lib/globals.py | 8 +- resources/lib/queries.py | 480 ++++--- resources/lib/report.py | 4 +- resources/lib/repositories.py | 1022 ++++++++------ resources/lib/services.py | 4 +- resources/lib/viewqueries.py | 474 ++++--- resources/lib/views.py | 208 ++- resources/lib/webservice.py | 48 +- resources/migrations/1.5.0_002.sql | 375 +++++ resources/schema.sql | 255 ++-- tests/misc_commands_test.py | 3 +- tests/view_rendering_commands_test.py | 12 +- 37 files changed, 5160 insertions(+), 2428 deletions(-) create mode 100644 media/theme/Libraries_icon.png create mode 100644 media/theme/Libraries_poster.png delete mode 100644 resources/lib/commands/rom_scanner_commands.py create mode 100644 resources/lib/commands/source_commands.py create mode 100644 resources/migrations/1.5.0_002.sql diff --git a/AUTHORS.md b/AUTHORS.md index 6c8ca057..3fe4e8f4 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,21 +1,22 @@ -## Advanced Kodi Launcher +# Advanced Kodi Launcher **Advanced Kodi Launcher** was another (friendly) fork from Advanced Emulator Launcher version 0.10.0 in 2020 by [Chrisism](mailto:crizizz@gmail.com). AKL closely follows AEL, but focusses on being more modulair and being able to support different types of launcher applications. +Kodi forum thread: https://forum.kodi.tv/showthread.php?tid=366351 +# Advanced Emulator Launcher Advanced Emulator Launcher was originally forked from Advanced Launcher version 2.5.8 on May 2016 by Wintermute0110 . First public released version of Advanced Emulator Launcher was 0.9.0 on Aug 2016. Kodi forum thread: http://forum.kodi.tv/showthread.php?tid=287826 - AEL contributors: Chrisism, Paradix, Rufoo. -# Advanced Launcher # +# Advanced Launcher Advanced Launcher was originally forked from Launcher by Angelscry. First version of Advanced Launcher was released on November 2010. Other people credited in @@ -24,7 +25,7 @@ Advanced Launcher are JustSomeUser, CinPoU, Leo212, Zerqent, Zosky, and Atsumori Kodi forum thread: http://forum.kodi.tv/showthread.php?tid=85724 -# Launcher # +# Launcher Launcher was the original project that started it all. First Launcher version was released on August 2008. Launcher was developed by JustSomeUser, CinPoU, diff --git a/addon.py b/addon.py index 2275dc7c..a90b3310 100644 --- a/addon.py +++ b/addon.py @@ -4,7 +4,7 @@ # # Copyright (c) Chrisism -# Big Portions (c) Wintermute0110 +# Big Portions (c) Wintermute0110 # Portions (c) 2010-2015 Angelscry and others # # This program is free software; you can redistribute it and/or modify diff --git a/addon.xml b/addon.xml index d6b6dcf2..8b13bd89 100644 --- a/addon.xml +++ b/addon.xml @@ -3,7 +3,7 @@ - + executable game diff --git a/changelog.md b/changelog.md index 7dc5c479..16cf4154 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,6 @@ ## Current +- Separated launchers +- Implemented sources - Added extra field to metadata item - Search term mode applicable for multi ROM scraping - Refactoring of default asset mapping diff --git a/media/theme/Libraries_icon.png b/media/theme/Libraries_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf3d93fe858b15cd79e42b4ecf2fbb19ea0dc856 GIT binary patch literal 33674 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 literal 0 HcmV?d00001 diff --git a/media/theme/Libraries_poster.png b/media/theme/Libraries_poster.png new file mode 100644 index 0000000000000000000000000000000000000000..89ba0f2888d97a8b8cc6b95baf8f9fdb26ada140 GIT binary patch literal 74038 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 DvxA&C literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index bb0665ad..ac013e01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ kodi-addon-checker==0.0.26 Kodistubs==19.0.3 routing==0.2.3 pytest==6.2.5 -script.module.akl==1.0.12 +script.module.akl==1.1.0-rc0 requests==2.22.0 flake8==5.0.4 diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 57a678d1..6dbab558 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -343,6 +343,18 @@ msgctxt "#40816" msgid "Default box size" msgstr "" +msgctxt "#40817" +msgid "Is Favourite" +msgstr "" + +msgctxt "#40818" +msgid "Is Favourite" +msgstr "" + +msgctxt "#40819" +msgid "Times launched" +msgstr "" + ############################ # Constant Enum values ############################ @@ -436,6 +448,38 @@ msgctxt "#42505" msgid "ROM" msgstr "domain" +msgctxt "#42506" +msgid "Source" +msgstr "domain" + +msgctxt "#42507" +msgid "No Type" +msgstr "domain" + +msgctxt "#42508" +msgid "All" +msgstr "domain" + +msgctxt "#42509" +msgid "Ruleset" +msgstr "domain" + +msgctxt "#42510" +msgid "Rules" +msgstr "domain" + +msgctxt "#42511" +msgid "Rule" +msgstr "domain" + +msgctxt "#42512" +msgid "Categories" +msgstr "domain" + +msgctxt "#42513" +msgid "Collections" +msgstr "domain" + ############################ # Advanced Enum values ############################ @@ -460,6 +504,42 @@ msgctxt "#30915" msgid "DEBUG" msgstr "LOG ENUM" +############################ +# Domain Enum values +############################ + +msgctxt "#30916" +msgid "all of the rules" +msgstr "RuleSetOperator" + +msgctxt "#30917" +msgid "one or more of the rules" +msgstr "RuleSetOperator" + +msgctxt "#30918" +msgid "Equals" +msgstr "RuleOperator" + +msgctxt "#30919" +msgid "Not Equals" +msgstr "RuleOperator" + +msgctxt "#30920" +msgid "Contains" +msgstr "RuleOperator" + +msgctxt "#30921" +msgid "Does not contain" +msgstr "RuleOperator" + +msgctxt "#30922" +msgid "More than" +msgstr "RuleOperator" + +msgctxt "#30923" +msgid "Less than" +msgstr "RuleOperator" + ################################################################################################################ ############################ @@ -603,7 +683,7 @@ msgid "Edit ROM" msgstr "" msgctxt "#40884" -msgid "Link ROM in other collection" +msgid "Link ROM in other collections/categories" msgstr "" msgctxt "#40885" @@ -627,7 +707,7 @@ msgid "Add new ROM Collection" msgstr "" msgctxt "#40890" -msgid "Add new ROM (Standalone)" +msgid "Standalone ROM" msgstr "" msgctxt "#40891" @@ -722,6 +802,38 @@ msgctxt "#40913" msgid "Global ROM Audit statistics (Redump only)" msgstr "" +msgctxt "#40914" +msgid "Sources" +msgstr "" + +msgctxt "#40915" +msgid "Edit Source" +msgstr "" + +msgctxt "#40916" +msgid "Add Source" +msgstr "" + +msgctxt "#40917" +msgid "Add Launcher" +msgstr "" + +msgctxt "#40918" +msgid "Edit Launcher" +msgstr "" + +msgctxt "#40919" +msgid "Remove Launcher" +msgstr "" + +msgctxt "#40920" +msgid "Launchers" +msgstr "" + +msgctxt "#40921" +msgid "Add new ruleset" +msgstr "" + ############################ # Headers and texts / notifications / dialogs ############################ @@ -734,11 +846,11 @@ msgid "Invalid parameters supplied." msgstr "" msgctxt "#40952" -msgid "Is it a directly launchable file/executable?\nChoose no if you want to assign a separate launcher afterwards." +msgid "File is directly executable/launchable" msgstr "" msgctxt "#40953" -msgid "Select file" +msgid "Select file to execute (Skip if not available)" msgstr "" msgctxt "#40954" @@ -854,7 +966,7 @@ msgid "Creating new AKL database" msgstr "" msgctxt "#40982" -msgid "No scanners configured for this romcollection!" +msgid "Should new platform be applied to existing ROMs in this source?" msgstr "" msgctxt "#40983" @@ -926,7 +1038,7 @@ msgid "Scan completed. Found {0} addons" msgstr "" msgctxt "#41000" -msgid "No ROM scanners configured." +msgid "Scrape collection '{0}' ROMs with '{1}'" msgstr "" msgctxt "#41001" @@ -938,7 +1050,7 @@ msgid "No launchers configured for this ROM!" msgstr "" msgctxt "#41003" -msgid "No launchers configured for this collection." +msgid "No launchers associated for this collection." msgstr "" msgctxt "#41004" @@ -954,7 +1066,7 @@ msgid "Configured ROM scanner '{0}'" msgstr "" msgctxt "#41007" -msgid "Stored scanned ROMS in ROMs Collection '{0}'" +msgid "Stored scanned ROMS in source '{0}'" msgstr "" msgctxt "#41008" @@ -966,7 +1078,7 @@ msgid "Stored scraped ROM '{0}'" msgstr "" msgctxt "#41010" -msgid "Removed ROMS from ROMs Collection '{0}'" +msgid "Removed ROMS from Source '{0}'" msgstr "" msgctxt "#41011" @@ -1162,11 +1274,11 @@ msgid "Clear all tags from ROM '{0}'?" msgstr "" msgctxt "#41059" -msgid "Are you sure to delete launcher '{}'?" +msgid "Are you sure to unassociate launcher '{}'?" msgstr "" msgctxt "#41060" -msgid "Are you sure to delete ROM scanner '{}'?" +msgid "Items must match" msgstr "" msgctxt "#41061" @@ -1350,7 +1462,7 @@ msgid "Choose launcher" msgstr "" msgctxt "#41106" -msgid "Manage ROM scanners for {}" +msgid "Choose launcher addon type" msgstr "" msgctxt "#41107" @@ -1362,15 +1474,15 @@ msgid "Choose scanner to edit" msgstr "" msgctxt "#41109" -msgid "Choose scanner to remove" -msgstr "" +msgid "Added launcher '{0}'" +msgstr "" msgctxt "#41110" -msgid "Choose ROM scanner" +msgid "Scrape source '{0}' ROMs" msgstr "" msgctxt "#41111" -msgid "Scrape collection '{0}' ROMs with '{1}'" +msgid "Scrape source '{0}' ROMs with '{1}'" msgstr "" msgctxt "#41112" @@ -1437,10 +1549,6 @@ msgctxt "#41127" msgid "Edit Launcher '{0}' metadata" msgstr "" -msgctxt "#41128" -msgid "Move to category" -msgstr "" - msgctxt "#41128" msgid "Manage ROM Collection '{}' ROMs" msgstr "" @@ -1450,7 +1558,7 @@ msgid "ROM Asset directories" msgstr "" msgctxt "#41130" -msgid "Import ROMs in ROMCollection '{}'" +msgid "Import rules for ROMs in ROMCollection '{}'" msgstr "" msgctxt "#41131" @@ -1490,7 +1598,7 @@ msgid "Directory to store artwork not found.\nConfigure it before you can edit a msgstr "" msgctxt "#41140" -msgid "Unknown obj_instance.get_assets_kind() {}.\nThis is a bug, please report it." +msgid "Unknown object type {}.\nThis is a bug, please report it." msgstr "" msgctxt "#41141" @@ -1573,6 +1681,122 @@ msgctxt "#41160" msgid "Select {} path" msgstr "" +msgctxt "#41161" +msgid "Rendering source views" +msgstr "" + +msgctxt "#41162" +msgid "Manage Source '{}' ROMs" +msgstr "" + +msgctxt "#41163" +msgid "Source has no ROMs. Nothing to do." +msgstr "" + +msgctxt "#41164" +msgid "Source '{0}' has {1} ROMs. Are you sure you want to clear them from this source?" +msgstr "" + +msgctxt "#41165" +msgid "Cleared ROMs from source" +msgstr "" + +msgctxt "#41166" +msgid "New Source Name" +msgstr "" + +msgctxt "#41167" +msgid "Select action for Source {0}" +msgstr "" + +msgctxt "#41168" +msgid "No launchers configured for this source!" +msgstr "" + +msgctxt "#41169" +msgid "Source '{0}' has {1} ROMs. All the references to these ROMs will be deleted also from the database. " +msgstr "" + +msgctxt "#41170" +msgid "Deleted source {0}" +msgstr "" + +msgctxt "#41171" +msgid "Set launcher '{0}' as default?" +msgstr "" + +msgctxt "#41172" +msgid "Select source" +msgstr "" + +msgctxt "#41173" +msgid "Remove all rules" +msgstr "" + +msgctxt "#41174" +msgid "Ruleset for source {0}" +msgstr "" + +msgctxt "#41175" +msgid "This action will remove all rules applied.\nAre you sure to continue?" +msgstr "" + +msgctxt "#41176" +msgid "Removed all rules. Now importing all from source." +msgstr "" + +msgctxt "#41177" +msgid "Select field for rule" +msgstr "" + +msgctxt "#41178" +msgid "Select operator" +msgstr "" + +msgctxt "#41179" +msgid "Value to use for rule" +msgstr "" + +msgctxt "#41180" +msgid "Ruleset updated" +msgstr "" + +msgctxt "#41181" +msgid "Cannot find item" +msgstr "" + +msgctxt "#41182" +msgid "Choose action for rule" +msgstr "" + +msgctxt "#41183" +msgid "ROMs imported" +msgstr "" + +msgctxt "#41184" +msgid "Manage ruleset" +msgstr "" + +msgctxt "#41185" +msgid "Import ROMs by ruleset" +msgstr "" + +msgctxt "#41186" +msgid "Choose destination to link ROM with" +msgstr "" + +msgctxt "#41187" +msgid "Linked ROM {0} in Category {1}" +msgstr "" + +msgctxt "#41188" +msgid "Linked ROM {0} in Collection {1}" +msgstr "" + +msgctxt "#41189" +msgid "Currently importing: {0}" +msgstr "" + ############################ # List/Action options ############################ @@ -1722,7 +1946,7 @@ msgid "No" msgstr "" msgctxt "#42037" -msgid "Set the title of the launcher" +msgid "Set the title of the ROM collection" msgstr "" msgctxt "#42038" @@ -1874,149 +2098,181 @@ msgid "Browse by Rating" msgstr "" msgctxt "#42080" -msgid "Add new scanner" +msgid "Clear ROMs from source" msgstr "" msgctxt "#42081" -msgid "Edit scanner" +msgid "Edit source scanner" msgstr "" msgctxt "#42082" -msgid "Remove scanner" +msgid "Import ROMs" msgstr "" msgctxt "#42083" -msgid "Change root assets path: '{}'" +msgid "Change root assets path" msgstr "" msgctxt "#42084" -msgid "Change {} path: '{}'" +msgid "Change {} path" +msgstr "" + +msgctxt "#42085" +msgid "Delete Source" +msgstr "" + +msgctxt "#42086" +msgid "Add rule" +msgstr "" + +msgctxt "#42087" +msgid "Edit rule" +msgstr "" + +msgctxt "#42088" +msgid "Remove rule" +msgstr "" + +msgctxt "#42089" +msgid "Execute ruleset" +msgstr "" + +msgctxt "#42090" +msgid "Create new launcher..." msgstr "" ############################ # Context menu item descriptions ############################ -msgctxt "#42001" +msgctxt "#44001" msgid "Execute several [COLOR orange]Utilities[/COLOR]." msgstr "viewqueries" -msgctxt "#42002" +msgctxt "#44002" msgid "Generate and view [COLOR orange]Global Reports[/COLOR]." msgstr "viewqueries" -msgctxt "#42003" +msgctxt "#44003" msgid "Reset the AKL database. You will loose all data." msgstr "viewqueries" -msgctxt "#42004" +msgctxt "#44004" msgid "Rebuild all the container views in the application." msgstr "viewqueries" -msgctxt "#42005" +msgctxt "#44005" msgid "Browse AKL Favourite ROMs" msgstr "domain" -msgctxt "#42006" +msgctxt "#44006" msgid "Browse the ROMs you played recently" msgstr "domain" -msgctxt "#42007" +msgctxt "#44007" msgid "Browse the ROMs you play most" msgstr "domain" -msgctxt "#42008" +msgctxt "#44008" msgid "Browse ROMs filtered on '{0}'" msgstr "domain" -msgctxt "#42009" +msgctxt "#44009" msgid "Browse the ROMs by specifics" msgstr "domain" -msgctxt "#42010" +msgctxt "#44010" msgid "Browse the ROMs by title" msgstr "domain" -msgctxt "#42011" +msgctxt "#44011" msgid "Browse the ROMs by year" msgstr "domain" -msgctxt "#42012" +msgctxt "#44012" msgid "Browse the ROMs by genre" msgstr "domain" -msgctxt "#42013" +msgctxt "#44013" msgid "Browse the ROMs by developer" msgstr "domain" -msgctxt "#42014" +msgctxt "#44014" msgid "Browse the ROMs by number of players" msgstr "domain" -msgctxt "#42015" +msgctxt "#44015" msgid "Browse the ROMs by ESRB rating" msgstr "domain" -msgctxt "#42016" +msgctxt "#44016" msgid "Browse the ROMs by PEGI rating" msgstr "domain" -msgctxt "#42017" +msgctxt "#44017" msgid "Browse the ROMs by rating" msgstr "domain" -msgctxt "#42018" +msgctxt "#44018" msgid "Rebuild all the virtual categories and collections in the container" msgstr "viewqueries" -msgctxt "#42019" +msgctxt "#44019" msgid "Scan for addons that can be used by AKL (launchers, scrapers etc.)" msgstr "viewqueries" -msgctxt "#42020" +msgctxt "#44020" msgid "Shows previously scanned addons that can be used by AKL (launchers, scrapers etc.)" msgstr "viewqueries" -msgctxt "#42021" +msgctxt "#44021" msgid "Manage existing/available tags for ROMs" msgstr "viewqueries" -msgctxt "#42022" +msgctxt "#44022" msgid "Execute several [COLOR orange]Utilities[/COLOR]." msgstr "viewqueries" -msgctxt "#42023" +msgctxt "#44023" msgid "Exports all AKL categories and collections into an XML configuration file.\nYou can later reimport this XML file." msgstr "viewqueries" -msgctxt "#42024" +msgctxt "#44024" msgid "Check all collections for missing launchers or scanners, missing artwork, wrong platform names, asset path existence, etc." msgstr "viewqueries" -msgctxt "#42025" +msgctxt "#44025" msgid "Scans existing [COLOR=orange]ROMs artwork images[/COLOR] in ROM Collections and verifies that the images have correct extension and size is greater than 0. You can delete corrupted images to be rescraped later." msgstr "viewqueries" -msgctxt "#42026" +msgctxt "#44026" msgid "Scans all ROM collections and finds [COLOR orange]redundant ROMs artwork[/COLOR]. You may delete these unneeded images." msgstr "viewqueries" -msgctxt "#42027" +msgctxt "#44027" msgid "Display the auto-detected No-Intro/Redump DATs that will be used for the ROM audit. You have to configure the DAT directories in [COLOR orange]AKL addon settings[/COLOR], [COLOR=orange]ROM Audit[/COLOR] tab." msgstr "viewqueries" -msgctxt "#42028" +msgctxt "#44028" msgid "Shows a report of all ROM collections with number of ROMs." msgstr "viewqueries" -msgctxt "#42029" +msgctxt "#44029" msgid "Shows a report of all audited ROM collections, with Have, Miss and Unknown statistics." msgstr "viewqueries" -msgctxt "#42030" +msgctxt "#44030" msgid "Shows a report of all audited ROM Launchers, with Have, Miss and Unknown statistics. Only No-Intro platforms (cartridge-based) are reported." msgstr "viewqueries" -msgctxt "#42031" +msgctxt "#44031" msgid "Shows a report of all audited ROM Launchers, with Have, Miss and Unknown statistics. Only Redump platforms (optical-based) are reported." -msgstr "viewqueries" \ No newline at end of file +msgstr "viewqueries" + +msgctxt "#44032" +msgid "Manage your game [COLOR orange]libraries[/COLOR] and sources." +msgstr "viewqueries" + +msgctxt "#44033" +msgid "Manage your game [COLOR orange]launchers[/COLOR]." +msgstr "viewqueries" diff --git a/resources/lib/apiqueries.py b/resources/lib/apiqueries.py index 8ee6407b..159c7d91 100644 --- a/resources/lib/apiqueries.py +++ b/resources/lib/apiqueries.py @@ -24,7 +24,7 @@ # AKL modules from resources.lib import globals -from resources.lib.repositories import UnitOfWork, ROMsRepository, ROMCollectionRepository +from resources.lib.repositories import UnitOfWork, ROMsRepository, ROMCollectionRepository, SourcesRepository, LaunchersRepository logger = logging.getLogger(__name__) @@ -32,96 +32,98 @@ def qry_get_rom(rom_id: str) -> str: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - rom_repository = ROMsRepository(uow) + rom_repository = ROMsRepository(uow) rom = rom_repository.find_rom(rom_id) - if rom is None: return None + if rom is None: + return None rom_dto = rom.create_dto() return json.dumps(rom_dto.get_data_dic()) + def qry_get_rom_collection(collection_id: str) -> str: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - collection_repository = ROMCollectionRepository(uow) + collection_repository = ROMCollectionRepository(uow) rom_collection = collection_repository.find_romcollection(collection_id) - if rom_collection is None: return None + if rom_collection is None: + return None data = rom_collection.get_data_dic() return json.dumps(data) + -def qry_get_roms(collection_id: str) -> str: +def qry_get_roms(source_id: str) -> str: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - collection_repository = ROMCollectionRepository(uow) - rom_repository = ROMsRepository(uow) - - collection = collection_repository.find_romcollection(collection_id) - roms = rom_repository.find_roms_by_romcollection(collection) + source_repository = SourcesRepository(uow) + rom_repository = ROMsRepository(uow) + + source = source_repository.find(source_id) + roms = rom_repository.find_roms_by_source(source) + + if roms is None: + return None - if roms is None: return None data = [] for rom in roms: rom_dto = rom.create_dto() data.append(rom_dto.get_data_dic()) - return json.dumps(data) - -def qry_get_launchers(collection_id: str) -> str: - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - collection_repository = ROMCollectionRepository(uow) - rom_collection = collection_repository.find_romcollection(collection_id) - - if rom_collection is None: return None - - launchers_data = {} - launchers = rom_collection.get_launchers() - for launcher in launchers: - launchers_data[launcher.get_id()] = launcher.get_settings() - - return json.dumps(launchers_data) + return json.dumps(data) + -def qry_get_rom_launcher_settings(rom_id:str, launcher_id: str) -> str: +def qry_get_launcher_settings(launcher_id: str) -> str: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - rom_repository = ROMsRepository(uow) - romcollection_repository = ROMCollectionRepository(uow) - - rom = rom_repository.find_rom(rom_id) - launcher = rom.get_launcher(launcher_id) + repository = LaunchersRepository(uow) + launcher = repository.find(launcher_id) if launcher is not None: return launcher.get_settings_str() - - romcollections = romcollection_repository.find_romcollections_by_rom(rom.get_id()) - for romcollection in romcollections: - launcher = romcollection.get_launcher(launcher_id) - if launcher is not None: - return launcher.get_settings_str() - + return None -def qry_get_collection_launcher_settings(collection_id:str, launcher_id: str) -> str: + +def qry_get_collection_launcher_settings(collection_id: str, launcher_id: str) -> str: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - collection_repository = ROMCollectionRepository(uow) - rom_collection = collection_repository.find_romcollection(collection_id) + collection_repository = ROMCollectionRepository(uow) + rom_collection = collection_repository.find_romcollection(collection_id) - if rom_collection is None: return None + if rom_collection is None: + return None launcher = rom_collection.get_launcher(launcher_id) return launcher.get_settings_str() - -def qry_get_collection_scanner_settings(collection_id:str, scanner_id: str) -> str: + + +def qry_get_source_scanner_settings(source_id: str) -> str: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - collection_repository = ROMCollectionRepository(uow) - rom_collection = collection_repository.find_romcollection(collection_id) + source_repository = SourcesRepository(uow) + source = source_repository.find(source_id) - if rom_collection is None: return None + if source is None: + return None - scanner = rom_collection.get_scanner(scanner_id) - return scanner.get_settings_str() - \ No newline at end of file + return source.get_settings_str() + + +def qry_get_source_launchers(source_id: str) -> str: + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + source_repository = SourcesRepository(uow) + source = source_repository.find(source_id) + + if source is None: + return None + + launchers_data = {} + launchers = source.get_launchers() + for launcher in launchers: + launchers_data[launcher.get_id()] = launcher.get_settings() + + return json.dumps(launchers_data) diff --git a/resources/lib/commands/__init__.py b/resources/lib/commands/__init__.py index cdf55f44..dcec6061 100644 --- a/resources/lib/commands/__init__.py +++ b/resources/lib/commands/__init__.py @@ -23,10 +23,10 @@ import resources.lib.commands.romcollection_roms_commands import resources.lib.commands.rom_commands import resources.lib.commands.rom_launcher_commands -import resources.lib.commands.rom_scanner_commands +import resources.lib.commands.source_commands import resources.lib.commands.rom_scraper_commands import resources.lib.commands.stats_commands import resources.lib.commands.misc_commands import resources.lib.commands.chk_commands import resources.lib.commands.report_commands -import resources.lib.commands.search_commands \ No newline at end of file +import resources.lib.commands.search_commands diff --git a/resources/lib/commands/addon_commands.py b/resources/lib/commands/addon_commands.py index 69230fae..91703836 100644 --- a/resources/lib/commands/addon_commands.py +++ b/resources/lib/commands/addon_commands.py @@ -84,7 +84,7 @@ def cmd_show_addons(args): @AppMediator.register('ADDON_DETAILS') def cmd_addon_details(args): - addon_id:str = args['addon_id'] if 'addon_id' in args else None + addon_id: str = args['addon_id'] if 'addon_id' in args else None options = collections.OrderedDict() options["UPDATE"] = kodi.translate(42009) diff --git a/resources/lib/commands/api_commands.py b/resources/lib/commands/api_commands.py index 56acce08..a27e8f8a 100644 --- a/resources/lib/commands/api_commands.py +++ b/resources/lib/commands/api_commands.py @@ -27,184 +27,212 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals -from resources.lib.repositories import UnitOfWork, AelAddonRepository, ROMCollectionRepository, ROMsRepository -from resources.lib.domain import ROM +from resources.lib.repositories import UnitOfWork, ROMCollectionRepository, ROMsRepository, SourcesRepository +from resources.lib.repositories import AelAddonRepository, LaunchersRepository +from resources.lib.domain import ROM, ROMLauncherAddon logger = logging.getLogger(__name__) + # ------------------------------------------------------------------------------------------------- # ROMCollection API commands -# ------------------------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------------------------- def cmd_set_launcher_args(args) -> bool: - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - rom_id:str = args['rom_id'] if 'rom_id' in args else None - launcher_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - addon_id:str = args['addon_id'] if 'addon_id' in args else None - launcher_settings = args['settings'] if 'settings' in args else None - - metadata_updated = False + launcher_id: str = args['launcher_id'] if 'launcher_id' in args else None + addon_id: str = args['addon_id'] if 'addon_id' in args else None + launcher_settings = args['settings'] if 'settings' in args else None + + entity_type = args['entity_type'] if 'entity_type' in args else None + entity_id: str = args['entity_id'] if 'entity_id' in args else None + redirect_to_action = None + args = None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: addon_repository = AelAddonRepository(uow) - romcollection_repository = ROMCollectionRepository(uow) - rom_repository = ROMsRepository(uow) + launchers_repository = LaunchersRepository(uow) addon = addon_repository.find_by_addon_id(addon_id, constants.AddonType.LAUNCHER) + launcher = launchers_repository.find(launcher_id) + + if launcher is None: + launcher = ROMLauncherAddon(None, addon) + launcher.set_settings(launcher_settings) + launchers_repository.insert_launcher(launcher) + else: + launcher.set_settings(launcher_settings) + launchers_repository.update_launcher(launcher) - if romcollection_id is not None: - romcollection = romcollection_repository.find_romcollection(romcollection_id) - if launcher_id is None: - romcollection.add_launcher(addon, launcher_settings, True) - else: - launcher = romcollection.get_launcher(launcher_id) - launcher.set_settings(launcher_settings) + if entity_type: + if entity_type == constants.OBJ_ROM: + entity_repo = ROMsRepository(uow) + rom = entity_repo.find_rom(entity_id) + rom.add_launcher(launcher) + entity_repo.update_rom(rom) + redirect_to_action = "EDIT_ROM_LAUNCHERS" + args = {'rom_id': entity_id} - if 'romcollection' in launcher_settings \ - and kodi.dialog_yesno(kodi.translate(41050)): - romcollection.import_data_dic(launcher_settings['romcollection']) - metadata_updated = True + if entity_type == constants.OBJ_ROMCOLLECTION: + entity_repo = ROMCollectionRepository(uow) + collection = entity_repo.find_romcollection(entity_id) + collection.add_launcher(launcher) + entity_repo.update_romcollection(collection) + redirect_to_action = "EDIT_ROMCOLLECTION_LAUNCHERS" + args = {'romcollection_id': entity_id} - romcollection_repository.update_romcollection(romcollection) - uow.commit() - - if metadata_updated: AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) - AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) - else: - rom = rom_repository.find_rom(rom_id) - if launcher_id is None: - rom.add_launcher(addon, launcher_settings, True) - else: - launcher = rom.get_launcher(launcher_id) - launcher.set_settings(launcher_settings) + if entity_type == constants.OBJ_SOURCE: + entity_repo = SourcesRepository(uow) + source = entity_repo.find(entity_id) + source.add_launcher(launcher) + entity_repo.update_source(source) + redirect_to_action = "EDIT_SOURCE_LAUNCHERS" + args = {'source_id': entity_id} - rom_repository.update_rom(rom) - uow.commit() + uow.commit() - kodi.notify(kodi.translate(41005).format(addon.get_name())) + kodi.refresh_container() + kodi.notify(kodi.translate(41005).format(launcher.get_name())) + + if entity_type: + AppMediator.async_cmd(redirect_to_action, args) + return True # ------------------------------------------------------------------------------------------------- -# ROMCollection scanner API commands +# Source scanner API commands # ------------------------------------------------------------------------------------------------- def cmd_set_scanner_settings(args) -> bool: + # TODO: backwards compatiblity romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None - scanner_id: str = args['akl_addon_id'] if 'akl_addon_id' in args else None - addon_id: str = args['addon_id'] if 'addon_id' in args else None + source_id: str = args['source_id'] if 'source_id' in args else None + source_id = romcollection_id if not source_id else source_id + settings: dict = args['settings'] if 'settings' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - addon_repository = AelAddonRepository(uow) - romcollection_repository = ROMCollectionRepository(uow) + src_repository = SourcesRepository(uow) + source = src_repository.find(source_id) - addon = addon_repository.find_by_addon_id(addon_id, constants.AddonType.SCANNER) - romcollection = romcollection_repository.find_romcollection(romcollection_id) - - if scanner_id is None: - romcollection.add_scanner(addon, settings) - else: - scanner = romcollection.get_scanner(scanner_id) - scanner.set_settings(settings) + source.set_settings(settings) - romcollection_repository.update_romcollection(romcollection) + src_repository.update_source(source) uow.commit() - kodi.notify(kodi.translate(41006).format(addon.get_name())) + kodi.notify(kodi.translate(41006).format(source.addon.get_name())) + AppMediator.async_cmd('RENDER_SOURCES_VIEW') if kodi.dialog_yesno(kodi.translate(41051)): - AppMediator.async_cmd('SCAN_ROMS', {'romcollection_id': romcollection_id}) + AppMediator.async_cmd('SCAN_ROMS', {'source_id': source_id}) else: - AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) + AppMediator.async_cmd('SOURCE_MANAGE_ROMS', {'source_id': source_id}) return True + def cmd_store_scanned_roms(args) -> bool: - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - scanner_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - new_roms:list = args['roms'] if 'roms' in args else None + # TODO: backwards compatiblity + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + source_id: str = args['source_id'] if 'source_id' in args else None + source_id = romcollection_id if not source_id else source_id + + new_roms: list = args['roms'] if 'roms' in args else None if new_roms is None: + AppMediator.async_cmd('SOURCE_MANAGE_ROMS', {'source_id': source_id}) return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - romcollection_repository = ROMCollectionRepository(uow) - rom_repository = ROMsRepository(uow) - - romcollection = romcollection_repository.find_romcollection(romcollection_id) - + rom_repository = ROMsRepository(uow) + src_repository = SourcesRepository(uow) + source = src_repository.find(source_id) + for rom_data in new_roms: api_rom_obj = ROMObj(rom_data) rom_obj = ROM() rom_obj.update_with(api_rom_obj, overwrite_existing_metadata=True, update_scanned_data=True) - rom_obj.set_platform(romcollection.get_platform()) - rom_obj.scanned_with(scanner_id) - rom_obj.apply_romcollection_asset_paths(romcollection) + rom_obj.set_platform(source.get_platform()) + rom_obj.scanned_by(source.get_id()) + rom_obj.apply_source_asset_paths(source) rom_repository.insert_rom(rom_obj) - romcollection_repository.add_rom_to_romcollection(romcollection.get_id(), rom_obj.get_id()) uow.commit() - kodi.notify(kodi.translate(41007).format(romcollection.get_name())) - - AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + kodi.notify(kodi.translate(41007).format(source.get_name())) + + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_TITLE_ID}) - AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) + AppMediator.async_cmd('SOURCE_MANAGE_ROMS', {'source_id': source_id}) return True + def cmd_remove_roms(args) -> bool: - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - scanner_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - rom_ids:list = args['rom_ids'] if 'rom_ids' in args else None + # TODO: backwards compatiblity + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + source_id: str = args['source_id'] if 'source_id' in args else None + source_id = romcollection_id if not source_id else source_id + rom_ids: list = args['rom_ids'] if 'rom_ids' in args else None if rom_ids is None: return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - romcollection_repository = ROMCollectionRepository(uow) - rom_repository = ROMsRepository(uow) - romcollection = romcollection_repository.find_romcollection(romcollection_id) + sources_repository = SourcesRepository(uow) + romcollections_repository = ROMCollectionRepository(uow) + rom_repository = ROMsRepository(uow) + + romcollections = romcollections_repository.find_romcollections_by_source(source) + source = sources_repository.find(source_id) for rom_id in rom_ids: rom_repository.delete_rom(rom_id) uow.commit() - kodi.notify(kodi.translate(41010).format(romcollection.get_name())) + kodi.notify(kodi.translate(41010).format(source.get_name())) - AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') - AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) + AppMediator.async_cmd('EDIT_SOURCE', {'source_id': source_id}) return True + def cmd_store_scraped_roms(args) -> bool: - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - scraper_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - scraped_roms:list = args['roms'] if 'roms' in args else None - settings_dic:dict = args['applied_settings'] if 'applied_settings' in args else {} - applied_settings = ScraperSettings.from_settings_dict(settings_dic) + entity_type = args['entity_type'] if 'entity_type' in args else None + entity_id: str = args['entity_id'] if 'entity_id' in args else None + scraped_roms: list = args['roms'] if 'roms' in args else None + settings_dic: dict = args['applied_settings'] if 'applied_settings' in args else {} + applied_settings = ScraperSettings.from_settings_dict(settings_dic) if scraped_roms is None: return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: + source_repository = SourcesRepository(uow) romcollection_repository = ROMCollectionRepository(uow) - rom_repository = ROMsRepository(uow) + rom_repository = ROMsRepository(uow) - romcollection = romcollection_repository.find_romcollection(romcollection_id) - existing_roms = rom_repository.find_roms_by_romcollection(romcollection) - existing_roms_by_id = { rom.get_id(): rom for rom in existing_roms } + entity_name = 'UNKNOWN' + if entity_type == constants.OBJ_SOURCE: + source = source_repository.find(entity_id) + existing_roms = rom_repository.find_roms_by_source(source) + entity_name = source.get_name() + + if entity_type == constants.OBJ_ROMCOLLECTION: + romcollection = romcollection_repository.find_romcollection(entity_id) + existing_roms = rom_repository.find_roms_by_romcollection(romcollection) + entity_name = romcollection_repository.get_name() + + existing_roms_by_id = {rom.get_id(): rom for rom in existing_roms} metadata_is_updated = applied_settings.scrape_metadata_policy != constants.SCRAPE_ACTION_NONE - assets_are_updated = applied_settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE + assets_are_updated = applied_settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE - metadata_to_update = applied_settings.metadata_IDs_to_scrape if metadata_is_updated else [] - assets_to_update = applied_settings.asset_IDs_to_scrape if assets_are_updated else [] + metadata_to_update = applied_settings.metadata_IDs_to_scrape if metadata_is_updated else [] + assets_to_update = applied_settings.asset_IDs_to_scrape if assets_are_updated else [] logger.debug('========================== Applied scraper settings ==========================') logger.debug('Metadata IDs: {}'.format(', '.join(applied_settings.metadata_IDs_to_scrape))) @@ -217,38 +245,46 @@ def cmd_store_scraped_roms(args) -> bool: api_rom_obj = ROMObj(rom_data) if api_rom_obj.get_id() not in existing_roms_by_id: - logger.warning('Scraped ROM {} with ID {} could not be found in collection#{} {}. Will be skipped.'.format( - api_rom_obj.get_name(), + logger.warning('Scraped ROM {} with ID {} could not be found in {}#{} {}. Will be skipped.'.format( + api_rom_obj.get_name(), api_rom_obj.get_id(), - romcollection.get_id(), - romcollection.get_name())) + kodi.translate(entity_type), + entity_id, + entity_name)) continue rom_obj = existing_roms_by_id[api_rom_obj.get_id()] rom_obj.update_with( - api_rom_obj, - metadata_to_update, - assets_to_update, + api_rom_obj, + metadata_to_update, + assets_to_update, overwrite_existing_metadata=applied_settings.overwrite_existing_meta, overwrite_existing_assets=applied_settings.overwrite_existing_assets) - #rom_obj.scraped_with(scraper_id) + # rom_obj.scraped_with(scraper_id) rom_repository.update_rom(rom_obj) uow.commit() kodi.notify(kodi.translate(41008).format(romcollection.get_name())) - AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) - if metadata_is_updated: AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') - AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) + if metadata_is_updated: + AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') + + if entity_type == constants.OBJ_ROMCOLLECTION: + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': entity_id}) + AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': entity_id}) + + if entity_type == constants.OBJ_SOURCE: + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': entity_id}) + AppMediator.async_cmd('SOURCE_MANAGE_ROMS', {'source_id': entity_id}) return True + def cmd_store_scraped_single_rom(args) -> bool: - rom_id:str = args['rom_id'] if 'rom_id' in args else None - scraper_id:str = args['akl_addon_id'] if 'akl_addon_id' in args else None - scraped_rom_data:dict= args['rom'] if 'rom' in args else None - settings_dic:dict = args['applied_settings'] if 'applied_settings' in args else {} - applied_settings = ScraperSettings.from_settings_dict(settings_dic) + rom_id: str = args['rom_id'] if 'rom_id' in args else None + scraped_rom_data: dict = args['rom'] if 'rom' in args else None + settings_dic: dict = args['applied_settings'] if 'applied_settings' in args else {} + applied_settings = ScraperSettings.from_settings_dict(settings_dic) if scraped_rom_data is None: return @@ -258,18 +294,18 @@ def cmd_store_scraped_single_rom(args) -> bool: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: romcollection_repository = ROMCollectionRepository(uow) - rom_repository = ROMsRepository(uow) - + rom_repository = ROMsRepository(uow) + rom_romcollections = romcollection_repository.find_romcollections_by_rom(rom_id) rom_collection_ids = [collection.get_id() for collection in rom_romcollections] rom = rom_repository.find_rom(rom_id) metadata_is_updated = applied_settings.scrape_metadata_policy != constants.SCRAPE_ACTION_NONE - assets_are_updated = applied_settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE + assets_are_updated = applied_settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE metadata_to_update = applied_settings.metadata_IDs_to_scrape if metadata_is_updated else [] - assets_to_update = applied_settings.asset_IDs_to_scrape if assets_are_updated else [] + assets_to_update = applied_settings.asset_IDs_to_scrape if assets_are_updated else [] logger.debug('========================== Applied scraper settings ==========================') logger.debug('Metadata IDs: {}'.format(', '.join(applied_settings.metadata_IDs_to_scrape))) @@ -279,21 +315,22 @@ def cmd_store_scraped_single_rom(args) -> bool: logger.debug('Assets updated: {}'.format('Yes' if assets_are_updated else 'No')) rom.update_with(scraped_rom, - metadata_to_update, - assets_to_update, - overwrite_existing_metadata=applied_settings.overwrite_existing_meta, - overwrite_existing_assets=applied_settings.overwrite_existing_assets) - #rom_obj.scraped_with(scraper_id) + metadata_to_update, + assets_to_update, + overwrite_existing_metadata=applied_settings.overwrite_existing_meta, + overwrite_existing_assets=applied_settings.overwrite_existing_assets) + # rom_obj.scraped_with(scraper_id) rom_repository.update_rom(rom) uow.commit() kodi.notify(kodi.translate(41009).format(rom.get_name())) + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': rom.get_scanned_by()}) for collection_id in rom_collection_ids: AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': collection_id}) - scraped_meta = applied_settings.scrape_metadata_policy != constants.SCRAPE_ACTION_NONE + scraped_meta = applied_settings.scrape_metadata_policy != constants.SCRAPE_ACTION_NONE scraped_assets = applied_settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE if metadata_is_updated: @@ -309,4 +346,4 @@ def cmd_store_scraped_single_rom(args) -> bool: else: AppMediator.async_cmd('EDIT_ROM', {'rom_id': rom_id}) - return True \ No newline at end of file + return True diff --git a/resources/lib/commands/category_commands.py b/resources/lib/commands/category_commands.py index a09a1d70..b4b35970 100644 --- a/resources/lib/commands/category_commands.py +++ b/resources/lib/commands/category_commands.py @@ -13,7 +13,7 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# --- Python standard library --- +# --- Python standard source --- from __future__ import unicode_literals from __future__ import division @@ -30,6 +30,7 @@ logger = logging.getLogger(__name__) + @AppMediator.register('ADD_CATEGORY') def cmd_add_category(args): logger.debug('cmd_add_category() BEGIN') @@ -38,13 +39,16 @@ def cmd_add_category(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = CategoryRepository(uow) - parent_category = repository.find_category(parent_id) if parent_id is not None else None + repository = CategoryRepository(uow) + parent_category = repository.find_category(parent_id) if parent_id is not None else None grand_parent_category = repository.find_category(grand_parent_id) if grand_parent_id is not None else None if grand_parent_category is not None: options_dialog = kodi.ListDialog() - selected_option = options_dialog.select(kodi.translate(41084),[parent_category.get_name(), grand_parent_category.get_name()]) + selected_option = options_dialog.select(kodi.translate(41084), [ + parent_category.get_name(), + grand_parent_category.get_name() + ]) if selected_option > 0: parent_category = grand_parent_category @@ -64,10 +68,11 @@ def cmd_add_category(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) + @AppMediator.register('EDIT_CATEGORY') def cmd_edit_category(args): logger.debug('EDIT_CATEGORY: BEGIN') - category_id:str = args['category_id'] if 'category_id' in args else None + category_id: str = args['category_id'] if 'category_id' in args else None if category_id is None: logger.warning('cmd_add_category(): No category id supplied.') @@ -84,12 +89,12 @@ def cmd_edit_category(args): options['CATEGORY_EDIT_METADATA'] = kodi.translate(40853) options['CATEGORY_EDIT_ASSETS'] = kodi.translate(40854) options['CATEGORY_EDIT_DEFAULT_ASSETS'] = kodi.translate(40859) - options['CATEGORY_STATUS'] = f'{kodi.translate(40859)} {category.get_finished_str_code()}' + options['CATEGORY_STATUS'] = f'{kodi.translate(40860)} {kodi.translate(category.get_finished_str_code())}' options['EXPORT_CATEGORY_XML'] = kodi.translate(40861) options['DELETE_CATEGORY'] = kodi.translate(40862) - s = f'{kodi.translate(40950)} "{category.get_name}"' - selected_option = kodi.OrdDictionaryDialog().select(s, options) + s = f'{kodi.translate(40950)} "{category.get_name()}"' + selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu @@ -99,10 +104,11 @@ def cmd_edit_category(args): # >> Execute subcommand. May be atomic, maybe a submenu. logger.debug(f'EDIT_CATEGORY: Selected {selected_option}') AppMediator.sync_cmd(selected_option, args) - -# --- Submenu command --- + + +# --- Submenu command --- @AppMediator.register('CATEGORY_EDIT_METADATA') -def cmd_edit_metadata_category(args): +def cmd_edit_metadata_category(args): logger.debug('CATEGORY_EDIT_METADATA: cmd_edit_metadata_category() BEGIN') category_id = args['category_id'] if 'category_id' in args else None selected_option = None @@ -112,9 +118,9 @@ def cmd_edit_metadata_category(args): repository = CategoryRepository(uow) category = repository.find_category(category_id) - NFO_FileName = category.get_NFO_name() + NFO_FileName = category.get_NFO_name() NFO_found_str = kodi.translate(42019) if NFO_FileName.exists() else kodi.translate(42020) - plot_str = text.limit_string(category.get_plot(), constants.PLOT_STR_MAXSIZE) + plot_str = text.limit_string(category.get_plot(), constants.PLOT_STR_MAXSIZE) options = collections.OrderedDict() options['CATEGORY_EDIT_METADATA_TITLE'] = kodi.translate(40863).format(category.get_name()) @@ -140,6 +146,7 @@ def cmd_edit_metadata_category(args): logger.debug('CATEGORY_EDIT_METADATA: cmd_edit_metadata_category() Selected {0}'.format(selected_option)) AppMediator.sync_cmd(selected_option, args) + @AppMediator.register('CATEGORY_EDIT_ASSETS') def cmd_category_edit_assets(args): category_id = args['category_id'] if 'category_id' in args else None @@ -156,7 +163,7 @@ def cmd_category_edit_assets(args): return asset = g_assetFactory.get_asset_info(selected_asset_to_edit) - #if selected_asset_to_edit == editors.SCRAPE_CMD: + # if selected_asset_to_edit == editors.SCRAPE_CMD: # AppMediator.async_cmd('EDIT_CATEGORY_MENU', args) # globals.run_command(scrape_cmd, rom=obj_instance) # edit_object_assets(obj_instance, selected_option) @@ -169,9 +176,10 @@ def cmd_category_edit_assets(args): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) - AppMediator.sync_cmd('CATEGORY_EDIT_ASSETS', {'category_id': category_id, 'selected_asset': asset.id}) + AppMediator.sync_cmd('CATEGORY_EDIT_ASSETS', {'category_id': category_id, 'selected_asset': asset.id}) + @AppMediator.register('CATEGORY_EDIT_DEFAULT_ASSETS') def cmd_category_edit_default_assets(args): @@ -192,10 +200,11 @@ def cmd_category_edit_default_assets(args): repository.update_category(category) uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.sync_cmd('CATEGORY_EDIT_DEFAULT_ASSETS', {'category_id': category_id, 'selected_asset': selected_asset_to_edit.id}) + @AppMediator.register('CATEGORY_STATUS') def cmd_category_status(args): category_id = args['category_id'] if 'category_id' in args else None @@ -209,9 +218,10 @@ def cmd_category_status(args): uow.commit() AppMediator.async_cmd('RENDER_CATEGORY_VIEW', args) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.sync_cmd('EDIT_CATEGORY', args) - + + # # Remove category. Also removes launchers in that category # @@ -224,7 +234,6 @@ def cmd_category_delete(args): category = repository.find_category(category_id) category_name = category.get_name() - del_q = kodi.translate(41066).format(category_name) if category.has_items(): question = kodi.translate(41067).format(category_name, category.num_categories(), category.num_romcollections()) \ + kodi.translate(41066).format(category_name) @@ -232,14 +241,15 @@ def cmd_category_delete(args): question = kodi.translate(41068).format(category_name) + kodi.translate(41066).format(category_name) ret = kodi.dialog_yesno(question) - if not ret: return + if not ret: + return logger.info(f'Deleting category "{category_name}" ID {category.get_id()}') repository.delete_category(category_id) uow.commit() kodi.notify(kodi.translate(41037).format(category_name)) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.async_cmd('CLEANUP_VIEWS') AppMediator.sync_cmd('EDIT_CATEGORY', args) @@ -309,9 +319,10 @@ def cmd_category_metadata_developer(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) + @AppMediator.register('CATEGORY_EDIT_METADATA_RATING') def cmd_category_metadata_rating(args): - category_id = args['category_id'] if 'category_id' in args else None + category_id = args['category_id'] if 'category_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = CategoryRepository(uow) @@ -324,9 +335,10 @@ def cmd_category_metadata_rating(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) + @AppMediator.register('CATEGORY_EDIT_METADATA_PLOT') def cmd_category_metadata_plot(args): - category_id = args['category_id'] if 'category_id' in args else None + category_id = args['category_id'] if 'category_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = CategoryRepository(uow) @@ -338,10 +350,11 @@ def cmd_category_metadata_plot(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': category.get_parent_id()}) AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) - + + @AppMediator.register('CATEGORY_IMPORT_NFO_FILE_DEFAULT') def cmd_category_import_nfo_file(args): - category_id = args['category_id'] if 'category_id' in args else None + category_id = args['category_id'] if 'category_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = CategoryRepository(uow) @@ -357,8 +370,9 @@ def cmd_category_import_nfo_file(args): AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) + @AppMediator.register('CATEGORY_IMPORT_NFO_FILE_BROWSE') -def cmd_category_browse_import_nfo_file(args): +def cmd_category_browse_import_nfo_file(args): category_id = args['category_id'] if 'category_id' in args else None NFO_file = kodi.browse(text=kodi.translate(41143), mask='.nfo') @@ -383,6 +397,7 @@ def cmd_category_browse_import_nfo_file(args): AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) + @AppMediator.register('CATEGORY_SAVE_NFO_FILE') def cmd_category_save_nfo_file(args): category_id = args['category_id'] if 'category_id' in args else None @@ -405,6 +420,7 @@ def cmd_category_save_nfo_file(args): AppMediator.sync_cmd('CATEGORY_EDIT_METADATA', args) + @AppMediator.register('CATEGORY_EXPORT_CATEGORY_XML') # --- Export Category XML configuration --- def cmd_category_export_xml(args): diff --git a/resources/lib/commands/chk_commands.py b/resources/lib/commands/chk_commands.py index 621ce836..9ea1639f 100644 --- a/resources/lib/commands/chk_commands.py +++ b/resources/lib/commands/chk_commands.py @@ -13,7 +13,7 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# --- Python standard library --- +# --- Python standard source --- from __future__ import unicode_literals from __future__ import division diff --git a/resources/lib/commands/misc_commands.py b/resources/lib/commands/misc_commands.py index 9ea2e8d4..501a3a2f 100644 --- a/resources/lib/commands/misc_commands.py +++ b/resources/lib/commands/misc_commands.py @@ -21,7 +21,6 @@ import typing import collections -from datetime import datetime from xml.etree import cElementTree as ET from xml.dom import minidom from distutils.version import LooseVersion @@ -31,7 +30,7 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals -from resources.lib.repositories import UnitOfWork, AelAddonRepository, CategoryRepository, ROMCollectionRepository, XmlConfigurationRepository +from resources.lib.repositories import UnitOfWork, AelAddonRepository, CategoryRepository, ROMCollectionRepository, XmlConfigurationRepository, SourcesRepository from resources.lib.domain import Category, ROMCollection, AelAddon logger = logging.getLogger(__name__) @@ -41,8 +40,8 @@ def cmd_execute_import_launchers(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - addon_repository = AelAddonRepository(uow) - available_launchers = [*addon_repository.find_all_launchers()] + addon_repository = AelAddonRepository(uow) + available_launchers = [*addon_repository.find_all_launcher_addons()] categories_repository = CategoryRepository(uow) existing_categories = [*categories_repository.find_all_categories()] @@ -233,7 +232,7 @@ def cmd_execute_migrations(args): migrations_in_database = uow.get_migrations_history() options = collections.OrderedDict() - migrations_files_available = uow.get_migration_files(LooseVersion("0.0.0")) + migrations_files_available = uow.get_migration_files(LooseVersion("0.0.0")) for migration_file in migrations_files_available: file_name = migration_file.getBase() @@ -247,6 +246,7 @@ def cmd_execute_migrations(args): if selected_file is None: return + logger.debug(f"RUN_DB_MIGRATIONS: Selected {selected_file}") migration_file = io.FileName(selected_file) version_to_store = LooseVersion(globals.addon_version) file_version = uow.get_version_from_migration_file(migration_file) @@ -256,35 +256,38 @@ def cmd_execute_migrations(args): version_to_store = db_version dialog = kodi.ListDialog() - selected_index = dialog.select(kodi.translate(41089).format(migration_file.getBaseNoExt()),[ + selected_index = dialog.select(kodi.translate(41089).format(migration_file.getBaseNoExt()), [ kodi.translate(41090), kodi.translate(41091) ]) - if not selected_index: + + if selected_index is None or selected_index < 0: return if selected_index == 0: if not kodi.dialog_yesno(kodi.translate(41055).format(migration_file.getBaseNoExt())): return - uow.migrate_database([migration_file], version_to_store, selected_index==1) + uow.migrate_database([migration_file], version_to_store, selected_index == 1) kodi.notify(kodi.translate(41016)) + @AppMediator.register('CHECK_DUPLICATE_ASSET_DIRS') def cmd_check_duplicate_asset_dirs(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + source_id: str = args['source_id'] if 'source_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMCollectionRepository(uow) - romcollection = repository.find_romcollection(romcollection_id) + repository = SourcesRepository(uow) + source = repository.find(source_id) # >> Check for duplicate paths and warn user. - duplicated_name_list = romcollection.get_duplicated_asset_dirs() + duplicated_name_list = source.get_duplicated_asset_dirs() if duplicated_name_list: duplicated_asset_srt = ', '.join(duplicated_name_list) kodi.dialog_OK(kodi.translate(41147).format(duplicated_asset_srt)) + def _apply_addon_launcher_for_legacy_launcher(collection: ROMCollection, available_addons: typing.Dict[str, AelAddon]): launcher_type = collection.get_custom_attribute('type') logger.debug(f'Migrating launcher of type "{launcher_type}" for romcollection {collection.get_name()}') diff --git a/resources/lib/commands/rom_commands.py b/resources/lib/commands/rom_commands.py index 9cde3d9e..052cd089 100644 --- a/resources/lib/commands/rom_commands.py +++ b/resources/lib/commands/rom_commands.py @@ -25,8 +25,10 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals, editors -from resources.lib.repositories import CategoryRepository, ROMsRepository, ROMCollectionRepository, AelAddonRepository, UnitOfWork -from resources.lib.domain import g_assetFactory, ROM +from resources.lib.repositories import CategoryRepository, ROMsRepository, ROMCollectionRepository, UnitOfWork +from resources.lib.repositories import SourcesRepository + +from resources.lib.domain import g_assetFactory logger = logging.getLogger(__name__) @@ -34,6 +36,7 @@ # ROM context menu. # ------------------------------------------------------------------------------------------------- + # --- Main menu command --- @AppMediator.register('EDIT_ROM') def cmd_edit_rom(args): @@ -56,7 +59,7 @@ def cmd_edit_rom(args): options['ROM_EDIT_ASSETS'] = kodi.translate(40854) options['ROM_EDIT_DEFAULT_ASSETS'] = kodi.translate(40859) options['EDIT_ROM_STATUS'] = kodi.translate(42013).format( - kodi.translate(rom.get_finished_str_code())) + kodi.translate(rom.get_finished_str_code())) if rom.has_launchers(): options['EDIT_ROM_LAUNCHERS'] = kodi.translate(42016) else: @@ -68,13 +71,14 @@ def cmd_edit_rom(args): selected_option = kodi.OrdDictionaryDialog().select(s, options) if selected_option is None: # >> Exits context menu - logger.debug('EDIT_ROM: cmd_edit_rom() Selected None. Closing context menu') + logger.debug('EDIT_ROM: Selected None. Closing context menu') return # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug(f'EDIT_ROM: cmd_edit_rom() Selected {selected_option}') + logger.debug(f'EDIT_ROM: Selected {selected_option}') AppMediator.sync_cmd(selected_option, args) - + + # --- Submenu commands --- @AppMediator.register('ROM_EDIT_METADATA') def cmd_rom_metadata(args): @@ -92,7 +96,7 @@ def cmd_rom_metadata(args): NFO_found_str = kodi.translate(42019) if NFO_FileName and NFO_FileName.exists() else kodi.translate(42020) options = collections.OrderedDict() - options['ROM_EDIT_METADATA_TITLE'] = kodi.translate(40863).format(rom.get_name) + options['ROM_EDIT_METADATA_TITLE'] = kodi.translate(40863).format(rom.get_name()) options['ROM_EDIT_METADATA_PLATFORM'] = kodi.translate(40864).format(rom.get_platform()) options['ROM_EDIT_METADATA_RELEASEYEAR'] = kodi.translate(40865).format(rom.get_releaseyear()) options['ROM_EDIT_METADATA_GENRE'] = kodi.translate(40867).format(rom.get_genre()) @@ -103,7 +107,7 @@ def cmd_rom_metadata(args): options['ROM_EDIT_METADATA_PEGI'] = kodi.translate(40873).format(rom.get_pegi_rating()) options['ROM_EDIT_METADATA_RATING'] = kodi.translate(40869).format(rating) options['ROM_EDIT_METADATA_PLOT'] = kodi.translate(40870).format(plot_str) - options['ROM_EDIT_METADATA_TAGS'] = kodi.translate(40866) + options['ROM_EDIT_METADATA_TAGS'] = kodi.translate(40866) options['ROM_EDIT_METADATA_BOXSIZE'] = kodi.translate(40875).format(rom.get_box_sizing()) options['ROM_LOAD_PLOT'] = kodi.translate(40879) options['ROM_IMPORT_NFO_FILE_DEFAULT'] = kodi.translate(40876).format(NFO_found_str) @@ -121,19 +125,23 @@ def cmd_rom_metadata(args): # >> Execute edit metadata atomic subcommand. # >> Then, execute recursively this submenu again. - logger.debug('cmd_rom_metadata(EDIT_METADATA) Selected {0}'.format(selected_option)) + logger.debug(f'cmd_rom_metadata(EDIT_METADATA) Selected {selected_option}') AppMediator.sync_cmd(selected_option, args) - + + @AppMediator.register('ROM_EDIT_ASSETS') def cmd_rom_assets(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None - preselected_option = args['selected_asset'] if 'selected_asset' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None + preselected_option = args['selected_asset'] if 'selected_asset' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) + source_repository = SourcesRepository(uow) + source = source_repository.find(rom.get_scanned_by()) + romcollection_repository = ROMCollectionRepository(uow) rom_romcollections = romcollection_repository.find_romcollections_by_rom(rom_id) rom_collection_ids = [collection.get_id() for collection in rom_romcollections] @@ -147,11 +155,12 @@ def cmd_rom_assets(args): AppMediator.sync_cmd(editors.SCRAPE_CMD, args) return - asset = g_assetFactory.get_asset_info(selected_asset_to_edit) + assets_root_path = source.get_assets_root_path() if source else None + asset = g_assetFactory.get_asset_info(selected_asset_to_edit) # >> Execute edit asset menu subcommand. Then, execute recursively this submenu again. # >> The menu dialog is instantiated again so it reflects the changes just edited. # >> If edit_asset() returns a command other than scrape or None changes were made. - cmd = editors.edit_asset(rom, asset) + cmd = editors.edit_asset(rom, asset, assets_root_path) if cmd is not None: if cmd == 'SCRAPE_ASSET': args['selected_asset'] = asset.id @@ -161,13 +170,14 @@ def cmd_rom_assets(args): repository.update_rom(rom) uow.commit() for romcollection_id in rom_collection_ids: - AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) + + AppMediator.sync_cmd('ROM_EDIT_ASSETS', {'rom_id': rom_id, 'selected_asset': asset.id}) + - AppMediator.sync_cmd('ROM_EDIT_ASSETS', {'rom_id': rom_id, 'selected_asset': asset.id}) - @AppMediator.register('ROM_EDIT_DEFAULT_ASSETS') def cmd_rom_edit_default_assets(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None preselected_option = args['selected_asset'] if 'selected_asset' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -563,6 +573,7 @@ def cmd_rom_metadata_clear_tags(args): AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.sync_cmd('ROM_EDIT_METADATA_TAGS', args) + @AppMediator.register('ROM_EDIT_METADATA_BOXSIZE') def cmd_rom_metadata_boxsize(args): rom_id = args['rom_id'] if 'rom_id' in args else None @@ -578,6 +589,7 @@ def cmd_rom_metadata_boxsize(args): AppMediator.async_cmd('RENDER_ROM_VIEWS', {'rom_id': rom.get_id()}) AppMediator.sync_cmd('ROM_EDIT_METADATA', args) + @AppMediator.register('ROM_LOAD_PLOT') def cmd_rom_load_plot(args): rom_id = args['rom_id'] if 'rom_id' in args else None @@ -672,6 +684,7 @@ def cmd_rom_save_nfo_file(args): AppMediator.sync_cmd('ROM_EDIT_METADATA', args) + @AppMediator.register('MANAGE_ROM_TAGS') def cmd_manage_rom_tags(args): @@ -715,66 +728,55 @@ def cmd_manage_rom_tags(args): if did_tag_change: uow.commit() -# ------------------------------------------------------------------------------------------------- -# ROM ADD -# ------------------------------------------------------------------------------------------------- -@AppMediator.register('ADD_STANDALONE_ROM') -def cmd_add_rom(args): - parent_id = args['category_id'] if 'category_id' in args else None - grand_parent_id = args['parent_category_id'] if 'parent_category_id' in args else None + +@AppMediator.register('LINK_ROM') +def cmd_link_rom(args): + rom_id = args['rom_id'] if 'rom_id' in args else None + + destination_dialog = kodi.ListDialog() + selected_type = destination_dialog.select(kodi.translate(41186), [ + kodi.translate(42513), # COLLECTIONS [0] + kodi.translate(42512) # CATEGORIES [1] + ]) + + if selected_type is None: + logger.debug("LINK_ROM: No destination type selected. Ending") + return + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - category_repository = CategoryRepository(uow) - roms_repository = ROMsRepository(uow) - - parent_category = category_repository.find_category(parent_id) if parent_id is not None else None - grand_parent_category = category_repository.find_category(grand_parent_id) if grand_parent_id is not None else None - - if grand_parent_category is not None: - options_dialog = kodi.ListDialog() - selected_option = options_dialog.select(kodi.translate(41098),[parent_category.get_name(), grand_parent_category.get_name()]) - if selected_option > 0: - parent_category = grand_parent_category + repository = ROMsRepository(uow) + collections_repository = ROMCollectionRepository(uow) + categories_repository = CategoryRepository(uow) + + options = collections.OrderedDict() + if selected_type == 0: + romcollections = collections_repository.find_all_romcollections() + for collection in romcollections: + options[collection] = collection.get_name() + else: + categories = categories_repository.find_all_categories() + for category in categories: + options[category] = category.get_name() + + dialog = kodi.OrdDictionaryDialog() + selected_destination = dialog.select(kodi.translate(41186), options) - rom_name = "" - is_file_based = kodi.dialog_yesno(kodi.translate(40952)) - file_path = path = None - if is_file_based: - file_path = kodi.dialog_get_file(kodi.translate(40953)) - if file_path is not None: - path = io.FileName(file_path) - rom_name = path.getBaseNoExt() - - rom_name = kodi.dialog_keyboard(kodi.translate(40815), rom_name) - if rom_name is None: + if selected_destination is None: + logger.debug("LINK_ROM: No destination selected. Ending") return - dialog = kodi.ListDialog() - selected_idx = dialog.select(kodi.translate(41099), platforms.AKL_platform_list) - platform = platforms.AKL_platform_list[selected_idx] - - rom_obj = ROM() - rom_obj.set_name(rom_name) - rom_obj.set_platform(platform) - if file_path: - rom_obj.set_scanned_data_element("file", file_path) - if is_file_based: - addon_repository = AelAddonRepository(uow) - addon_id = "script.akl.defaults" - addon = addon_repository.find_by_addon_id(addon_id, constants.AddonType.LAUNCHER) - rom_obj.add_launcher(addon, { - "addon_id": addon_id, - "application": file_path, - "args": "", - "secname": path.getBase() - }) - - roms_repository.insert_rom(rom_obj) - category_repository.add_rom_to_category(parent_category.get_id(), rom_obj.get_id()) + rom = repository.find_rom(rom_id) + if selected_type == 0: + collections_repository.add_rom_to_romcollection(selected_destination.get_id(), rom.get_id()) + else: + categories_repository.add_rom_to_category(selected_destination.get_id(), rom.get_id()) + uow.commit() - - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) - AppMediator.async_cmd('RENDER_VCATEGORY_VIEW', {'vcategory_id': constants.VCATEGORY_TITLE_ID}) - kodi.notify(kodi.translate(41035).format(rom_name)) - kodi.refresh_container() \ No newline at end of file + + if selected_type == 0: + kodi.notify(kodi.translate(41188).format(rom.get_name(), selected_destination.get_name())) + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': selected_destination.get_id()}) + else: + kodi.notify(kodi.translate(41187).format(rom.get_name(), selected_destination.get_name())) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': selected_destination.get_id()}) diff --git a/resources/lib/commands/rom_launcher_commands.py b/resources/lib/commands/rom_launcher_commands.py index ca4d1f88..c039a86b 100644 --- a/resources/lib/commands/rom_launcher_commands.py +++ b/resources/lib/commands/rom_launcher_commands.py @@ -25,19 +25,114 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals -from resources.lib.repositories import UnitOfWork, ROMCollectionRepository, ROMsRepository, AelAddonRepository +from resources.lib.repositories import UnitOfWork, AelAddonRepository, LaunchersRepository +from resources.lib.repositories import ROMCollectionRepository, ROMsRepository, SourcesRepository from resources.lib.domain import AelAddon, ROMLauncherAddon, ROMLauncherAddonFactory logger = logging.getLogger(__name__) + # ------------------------------------------------------------------------------------------------- -# ROMCollection launcher management. +# Launcher management. # ------------------------------------------------------------------------------------------------- +@AppMediator.register('ADD_LAUNCHER') +def cmd_add_launcher(args): + options = collections.OrderedDict() + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = AelAddonRepository(uow) + addons = repository.find_all_launcher_addons() + + for addon in addons: + options[addon] = addon.get_name() + + s = kodi.translate(41106) + selected_option: AelAddon = kodi.OrdDictionaryDialog().select(s, options) + + if selected_option is None: + # >> Exits context menu + logger.debug('Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) + return + + # >> Execute subcommand. May be atomic, maybe a submenu. + logger.debug(f'Selected {selected_option.get_id()}') + + selected_launcher = ROMLauncherAddonFactory.create(selected_option, {}) + selected_launcher.configure(args) + + +@AppMediator.register('EDIT_LAUNCHER') +def cmd_edit_launcher(args): + logger.debug('EDIT_LAUNCHER') + launcher_id: str = args['launcher_id'] if 'launcher_id' in args else None + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = LaunchersRepository(uow) + launcher = repository.find(launcher_id) + + launcher.configure(args) + + +@AppMediator.register('DELETE_LAUNCHER') +def cmd_delete_launcher(args): + logger.debug('DELETE_LAUNCHER') + launcher_id: str = args['launcher_id'] if 'launcher_id' in args else None + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = LaunchersRepository(uow) + launcher = repository.find(launcher_id) + + confirmed = kodi.dialog_yesno(kodi.translate(41066).format(launcher.get_name())) + if not confirmed: + return + + repository.delete_launcher(launcher) + kodi.refresh_container() + + +# ------------------------------------------------------------------------------------------------- +# Launcher association. +# ------------------------------------------------------------------------------------------------- +@AppMediator.register('EDIT_SOURCE_LAUNCHERS') +def cmd_manage_source_launchers(args): + logger.debug('EDIT_SOURCE_LAUNCHERS: SHOW MENU') + source_id: str = args['source_id'] if 'source_id' in args else None + + selected_option = None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + launchers = source.get_launchers() + default_launcher = next((lr for lr in launchers if lr.is_default()), launchers[0]) if len(launchers) > 0 else None + default_launcher_name = default_launcher.get_name() if default_launcher is not None else 'None' + + options = collections.OrderedDict() + options['ADD_SOURCE_LAUNCHER'] = kodi.translate(42026) + options['REMOVE_SOURCE_LAUNCHER'] = kodi.translate(42028) + options['SET_DEFAULT_SOURCE_LAUNCHER'] = kodi.translate(42029).format(default_launcher_name) + + s = kodi.translate(41100).format(source.get_name()) + selected_option = kodi.OrdDictionaryDialog().select(s, options) + if selected_option is None: + # >> Exits context menu + logger.debug('EDIT_SOURCE_LAUNCHERS: Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_SOURCE', args) + return + + # >> Execute subcommand. May be atomic, maybe a submenu. + logger.debug(f'EDIT_SOURCE_LAUNCHERS: Selected {selected_option}') + AppMediator.sync_cmd(selected_option, args) + @AppMediator.register('EDIT_ROMCOLLECTION_LAUNCHERS') def cmd_manage_romcollection_launchers(args): logger.debug('EDIT_ROMCOLLECTION_LAUNCHERS: cmd_manage_romcollection_launchers() SHOW MENU') - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None selected_option = None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -46,14 +141,13 @@ def cmd_manage_romcollection_launchers(args): romcollection = repository.find_romcollection(romcollection_id) launchers = romcollection.get_launchers() - default_launcher = next((l for l in launchers if l.is_default()), launchers[0]) if len(launchers) > 0 else None + default_launcher = next((lc for lc in launchers if lc.is_default()), launchers[0]) if len(launchers) > 0 else None default_launcher_name = default_launcher.get_name() if default_launcher is not None else 'None' options = collections.OrderedDict() - options['ADD_LAUNCHER'] = kodi.translate(42026) - options['EDIT_LAUNCHER'] = kodi.translate(42027) - options['REMOVE_LAUNCHER'] = kodi.translate(42028) - options['SET_DEFAULT_LAUNCHER'] = kodi.translate(42029).format(default_launcher_name) + options['ADD_COLLECTION_LAUNCHER'] = kodi.translate(42026) + options['REMOVE_COLLECTION_LAUNCHER'] = kodi.translate(42028) + options['SET_DEFAULT_COLLECTION_LAUNCHER'] = kodi.translate(42029).format(default_launcher_name) s = kodi.translate(41100).format(romcollection.get_name()) selected_option = kodi.OrdDictionaryDialog().select(s, options) @@ -67,24 +161,24 @@ def cmd_manage_romcollection_launchers(args): logger.debug(f'EDIT_ROMCOLLECTION_LAUNCHERS: Selected {selected_option}') AppMediator.sync_cmd(selected_option, args) + @AppMediator.register('EDIT_ROM_LAUNCHERS') def cmd_manage_rom_launchers(args): logger.debug('EDIT_ROM_LAUNCHERS: cmd_manage_rom_launchers() SHOW MENU') - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None selected_option = None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMsRepository(uow) + repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) launchers = rom.get_launchers() - default_launcher = next((l for l in launchers if l.is_default()), launchers[0]) if len(launchers) > 0 else None + default_launcher = next((lc for lc in launchers if lc.is_default()), launchers[0]) if len(launchers) > 0 else None default_launcher_name = default_launcher.get_name() if default_launcher is not None else kodi.translate(20010) options = collections.OrderedDict() options['ADD_ROM_LAUNCHER'] = kodi.translate(42026) - options['EDIT_ROM_LAUNCHER'] = kodi.translate(42027) options['REMOVE_ROM_LAUNCHER'] = kodi.translate(42028) options['SET_DEFAULT_ROM_LAUNCHER'] = kodi.translate(42029).format(default_launcher_name) @@ -100,136 +194,164 @@ def cmd_manage_rom_launchers(args): logger.debug(f'Selected {selected_option}') AppMediator.sync_cmd(selected_option, args) + # --- Sub commands --- @AppMediator.register('ADD_ROM_LAUNCHER') def cmd_add_rom_launchers(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None options = collections.OrderedDict() uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = AelAddonRepository(uow) - rom_repository = ROMsRepository(uow) + repository = LaunchersRepository(uow) + rom_repository = ROMsRepository(uow) - addons = repository.find_all_launchers() + launchers = repository.find_all() rom = rom_repository.find_rom(rom_id) + for launcher in launchers: + options[launcher] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) + options["NEW"] = kodi.translate(42090) + + s = kodi.translate(41101) + selected_option = kodi.OrdDictionaryDialog().select(s, options, use_details=True) + + if selected_option is None: + # >> Exits context menu + logger.debug('Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) + return + + if selected_option == "NEW": + args["entity_type"] = constants.OBJ_ROM + args["entity_id"] = rom_id + AppMediator.sync_cmd("ADD_LAUNCHER", args) + return + + selected_option: ROMLauncherAddon = selected_option + logger.debug(f'Selected {selected_option.get_id()}') + is_default = kodi.dialog_yesno(kodi.translate(41171).format(selected_option.get_name())) + + rom.add_launcher(launcher, is_default) + rom_repository.update_rom(rom) + logger.info(f'Added launcher#{selected_option.get_id()} to ROM {rom.get_id()}') + uow.commit() + + repository = AelAddonRepository(uow) + addons = repository.find_all_launcher_addons() + for addon in addons: options[addon] = addon.get_name() - s = kodi.translate(41101) - selected_option:AelAddon = kodi.OrdDictionaryDialog().select(s, options) - - if selected_option is None: - # >> Exits context menu - logger.debug('Selected None. Closing context menu') - AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug(f'Selected {selected_option.get_id()}') + s = kodi.translate(41106) + selected_option: AelAddon = kodi.OrdDictionaryDialog().select(s, options) - selected_launcher = ROMLauncherAddonFactory.create(selected_option, {}) - selected_launcher.configure_for_rom(rom) + kodi.notify(kodi.translate(41109).format(selected_option.get_name())) + AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) -@AppMediator.register('ADD_LAUNCHER') -def cmd_add_romcollection_launchers(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - + +@AppMediator.register('ADD_SOURCE_LAUNCHER') +def cmd_add_source_launchers(args): + source_id: str = args['source_id'] if 'source_id' in args else None + options = collections.OrderedDict() uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = AelAddonRepository(uow) - romcollection_repository = ROMCollectionRepository(uow) + repository = LaunchersRepository(uow) + source_repository = SourcesRepository(uow) - addons = repository.find_all_launchers() - romcollection = romcollection_repository.find_romcollection(romcollection_id) + launchers = repository.find_all() + source = source_repository.find(source_id) - for addon in addons: - options[addon] = addon.get_name() - - s = kodi.translate(41101) - selected_option:AelAddon = kodi.OrdDictionaryDialog().select(s, options) - - if selected_option is None: - # >> Exits context menu - logger.debug('ADD_LAUNCHER: cmd_add_romcollection_launchers() Selected None. Closing context menu') - AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('ADD_LAUNCHER: cmd_add_romcollection_launchers() Selected {}'.format(selected_option.get_id())) + for launcher in launchers: + options[launcher] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) + options["NEW"] = kodi.translate(42090) - selected_launcher = ROMLauncherAddonFactory.create(selected_option, {}) - selected_launcher.configure(romcollection) + s = kodi.translate(41101) + selected_option: ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options, use_details=True) + + if selected_option is None: + # >> Exits context menu + logger.debug('ADD_SOURCE_LAUNCHER: Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + return + + if selected_option == "NEW": + args["entity_type"] = constants.OBJ_SOURCE + args["entity_id"] = source_id + AppMediator.sync_cmd("ADD_LAUNCHER", args) + return + + logger.debug(f'ADD_SOURCE_LAUNCHER: Selected {selected_option.get_id()}') + is_default = kodi.dialog_yesno(kodi.translate(41171).format(selected_option.get_name())) + + source.add_launcher(launcher, is_default) + if kodi.dialog_yesno(kodi.translate(41050)): + source.import_data_dic(launcher.get_settings()['romcollection']) + + source_repository.update_source(source) + logger.info(f'Added launcher#{selected_option.get_id()} to Source {source.get_id()}') + uow.commit() -@AppMediator.register('EDIT_LAUNCHER') -def cmd_edit_romcollection_launchers(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - romcollection_repository = ROMCollectionRepository(uow) - romcollection = romcollection_repository.find_romcollection(romcollection_id) - - launchers = romcollection.get_launchers() - if len(launchers) == 0: - kodi.notify(kodi.translate(41003)) - AppMediator.async_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) - return + kodi.notify(kodi.translate(41109).format(selected_option.get_name())) + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + + +@AppMediator.register('ADD_COLLECTION_LAUNCHER') +def cmd_add_romcollection_launchers(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + metadata_updated = False options = collections.OrderedDict() - for launcher in launchers: - options[launcher] = launcher.get_name() - - s = kodi.translate(41102) - selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) - - if selected_option is None: - # >> Exits context menu - logger.debug('EDIT_LAUNCHER: cmd_edit_romcollection_launchers() Selected None. Closing context menu') - AppMediator.async_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('EDIT_LAUNCHER: cmd_edit_romcollection_launchers() Selected {}'.format(selected_option.get_id())) - selected_option.configure(romcollection) - -@AppMediator.register('EDIT_ROM_LAUNCHER') -def cmd_edit_rom_launcher(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMsRepository(uow) - rom = repository.find_rom(rom_id) + repository = LaunchersRepository(uow) + romcollection_repository = ROMCollectionRepository(uow) - launchers = rom.get_launchers() - if len(launchers) == 0: - kodi.notify(kodi.translate(41002)) - AppMediator.async_cmd('EDIT_ROM_LAUNCHERS', args) - return - - options = collections.OrderedDict() - for launcher in launchers: - options[launcher] = launcher.get_name() - - s = kodi.translate(41102) - selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) + launchers = repository.find_all() + romcollection = romcollection_repository.find_romcollection(romcollection_id) + + for launcher in launchers: + options[launcher] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) + options["NEW"] = kodi.translate(42090) + + s = kodi.translate(41101) + selected_option: ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options, use_details=True) - if selected_option is None: - # >> Exits context menu - logger.debug('Selected None. Closing context menu') - AppMediator.async_cmd('EDIT_ROM_LAUNCHERS', args) - return + if selected_option is None: + # >> Exits context menu + logger.debug('ADD_COLLECTION_LAUNCHER: Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) + return - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug(f'Selected {selected_option.get_id()}') - selected_option.configure_for_rom(rom) + if selected_option == "NEW": + args["entity_type"] = constants.OBJ_ROMCOLLECTION + args["entity_id"] = romcollection_id + AppMediator.sync_cmd("ADD_LAUNCHER", args) + return + + logger.debug(f'ADD_COLLECTION_LAUNCHER: Selected {selected_option.get_id()}') + is_default = kodi.dialog_yesno(kodi.translate(41171).format(selected_option.get_name())) + + romcollection.add_launcher(launcher, is_default) + if kodi.dialog_yesno(kodi.translate(41050)): + romcollection.import_data_dic(launcher.get_settings()['romcollection']) + metadata_updated = True + + romcollection_repository.update_romcollection(romcollection) + logger.info(f'Added launcher#{selected_option.get_id()} to ROMCollection {romcollection.get_id()}') + uow.commit() -@AppMediator.register('REMOVE_LAUNCHER') + if metadata_updated: + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + + kodi.notify(kodi.translate(41109).format(selected_option.get_name())) + AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) + + +@AppMediator.register('REMOVE_COLLECTION_LAUNCHER') def cmd_remove_romcollection_launchers(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -244,21 +366,21 @@ def cmd_remove_romcollection_launchers(args): options = collections.OrderedDict() for launcher in launchers: - options[launcher] = launcher.get_name() + options[launcher] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) s = kodi.translate(41103) - selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) + selected_option: ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options, use_details=True) if selected_option is None: # >> Exits context menu - logger.debug('REMOVE_LAUNCHER: cmd_remove_romcollection_launchers() Selected None. Closing context menu') + logger.debug('REMOVE_COLLECTION_LAUNCHER: cmd_remove_romcollection_launchers() Selected None. Closing context menu') AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('REMOVE_LAUNCHER: cmd_remove_romcollection_launchers() Selected {}'.format(selected_option.get_id())) + logger.debug('REMOVE_COLLECTION_LAUNCHER: Selected {}'.format(selected_option.get_id())) if not kodi.dialog_yesno(kodi.translate(41059).format(selected_option.get_name())): - logger.debug('REMOVE_LAUNCHER: cmd_remove_romcollection_launchers() Cancelled operation.') + logger.debug('REMOVE_COLLECTION_LAUNCHER: cmd_remove_romcollection_launchers() Cancelled operation.') AppMediator.async_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return @@ -268,10 +390,53 @@ def cmd_remove_romcollection_launchers(args): kodi.notify(kodi.translate(41004).format(selected_option.get_name())) AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) + + +@AppMediator.register('REMOVE_SOURCE_LAUNCHER') +def cmd_remove_source_launchers(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + source_repository = SourcesRepository(uow) + source = source_repository.find(source_id) + + launchers = source.get_launchers() + if len(launchers) == 0: + kodi.notify(kodi.translate(41168)) + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + return + + options = collections.OrderedDict() + for launcher in launchers: + options[launcher] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) + + s = kodi.translate(41103) + selected_option: ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options, use_details=True) + + if selected_option is None: + # >> Exits context menu + logger.debug('REMOVE_SOURCE_LAUNCHER: Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + return + + logger.debug(f'REMOVE_SOURCE_LAUNCHER: Selected {selected_option.get_id()}') + if not kodi.dialog_yesno(kodi.translate(41059).format(selected_option.get_name())): + logger.debug('REMOVE_SOURCE_LAUNCHER: Cancelled operation.') + AppMediator.async_cmd('EDIT_SOURCE_LAUNCHERS', args) + return + + source_repository.remove_launcher(source.get_id(), selected_option.get_id()) + logger.info(f'Removed launcher#{selected_option.get_id()}') + uow.commit() + + kodi.notify(kodi.translate(41004).format(selected_option.get_name())) + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + @AppMediator.register('REMOVE_ROM_LAUNCHER') def cmd_remove_rom_launchers(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -286,10 +451,10 @@ def cmd_remove_rom_launchers(args): options = collections.OrderedDict() for launcher in launchers: - options[launcher] = launcher.get_name() + options[launcher] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) s = kodi.translate(41103) - selected_option:ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options) + selected_option: ROMLauncherAddon = kodi.OrdDictionaryDialog().select(s, options, use_details=True) if selected_option is None: # >> Exits context menu @@ -297,7 +462,6 @@ def cmd_remove_rom_launchers(args): AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) return - # >> Execute subcommand. May be atomic, maybe a submenu. logger.debug(f'Selected {selected_option.get_id()}') if not kodi.dialog_yesno(kodi.translate(41059).format(selected_option.get_name())): logger.debug('Cancelled operation.') @@ -310,14 +474,15 @@ def cmd_remove_rom_launchers(args): kodi.notify(kodi.translate(41004).format(selected_option.get_name())) AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) + -@AppMediator.register('SET_DEFAULT_LAUNCHER') +@AppMediator.register('SET_DEFAULT_COLLECTION_LAUNCHER') def cmd_set_default_romcollection_launchers(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - romcollection_repository = ROMCollectionRepository(uow) + romcollection_repository = ROMCollectionRepository(uow) romcollection = romcollection_repository.find_romcollection(romcollection_id) launchers = romcollection.get_launchers() @@ -328,32 +493,70 @@ def cmd_set_default_romcollection_launchers(args): options = collections.OrderedDict() for launcher in launchers: - options[launcher.get_id()] = launcher.get_name() + options[launcher.get_id()] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) s = kodi.translate(41104) - selected_option = kodi.OrdDictionaryDialog().select(s, options) + selected_option = kodi.OrdDictionaryDialog().select(s, options, use_details=True) if selected_option is None: # >> Exits context menu - logger.debug('SET_DEFAULT_LAUNCHER: cmd_set_default_romcollection_launchers() Selected None. Closing context menu') + logger.debug('SET_DEFAULT_COLLECTION_LAUNCHER: Selected None. Closing context menu') AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) return # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('SET_DEFAULT_LAUNCHER: cmd_set_default_romcollection_launchers() Selected {}'.format(selected_option)) + logger.debug(f'SET_DEFAULT_COLLECTION_LAUNCHER: Selected {selected_option}') romcollection.set_launcher_as_default(selected_option) romcollection_repository.update_romcollection(romcollection) uow.commit() AppMediator.sync_cmd('EDIT_ROMCOLLECTION_LAUNCHERS', args) + + +@AppMediator.register('SET_DEFAULT_SOURCE_LAUNCHER') +def cmd_set_default_source_launchers(args): + source_id: str = args['source_id'] if 'source_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + source_repository = SourcesRepository(uow) + source = source_repository.find(source_id) + + launchers = source.get_launchers() + if len(launchers) == 0: + kodi.notify(kodi.translate(41168)) + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + return + + options = collections.OrderedDict() + for launcher in launchers: + options[launcher.get_id()] = launcher.get_name() + + s = kodi.translate(41104) + selected_option = kodi.OrdDictionaryDialog().select(s, options, use_details=True) + + if selected_option is None: + # >> Exits context menu + logger.debug('Selected None. Closing context menu') + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + return + + # >> Execute subcommand. May be atomic, maybe a submenu. + logger.debug(f'Selected {selected_option}') + source.set_launcher_as_default(selected_option) + source_repository.update_source(source) + uow.commit() + + AppMediator.sync_cmd('EDIT_SOURCE_LAUNCHERS', args) + + @AppMediator.register('SET_DEFAULT_ROM_LAUNCHER') def cmd_set_default_rom_launchers(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMsRepository(uow) + repository = ROMsRepository(uow) rom = repository.find_rom(rom_id) launchers = rom.get_launchers() @@ -364,10 +567,10 @@ def cmd_set_default_rom_launchers(args): options = collections.OrderedDict() for launcher in launchers: - options[launcher.get_id()] = launcher.get_name() + options[launcher.get_id()] = kodi.get_listitem(launcher.get_name(), launcher.get_addon_name()) s = kodi.translate(41104) - selected_option = kodi.OrdDictionaryDialog().select(s, options) + selected_option = kodi.OrdDictionaryDialog().select(s, options, use_details=True) if selected_option is None: # >> Exits context menu @@ -382,27 +585,33 @@ def cmd_set_default_rom_launchers(args): uow.commit() AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) - + + # ------------------------------------------------------------------------------------------------- # ROMCollection Launcher executing # ------------------------------------------------------------------------------------------------- @AppMediator.register('EXECUTE_ROM') def cmd_execute_rom_with_launcher(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - rom_repository = ROMsRepository(uow) + rom_repository = ROMsRepository(uow) romcollection_repository = ROMCollectionRepository(uow) - addon_repository = AelAddonRepository(uow) + source_repository = SourcesRepository(uow) + addon_repository = AelAddonRepository(uow) rom = rom_repository.find_rom(rom_id) logger.info(f'Executing ROM {rom.get_name()}') romcollections = romcollection_repository.find_romcollections_by_rom(rom.get_id()) + source = source_repository.find(rom.get_scanned_by()) launchers = rom.get_launchers() - for romcollection in romcollections: + if source: + launchers.extend(source.get_launchers()) + + for romcollection in romcollections: launchers.extend(romcollection.get_launchers()) - + if launchers is None or len(launchers) == 0: logger.warning(f'No launcher configured for ROM {rom.get_name()}') if not settings.getSettingAsBool('fallback_to_retroplayer'): @@ -423,10 +632,10 @@ def cmd_execute_rom_with_launcher(args): if launcher.is_default(): preselected = launcher dialog = kodi.OrdDictionaryDialog() - selected_launcher = dialog.select(kodi.translate(41105), launcher_options,preselect=preselected) + selected_launcher = dialog.select(kodi.translate(41105), launcher_options, preselect=preselected) if selected_launcher is None: return selected_launcher.launch(rom) - AppMediator.async_cmd('ROM_WAS_LAUNCHED', args) \ No newline at end of file + AppMediator.async_cmd('ROM_WAS_LAUNCHED', args) diff --git a/resources/lib/commands/rom_scanner_commands.py b/resources/lib/commands/rom_scanner_commands.py deleted file mode 100644 index 19982896..00000000 --- a/resources/lib/commands/rom_scanner_commands.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Advanced Kodi Launcher: Commands (romcollection scanner management) -# -# Copyright (c) Wintermute0110 / Chrisism -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 2 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# --- Python standard library --- -from __future__ import unicode_literals -from __future__ import division -from __future__ import annotations - -import logging -import collections - -from akl.utils import kodi - -from resources.lib.commands.mediator import AppMediator -from resources.lib import globals -from resources.lib.repositories import UnitOfWork, ROMCollectionRepository, ROMsRepository, AelAddonRepository -from resources.lib.domain import ROM, ROMCollectionScanner, AelAddon - -logger = logging.getLogger(__name__) - -# ------------------------------------------------------------------------------------------------- -# ROM Scanners management. -# ------------------------------------------------------------------------------------------------- - -# --- Submenu menu command --- -@AppMediator.register('EDIT_ROMCOLLECTION_SCANNERS') -def cmd_manage_romcollection_scanners(args): - logger.debug('EDIT_ROMCOLLECTION_SCANNERS: cmd_manage_romcollection_scanners() SHOW MENU') - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - selected_option = None - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - repository = ROMCollectionRepository(uow) - romcollection = repository.find_romcollection(romcollection_id) - - options = collections.OrderedDict() - options['ADD_SCANNER'] = kodi.translate(42080) - options['EDIT_SCANNER'] = kodi.translate(42081) - options['REMOVE_SCANNER'] = kodi.translate(42082) - - s = kodi.translate(41106).format(romcollection.get_name()) - selected_option = kodi.OrdDictionaryDialog().select(s, options) - if selected_option is None: - # >> Exits context menu - logger.debug('EDIT_ROMCOLLECTION_SCANNERS: cmd_manage_romcollection_scanners() Selected None. Closing context menu') - AppMediator.async_cmd('EDIT_ROMCOLLECTION', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('EDIT_ROMCOLLECTION_SCANNERS: cmd_manage_romcollection_scanners() Selected {}'.format(selected_option)) - AppMediator.async_cmd(selected_option, args) - -# --- Sub commands --- -@AppMediator.register('ADD_SCANNER') -def cmd_add_romcollection_scanner(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - options = collections.OrderedDict() - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - repository = AelAddonRepository(uow) - romcollection_repository = ROMCollectionRepository(uow) - - addons = repository.find_all_scanners() - romcollection = romcollection_repository.find_romcollection(romcollection_id) - - for addon in addons: - options[addon] = addon.get_name() - - s = kodi.translate(41107) - selected_option:AelAddon = kodi.OrdDictionaryDialog().select(s, options) - - if selected_option is None: - # >> Exits context menu - logger.debug('ADD_SCANNER: cmd_add_romcollection_scanner() Selected None. Closing context menu') - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('ADD_SCANNER: cmd_add_romcollection_scanner() Selected {}'.format(selected_option.get_id())) - - scanner_addon = ROMCollectionScanner(selected_option, {}) - kodi.notify(kodi.translate(40980)) - kodi.run_script( - selected_option.get_addon_id(), - scanner_addon.get_configure_command(romcollection)) - -@AppMediator.register('EDIT_SCANNER') -def cmd_edit_romcollection_scanners(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - romcollection_repository = ROMCollectionRepository(uow) - romcollection = romcollection_repository.find_romcollection(romcollection_id) - default_launcher = romcollection.get_default_launcher() - - scanners = romcollection.get_scanners() - if len(scanners) == 0: - kodi.notify(kodi.translate(40982)) - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - return - - options = collections.OrderedDict() - for scanner in scanners: - options[scanner] = scanner.get_name() - - s = kodi.translate(41108) - selected_option:ROMCollectionScanner = kodi.OrdDictionaryDialog().select(s, options) - - if selected_option is None: - # >> Exits context menu - logger.debug('EDIT_SCANNER: cmd_edit_romcollection_scanners() Selected None. Closing context menu') - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('EDIT_SCANNER: cmd_edit_romcollection_scanners() Selected {}'.format(selected_option.get_id())) - - kodi.notify(kodi.translate(40980)) - kodi.run_script( - selected_option.addon.get_addon_id(), - selected_option.get_configure_command(romcollection)) - -@AppMediator.register('REMOVE_SCANNER') -def cmd_remove_romcollection_scanner(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - romcollection_repository = ROMCollectionRepository(uow) - romcollection = romcollection_repository.find_romcollection(romcollection_id) - - scanners = romcollection.get_scanners() - if len(scanners) == 0: - kodi.notify(kodi.translate(40982)) - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - return - - options = collections.OrderedDict() - for scanner in scanners: - options[scanner] = scanner.get_name() - - s = kodi.translate(41109) - selected_option:ROMCollectionScanner = kodi.OrdDictionaryDialog().select(s, options) - - if selected_option is None: - # >> Exits context menu - logger.debug('REMOVE_SCANNER: cmd_remove_romcollection_scanner() Selected None. Closing context menu') - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - return - - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('REMOVE_SCANNER: cmd_remove_romcollection_scanner() Selected {}'.format(selected_option.get_id())) - if not kodi.dialog_yesno(kodi.translate(41060).format(selected_option.get_name())): - logger.debug('REMOVE_SCANNER: cmd_remove_romcollection_scanner() Cancelled operation.') - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - return - - romcollection_repository.remove_scanner(romcollection.get_id(), selected_option.get_id()) - logger.info('REMOVE_SCANNER: cmd_remove_romcollection_scanner() Removed scanner#{}'.format(selected_option.get_id())) - uow.commit() - - AppMediator.async_cmd('EDIT_ROMCOLLECTION_SCANNERS', args) - -# ------------------------------------------------------------------------------------------------- -# ROMCollection Scanner executing -# ------------------------------------------------------------------------------------------------- -@AppMediator.register('SCAN_ROMS') -def cmd_execute_rom_scanner(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) - with uow: - romcollection_repository = ROMCollectionRepository(uow) - romcollection = romcollection_repository.find_romcollection(romcollection_id) - - scanners = romcollection.get_scanners() - if scanners is None or len(scanners) == 0: - kodi.notify_warn(kodi.translate(41000)) - return - - selected_scanner = scanners[0] - if len(scanners) > 1: - scanner_options = collections.OrderedDict() - for scanner in scanners: - scanner_options[scanner] = scanner.get_name() - dialog = kodi.OrdDictionaryDialog() - selected_scanner = dialog.select(kodi.translate(41110), scanner_options) - - if selected_scanner is None: - # >> Exits context menu - logger.debug('SCAN_ROMS: cmd_execute_rom_scanner() Selected None. Closing context menu') - AppMediator.async_cmd('ROMCOLLECTION_MANAGE_ROMS', args) - return - - logger.info('SCAN_ROMS: selected scanner "{}"'.format(selected_scanner.get_name())) - kodi.notify(kodi.translate(40980)) - kodi.run_script( - selected_scanner.addon.get_addon_id(), - selected_scanner.get_scan_command(romcollection)) \ No newline at end of file diff --git a/resources/lib/commands/rom_scraper_commands.py b/resources/lib/commands/rom_scraper_commands.py index 6ece2257..a5b508c9 100644 --- a/resources/lib/commands/rom_scraper_commands.py +++ b/resources/lib/commands/rom_scraper_commands.py @@ -26,8 +26,9 @@ from resources.lib.commands.mediator import AppMediator from resources.lib import globals -from resources.lib.repositories import UnitOfWork, AelAddonRepository, ROMsRepository, ROMCollectionRepository -from resources.lib.domain import ROMCollection, ScraperAddon, g_assetFactory +from resources.lib.repositories import UnitOfWork, AelAddonRepository, ROMsRepository +from resources.lib.repositories import ROMCollectionRepository, SourcesRepository +from resources.lib.domain import ROMCollection, Source, ScraperAddon, g_assetFactory logger = logging.getLogger(__name__) @@ -36,22 +37,22 @@ # ------------------------------------------------------------------------------------------------- @AppMediator.register('SCRAPE_ROMS') def cmd_scrape_romcollection(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: collection_repository = ROMCollectionRepository(uow) collection = collection_repository.find_romcollection(romcollection_id) - scraper_settings:ScraperSettings = ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = ScraperSettings.from_addon_settings() - dialog_title = kodi.translate(41124).format(collection.get_name()) - selected_addon = _select_scraper(uow, dialog_title, scraper_settings) + dialog_title = kodi.translate(41124).format(collection.get_name()) + selected_addon = _select_scraper(uow, dialog_title, scraper_settings) if selected_addon is None: # >> Exits context menu logger.debug('SCRAPE_ROMS: cmd_scrape_romcollection() Selected None. Closing context menu') AppMediator.sync_cmd('ROMCOLLECTION_MANAGE_ROMS', args) - return + return scraper_settings.asset_IDs_to_scrape = selected_addon.get_supported_assets() scraper_settings.metadata_IDs_to_scrape = selected_addon.get_supported_metadata() @@ -64,17 +65,49 @@ def cmd_scrape_romcollection(args): AppMediator.sync_cmd('SCRAPE_ROMS_WITH_SETTINGS', args) + +@AppMediator.register('SCRAPE_SOURCE_ROMS') +def cmd_scrape_source(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + source_repository = SourcesRepository(uow) + source = source_repository.find(source_id) + + scraper_settings: ScraperSettings = ScraperSettings.from_addon_settings() + + dialog_title = kodi.translate(41110).format(source.get_name()) + selected_addon = _select_scraper(uow, dialog_title, scraper_settings) + if selected_addon is None: + # >> Exits context menu + logger.debug('SCRAPE_SOURCE_ROMS: cmd_scrape_source() Selected None. Closing context menu') + AppMediator.sync_cmd('SOURCE_MANAGE_ROMS', args) + return + + scraper_settings.asset_IDs_to_scrape = selected_addon.get_supported_assets() + scraper_settings.metadata_IDs_to_scrape = selected_addon.get_supported_metadata() + + logger.debug(f'cmd_scrape_source() Selected scraper#{selected_addon.get_name()}') + args['scraper_settings'] = scraper_settings + args['scraper_id'] = selected_addon.addon.get_id() + args['scraper_supported_metadata'] = selected_addon.get_supported_metadata() + args['scraper_supported_assets'] = selected_addon.get_supported_assets() + + AppMediator.sync_cmd('SCRAPE_ROMS_WITH_SETTINGS', args) + + # Scrape ROM - Select scraper to use @AppMediator.register('SCRAPE_ROM') def cmd_scrape_rom(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: roms_repository = ROMsRepository(uow) - rom = roms_repository.find_rom(rom_id) + rom = roms_repository.find_rom(rom_id) - scraper_settings:ScraperSettings = ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = ScraperSettings.from_addon_settings() dialog_title = kodi.translate(41123).format(rom.get_name()) selected_addon = _select_scraper(uow, dialog_title, scraper_settings) @@ -82,7 +115,7 @@ def cmd_scrape_rom(args): # >> Exits context menu logger.debug('SCRAPE_ROM: Selected None. Closing context menu') AppMediator.sync_cmd('EDIT_ROM', args) - return + return scraper_settings.asset_IDs_to_scrape = selected_addon.get_supported_assets() scraper_settings.metadata_IDs_to_scrape = selected_addon.get_supported_metadata() @@ -95,18 +128,22 @@ def cmd_scrape_rom(args): AppMediator.sync_cmd('SCRAPE_ROM_WITH_SETTINGS', args) + @AppMediator.register('SCRAPE_ROMS_WITH_SETTINGS') -def cmd_scrape_roms_in_romcollection(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - scraper_id:str = args['scraper_id'] if 'scraper_id' in args else None - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() +def cmd_scrape_roms_in_collection_or_source(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + source_id: str = args['source_id'] if 'source_id' in args else None + scraper_id: str = args['scraper_id'] if 'scraper_id' in args else None + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: addon_repository = AelAddonRepository(uow) collection_repository = ROMCollectionRepository(uow) + source_repository = SourcesRepository(uow) collection = collection_repository.find_romcollection(romcollection_id) + source = source_repository.find(source_id) addon = addon_repository.find(scraper_id) selected_addon = ScraperAddon(addon, scraper_settings) @@ -117,7 +154,7 @@ def cmd_scrape_roms_in_romcollection(args): options['SCRAPER_METADATA_POLICY'] = kodi.translate(41115).format(kodi.translate(scraper_settings.scrape_metadata_policy)) options['SCRAPER_ASSET_POLICY'] = kodi.translate(41116).format(kodi.translate(scraper_settings.scrape_assets_policy)) options['SCRAPER_SEARCH_TERM_MODE'] = kodi.translate(41117).format(kodi.translate(scraper_settings.search_term_mode)) - options['SCRAPER_GAME_SELECTION_MODE'] = kodi.translate(41118).format(kodi.translate(scraper_settings.game_selection_mode)) + options['SCRAPER_GAME_SELECTION_MODE'] = kodi.translate(41118).format(kodi.translate(scraper_settings.game_selection_mode)) options['SCRAPER_ASSET_SELECTION_MODE'] = kodi.translate(41119).format(kodi.translate(scraper_settings.asset_selection_mode)) options['SCRAPER_META_TO_SCRAPE'] = kodi.translate(42030).format(', '.join(metadata_to_scrape)) options['SCRAPER_ASSETS_TO_SCRAPE'] = kodi.translate(42031).format(', '.join([a.plural for a in assets_to_scrape])) @@ -126,12 +163,15 @@ def cmd_scrape_roms_in_romcollection(args): options['SCRAPER_IGNORE_TITLES_MODE'] = kodi.translate(42034).format(kodi.translate(42035) if scraper_settings.ignore_scrap_title else kodi.translate(42036)) options['SCRAPE'] = kodi.translate(40881) - dialog_title = kodi.translate(41111).format(collection.get_name(), selected_addon.get_name()) + dialog_title = kodi.translate(41000 if source is None else 41111).format( + collection.get_name() if collection is not None else source.get_name(), + selected_addon.get_name()) selected_option = kodi.OrdDictionaryDialog().select(dialog_title, options, preselect='SCRAPE') if selected_option is None: - logger.debug('cmd_scrape_roms_in_romcollection() Selected None. Closing context menu') + logger.debug('cmd_scrape_roms_in_collection_or_source() Selected None. Closing context menu') del args['scraper_settings'] - AppMediator.sync_cmd('SCRAPE_ROMS', args) + ret_cmd = 'SCRAPE_ROMS' if collection is not None else 'SCRAPE_SOURCE_ROMS' + AppMediator.sync_cmd(ret_cmd, args) return if selected_option != 'SCRAPE': @@ -139,20 +179,27 @@ def cmd_scrape_roms_in_romcollection(args): AppMediator.sync_cmd(selected_option, args) return - _check_collection_unset_asset_dirs(collection, scraper_settings) + if not source: + sources = source_repository.find_sources_by_collection(romcollection_id) + for source in sources: + _check_collection_unset_asset_dirs(source, scraper_settings) + else: + _check_collection_unset_asset_dirs(source, scraper_settings) selected_addon.set_scraper_settings(scraper_settings) kodi.notify(kodi.translate(40979)) + entity = collection if collection else source kodi.run_script( selected_addon.addon.get_addon_id(), - selected_addon.get_scrape_command_for_collection(collection)) + selected_addon.get_scrape_command(entity)) + # Scrape ROM - Apply settings and run scrape action @AppMediator.register('SCRAPE_ROM_WITH_SETTINGS') def cmd_scrape_rom_with_settings(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None - scraper_id:str = args['scraper_id'] if 'scraper_id' in args else None - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + rom_id: str = args['rom_id'] if 'rom_id' in args else None + scraper_id: str = args['scraper_id'] if 'scraper_id' in args else None + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -163,8 +210,8 @@ def cmd_scrape_rom_with_settings(args): addon = addon_repository.find(scraper_id) selected_addon = ScraperAddon(addon, scraper_settings) - assets_to_scrape = g_assetFactory.get_asset_list_by_IDs(scraper_settings.asset_IDs_to_scrape) - metadata_to_scrape = [constants.METADATA_DESCRIPTIONS[meta_id] for meta_id in scraper_settings.metadata_IDs_to_scrape] + assets_to_scrape = g_assetFactory.get_asset_list_by_IDs(scraper_settings.asset_IDs_to_scrape) + metadata_to_scrape = [constants.METADATA_DESCRIPTIONS[meta_id] for meta_id in scraper_settings.metadata_IDs_to_scrape] options = collections.OrderedDict() options['SCRAPER_METADATA_POLICY'] = kodi.translate(41115).format(kodi.translate(scraper_settings.scrape_metadata_policy)) @@ -193,15 +240,16 @@ def cmd_scrape_rom_with_settings(args): return # >> Execute scraper - selected_addon.set_scraper_settings(scraper_settings) + selected_addon.set_scraper_settings(scraper_settings) kodi.notify(kodi.translate(40979)) kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) + @AppMediator.register('SCRAPE_ROM_METADATA') def cmd_scrape_rom_metadata(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -241,18 +289,19 @@ def cmd_scrape_rom_metadata(args): kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) - + + @AppMediator.register('SCRAPE_ROM_ASSET') def cmd_scrape_rom_asset(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None - asset_id:str = args['selected_asset'] if 'selected_asset' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None + asset_id: str = args['selected_asset'] if 'selected_asset' in args else None asset_to_scrape = g_assetFactory.get_asset_info(asset_id) uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: roms_repository = ROMsRepository(uow) - rom = roms_repository.find_rom(rom_id) + rom = roms_repository.find_rom(rom_id) scraper_settings = ScraperSettings() scraper_settings.scrape_assets_policy = constants.SCRAPE_POLICY_SCRAPE_ONLY @@ -271,21 +320,22 @@ def cmd_scrape_rom_asset(args): return # >> Execute scraper - logger.debug('SCRAPE_ROM_ASSET: Selected scraper#{}'.format(selected_addon.get_name())) + logger.debug(f'SCRAPE_ROM_ASSET: Selected scraper#{selected_addon.get_name()}') kodi.notify(kodi.translate(40979)) kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) - + + @AppMediator.register('SCRAPE_ROM_ASSETS') def cmd_scrape_rom_assets(args): - rom_id:str = args['rom_id'] if 'rom_id' in args else None + rom_id: str = args['rom_id'] if 'rom_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: roms_repository = ROMsRepository(uow) - rom = roms_repository.find_rom(rom_id) + rom = roms_repository.find_rom(rom_id) scraper_settings = ScraperSettings.from_addon_settings() scraper_settings.scrape_assets_policy = constants.SCRAPE_POLICY_SCRAPE_ONLY @@ -319,17 +369,18 @@ def cmd_scrape_rom_assets(args): selected_addon.set_scraper_settings(scraper_settings) kodi.notify(kodi.translate(40979)) - # >> Execute scraper + # >> Execute scraper kodi.run_script( selected_addon.addon.get_addon_id(), selected_addon.get_scrape_command(rom)) + # ------------------------------------------------------------------------------------------------- # Scraper settings configuration # ------------------------------------------------------------------------------------------------- @AppMediator.register('SCRAPER_METADATA_POLICY') def cmd_configure_scraper_metadata_policy(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() options[constants.SCRAPE_ACTION_NONE] = kodi.translate(constants.SCRAPE_ACTION_NONE) @@ -352,7 +403,7 @@ def cmd_configure_scraper_metadata_policy(args): @AppMediator.register('SCRAPER_ASSET_POLICY') def cmd_configure_scraper_asset_policy(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() options[constants.SCRAPE_ACTION_NONE] = kodi.translate(constants.SCRAPE_ACTION_NONE) @@ -374,7 +425,7 @@ def cmd_configure_scraper_asset_policy(args): @AppMediator.register('SCRAPER_SEARCH_TERM_MODE') def cmd_configure_scraper_search_term_mode(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) @@ -386,7 +437,7 @@ def cmd_configure_scraper_search_term_mode(args): AppMediator.sync_cmd(args['ret_cmd'], args) return - scraper_settings.search_term_mode = selected_option + scraper_settings.search_term_mode = selected_option args['scraper_settings'] = scraper_settings AppMediator.sync_cmd(args['ret_cmd'], args) return @@ -394,10 +445,10 @@ def cmd_configure_scraper_search_term_mode(args): @AppMediator.register('SCRAPER_GAME_SELECTION_MODE') def cmd_configure_scraper_game_selection_mode(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() - options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) + options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) options[constants.SCRAPE_AUTOMATIC] = kodi.translate(constants.SCRAPE_AUTOMATIC) s = kodi.translate(41118).format(kodi.translate(scraper_settings.game_selection_mode)) selected_option = kodi.OrdDictionaryDialog().select(s, options, preselect=scraper_settings.game_selection_mode) @@ -414,7 +465,7 @@ def cmd_configure_scraper_game_selection_mode(args): @AppMediator.register('SCRAPER_ASSET_SELECTION_MODE') def cmd_configure_scraper_asset_selection_mode(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() options = collections.OrderedDict() options[constants.SCRAPE_MANUAL] = kodi.translate(constants.SCRAPE_MANUAL) @@ -434,8 +485,8 @@ def cmd_configure_scraper_asset_selection_mode(args): @AppMediator.register('SCRAPER_META_TO_SCRAPE') def cmd_configure_scraper_metadata_to_scrape(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() - scraper_supported_metadata:list = args['scraper_supported_metadata'] if 'scraper_supported_metadata' in args else [] + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_supported_metadata: list = args['scraper_supported_metadata'] if 'scraper_supported_metadata' in args else [] options = collections.OrderedDict() for metadata_id in constants.METADATA_IDS: @@ -456,8 +507,8 @@ def cmd_configure_scraper_metadata_to_scrape(args): @AppMediator.register('SCRAPER_ASSETS_TO_SCRAPE') def cmd_configure_scraper_assets_to_scrape(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() - supported_assets:list = args['scraper_supported_assets'] if 'scraper_supported_assets' in args else [] + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + supported_assets: list = args['scraper_supported_assets'] if 'scraper_supported_assets' in args else [] asset_options = g_assetFactory.get_all() options = collections.OrderedDict() @@ -479,7 +530,7 @@ def cmd_configure_scraper_assets_to_scrape(args): @AppMediator.register('SCRAPER_OVERWRITE_META_MODE') def cmd_configure_scraper_overwrite_meta_mode(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() scraper_settings.overwrite_existing_meta = not scraper_settings.overwrite_existing_meta args['scraper_settings'] = scraper_settings AppMediator.sync_cmd(args['ret_cmd'], args) @@ -487,7 +538,7 @@ def cmd_configure_scraper_overwrite_meta_mode(args): @AppMediator.register('SCRAPER_OVERWRITE_ASSETS_MODE') def cmd_configure_scraper_overwrite_assets_mode(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() scraper_settings.overwrite_existing_assets = not scraper_settings.overwrite_existing_assets args['scraper_settings'] = scraper_settings AppMediator.sync_cmd(args['ret_cmd'], args) @@ -495,37 +546,39 @@ def cmd_configure_scraper_overwrite_assets_mode(args): @AppMediator.register('SCRAPER_IGNORE_TITLES_MODE') def cmd_configure_scraper_ignore_mode(args): - scraper_settings:ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() + scraper_settings: ScraperSettings = args['scraper_settings'] if 'scraper_settings' in args else ScraperSettings.from_addon_settings() scraper_settings.ignore_scrap_title = not scraper_settings.ignore_scrap_title args['scraper_settings'] = scraper_settings - AppMediator.sync_cmd(args['ret_cmd'], args) - -def _select_scraper(uow:UnitOfWork, title: str, scraper_settings: ScraperSettings) -> ScraperAddon: - selected_addon = None - repository = AelAddonRepository(uow) - addons = repository.find_all_scrapers() + AppMediator.sync_cmd(args['ret_cmd'], args) + + +def _select_scraper(uow: UnitOfWork, title: str, scraper_settings: ScraperSettings) -> ScraperAddon: + selected_addon = None + repository = AelAddonRepository(uow) + addons = repository.find_all_scraper_addons() # --- Make a menu list of available metadata scrapers --- - options = {} + options = {} for addon in addons: scraper_addon = ScraperAddon(addon, scraper_settings) if scraper_addon.settings_are_applicable(): options[scraper_addon] = addon.get_name() - selected_addon:ScraperAddon = kodi.OrdDictionaryDialog().select(title, options) + selected_addon: ScraperAddon = kodi.OrdDictionaryDialog().select(title, options) return selected_addon -def _check_collection_unset_asset_dirs(romcollection: ROMCollection, scraper_settings:ScraperSettings) -> bool: + +def _check_collection_unset_asset_dirs(source: Source, scraper_settings: ScraperSettings) -> bool: logger.debug('_check_launcher_unset_asset_dirs() BEGIN ...') unconfigured_name_list = [] enabled_asset_list = [] for asset_id in scraper_settings.asset_IDs_to_scrape: rom_asset = g_assetFactory.get_asset_info(asset_id) - asset_path = romcollection.get_asset_path(rom_asset, False) + asset_path = source.get_asset_path(rom_asset, False) if asset_path is None: - logger.debug('Directory not set. Asset "{}" will be disabled'.format(rom_asset)) + logger.debug(f'Directory not set. Asset "{rom_asset}" will be disabled') unconfigured_name_list.append(rom_asset.name) else: enabled_asset_list.append(rom_asset.id) @@ -537,4 +590,4 @@ def _check_collection_unset_asset_dirs(romcollection: ROMCollection, scraper_set logger.debug(msg) kodi.dialog_OK(msg) return False - return True \ No newline at end of file + return True diff --git a/resources/lib/commands/romcollection_commands.py b/resources/lib/commands/romcollection_commands.py index 6666d496..d533b2e1 100644 --- a/resources/lib/commands/romcollection_commands.py +++ b/resources/lib/commands/romcollection_commands.py @@ -30,6 +30,7 @@ logger = logging.getLogger(__name__) + @AppMediator.register('ADD_ROMCOLLECTION') def cmd_add_collection(args): logger.debug('cmd_add_collection() BEGIN') @@ -45,7 +46,7 @@ def cmd_add_collection(args): if grand_parent_category is not None: options_dialog = kodi.ListDialog() selected_option = options_dialog.select(kodi.translate(41125), [ - parent_category.get_name(), + parent_category.get_name(), grand_parent_category.get_name() ]) if selected_option is None: @@ -56,7 +57,6 @@ def cmd_add_collection(args): wizard = kodi.WizardDialog_Selection(None, 'platform', kodi.translate(41099), platforms.AKL_platform_list) wizard = kodi.WizardDialog_Dummy(wizard, 'm_name', '', _get_name_from_platform) wizard = kodi.WizardDialog_Keyboard(wizard, 'm_name', kodi.translate(42037)) - wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', kodi.translate(42038), 0, '') romcollection = ROMCollection() entity_data = romcollection.get_data_dic() @@ -66,11 +66,6 @@ def cmd_add_collection(args): romcollection.import_data_dic(entity_data) - # --- create assets directory --- - assets_path = entity_data['assets_path'] - assets_path_FN = io.FileName(assets_path) - romcollection.set_assets_root_path(assets_path_FN, constants.ROM_ASSET_ID_LIST, create_default_subdirectories=True) - # --- Determine box size based on platform -- platform = platforms.get_AKL_platform(entity_data['platform']) romcollection.set_box_sizing(platform.default_box_size) @@ -81,8 +76,9 @@ def cmd_add_collection(args): kodi.notify(kodi.translate(41017).format(romcollection.get_name())) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) - + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': parent_category.get_id()}) + + def _get_name_from_platform(input, item_key, entity_data): title = entity_data['platform'] return title @@ -91,12 +87,13 @@ def _get_name_from_platform(input, item_key, entity_data): # ROMCollection context menu. # ------------------------------------------------------------------------------------------------- + # --- Main menu command --- @AppMediator.register('EDIT_ROMCOLLECTION') def cmd_edit_romcollection(args): logger.debug('EDIT_ROMCOLLECTION: cmd_edit_romcollection() BEGIN') - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None if romcollection_id is None: logger.warning('cmd_edit_romcollection(): No romcollection id supplied.') @@ -111,7 +108,7 @@ def cmd_edit_romcollection(args): cat_repository = CategoryRepository(uow) parent_id = romcollection.get_parent_id() - category = cat_repository.find_category(romcollection.get_parent_id()) if parent_id is not None else None + category = cat_repository.find_category(romcollection.get_parent_id()) if parent_id is not None else None category_name = kodi.translate(20010) if category is None else category.get_name() options = collections.OrderedDict() @@ -120,8 +117,8 @@ def cmd_edit_romcollection(args): options['ROMCOLLECTION_EDIT_DEFAULT_ASSETS'] = kodi.translate(40859) if romcollection.has_launchers(): options['EDIT_ROMCOLLECTION_LAUNCHERS'] = kodi.translate(42016) - else: - options['ADD_LAUNCHER'] = kodi.translate(42026) + else: + options['ADD_COLLECTION_LAUNCHER'] = kodi.translate(42026) options['ROMCOLLECTION_MANAGE_ROMS'] = kodi.translate(42039) options['EDIT_ROMCOLLECTION_CATEGORY'] = kodi.translate(42040).format(category_name) options['EDIT_ROMCOLLECTION_STATUS'] = kodi.translate(42041).format(kodi.translate(romcollection.get_finished_str_code())) @@ -141,6 +138,7 @@ def cmd_edit_romcollection(args): 'romcollection_id': romcollection_id, 'category_id': romcollection.get_parent_id() }) + # --- Submenu commands --- @AppMediator.register('ROMCOLLECTION_EDIT_METADATA') def cmd_romcollection_metadata(args): @@ -180,9 +178,10 @@ def cmd_romcollection_metadata(args): # >> Execute launcher edit metadata atomic subcommand. # >> Then, execute recursively this submenu again. - logger.debug('cmd_romcollection_metadata(EDIT_METADATA) Selected {0}'.format(selected_option)) + logger.debug(f'cmd_romcollection_metadata(EDIT_METADATA) Selected {selected_option}') AppMediator.sync_cmd(selected_option, args) + @AppMediator.register('ROMCOLLECTION_EDIT_ASSETS') def cmd_romcollection_edit_assets(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None @@ -213,9 +212,10 @@ def cmd_romcollection_edit_assets(args): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + + AppMediator.sync_cmd('ROMCOLLECTION_EDIT_ASSETS', {'romcollection_id': romcollection.get_id(), 'selected_asset': asset.id}) - AppMediator.sync_cmd('ROMCOLLECTION_EDIT_ASSETS', {'romcollection_id': romcollection.get_id(), 'selected_asset': asset.id}) @AppMediator.register('ROMCOLLECTION_EDIT_DEFAULT_ASSETS') def cmd_romcollection_edit_default_assets(args): @@ -236,13 +236,16 @@ def cmd_romcollection_edit_default_assets(args): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + + AppMediator.sync_cmd('ROMCOLLECTION_EDIT_DEFAULT_ASSETS', { + 'romcollection_id': romcollection.get_id(), + 'selected_asset': selected_asset_to_edit.id}) + - AppMediator.sync_cmd('ROMCOLLECTION_EDIT_DEFAULT_ASSETS', {'romcollection_id': romcollection.get_id(), 'selected_asset': selected_asset_to_edit.id}) - @AppMediator.register('EDIT_ROMCOLLECTION_STATUS') def cmd_romcollection_status(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) @@ -253,9 +256,10 @@ def cmd_romcollection_status(args): uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) - + + # # Remove ROMCollection # @@ -270,27 +274,29 @@ def cmd_romcollection_delete(args): if romcollection.num_roms() > 0: question = kodi.translate(41069).format(romcollection_name, romcollection.num_roms()) + \ - kodi.translate(41066).format(romcollection_name) + kodi.translate(41066).format(romcollection_name) else: question = kodi.translate(41066).format(romcollection_name) ret = kodi.dialog_yesno(question) - if not ret: return + if not ret: + return logger.info('Deleting romcollection "{}" ID {}'.format(romcollection_name, romcollection.get_id())) repository.delete_romcollection(romcollection.get_id()) uow.commit() kodi.notify(kodi.translate(41018).format(romcollection_name)) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.async_cmd('CLEANUP_VIEWS') AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) + # --- Atomic commands --- # --- Edition of the launcher name --- @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_TITLE') def cmd_romcollection_metadata_title(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) @@ -300,19 +306,20 @@ def cmd_romcollection_metadata_title(args): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_PLATFORM') def cmd_romcollection_metadata_platform(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) if editors.edit_field_by_list(romcollection, kodi.translate(40807), platforms.AKL_platform_list, - romcollection.get_platform, romcollection.set_platform): + romcollection.get_platform, romcollection.set_platform): repository.update_romcollection(romcollection) update_roms_too = kodi.dialog_yesno(kodi.translate(40955)) @@ -326,12 +333,13 @@ def cmd_romcollection_metadata_platform(args): uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_RELEASEYEAR') def cmd_romcollection_metadata_releaseyear(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) @@ -344,9 +352,10 @@ def cmd_romcollection_metadata_releaseyear(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_GENRE') def cmd_romcollection_metadata_genre(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) @@ -354,11 +363,12 @@ def cmd_romcollection_metadata_genre(args): if editors.edit_field_by_str(romcollection, kodi.translate(40801), romcollection.get_genre, romcollection.set_genre): repository.update_romcollection(romcollection) - uow.commit() + uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) - + + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_DEVELOPER') def cmd_romcollection_metadata_developer(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None @@ -369,14 +379,15 @@ def cmd_romcollection_metadata_developer(args): if editors.edit_field_by_str(romcollection, kodi.translate(40802), romcollection.get_developer, romcollection.set_developer): repository.update_romcollection(romcollection) - uow.commit() + uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_RATING') def cmd_romcollection_metadata_rating(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) @@ -389,9 +400,10 @@ def cmd_romcollection_metadata_rating(args): AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_PLOT') def cmd_romcollection_metadata_plot(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) @@ -403,23 +415,25 @@ def cmd_romcollection_metadata_plot(args): AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) - + + @AppMediator.register('ROMCOLLECTION_EDIT_METADATA_BOXSIZE') def cmd_romcollection_metadata_boxsize(args): - romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) if editors.edit_field_by_list(romcollection, kodi.translate(40816), constants.BOX_SIZES, - romcollection.get_box_sizing, romcollection.set_box_sizing): + romcollection.get_box_sizing, romcollection.set_box_sizing): repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + # --- Import launcher metadata from NFO file (default location) --- @AppMediator.register('ROMCOLLECTION_IMPORT_NFO_FILE_DEFAULT') def cmd_romcollection_import_nfo_file(args): @@ -439,15 +453,18 @@ def cmd_romcollection_import_nfo_file(args): AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_IMPORT_NFO_FILE_BROWSE') -def cmd_romcollection_browse_import_nfo_file(args): +def cmd_romcollection_browse_import_nfo_file(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None NFO_file = kodi.browse(text=kodi.translate(41143), mask='.nfo') logger.debug('cmd_romcollection_browse_import_nfo_file() Dialog().browse returned "{0}"'.format(NFO_file)) - if not NFO_file: return + if not NFO_file: + return NFO_FileName = io.FileName(NFO_file) - if not NFO_FileName.exists(): return + if not NFO_FileName.exists(): + return uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -463,6 +480,7 @@ def cmd_romcollection_browse_import_nfo_file(args): AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('ROMCOLLECTION_SAVE_NFO_FILE_DEFAULT') def cmd_romcollection_save_nfo_file(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None @@ -476,7 +494,7 @@ def cmd_romcollection_save_nfo_file(args): # >> user, so display nothing to not overwrite error notification. try: romcollection.export_to_NFO_file(NFO_FileName) - except: + except Exception: kodi.notify_warn(kodi.translate(41042).format(NFO_FileName.getPath())) logger.error("cmd_romcollection_save_nfo_file() Exception writing'{0}'".format(NFO_FileName.getPath())) else: @@ -485,24 +503,26 @@ def cmd_romcollection_save_nfo_file(args): AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) + @AppMediator.register('EDIT_ROMCOLLECTION_CATEGORY') def cmd_romcollection_change_category(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMCollectionRepository(uow) - cat_repository = CategoryRepository(uow) + repository = ROMCollectionRepository(uow) + cat_repository = CategoryRepository(uow) - romcollection = repository.find_romcollection(romcollection_id) - all_categories = cat_repository.find_all_categories() - root_category = cat_repository.find_category(constants.VCATEGORY_ADDONROOT_ID) + romcollection = repository.find_romcollection(romcollection_id) + all_categories = cat_repository.find_all_categories() + root_category = cat_repository.find_category(constants.VCATEGORY_ADDONROOT_ID) previous_category_id = romcollection.get_parent_id() - if previous_category_id is None: previous_category_id = root_category.get_id() + if previous_category_id is None: + previous_category_id = root_category.get_id() options = collections.OrderedDict() options[root_category] = root_category.get_name() - options.update({category:category.get_name() for category in all_categories}) + options.update({category: category.get_name() for category in all_categories}) selected_option = kodi.OrdDictionaryDialog().select(kodi.translate(41128), options) if selected_option is None: @@ -511,22 +531,23 @@ def cmd_romcollection_change_category(args): AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) return - selected_category:Category = selected_option + selected_category: Category = selected_option if not kodi.dialog_yesno(kodi.translate(41065).format(romcollection.get_name(), selected_category.get_name())): logger.debug('cmd_romcollection_change_category(): Cancelled') AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) return - logger.debug(f'cmd_romcollection_change_category() Moving collection#{romcollection_id} to category#{selected_category.get_id()}') + logger.debug(f'cmd_romcollection_change_category() Moving collection#{romcollection_id} to category#{selected_category.get_id()}') repository.update_romcollection_parent_reference(romcollection, selected_category) - uow.commit() + uow.commit() - kodi.notify(kodi.translate(41021)) + kodi.notify(kodi.translate(41021)) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': selected_category.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': previous_category_id}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': selected_category.get_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': previous_category_id}) AppMediator.sync_cmd('EDIT_ROMCOLLECTION', args) - + + @AppMediator.register('ROMCOLLECTION_EXPORT_ROMCOLLECTION_XML') # --- Export Category XML configuration --- def cmd_romcollection_export_xml(args): @@ -544,7 +565,7 @@ def cmd_romcollection_export_xml(args): # --- Ask user for a path to export the launcher configuration --- dir_path = kodi.browse(type=0, text='Select directory to export XML') - if not dir_path: + if not dir_path: AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) return @@ -568,4 +589,4 @@ def cmd_romcollection_export_xml(args): else: kodi.notify(kodi.translate(41023).format(romcollection.get_name())) - AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) \ No newline at end of file + AppMediator.sync_cmd('ROMCOLLECTION_EDIT_METADATA', args) diff --git a/resources/lib/commands/romcollection_roms_commands.py b/resources/lib/commands/romcollection_roms_commands.py index 3f49402a..587d8255 100644 --- a/resources/lib/commands/romcollection_roms_commands.py +++ b/resources/lib/commands/romcollection_roms_commands.py @@ -19,18 +19,18 @@ import logging import collections -import typing from akl import constants -from akl.utils import kodi, io +from akl.utils import kodi from resources.lib.commands.mediator import AppMediator from resources.lib import globals -from resources.lib.repositories import UnitOfWork, ROMCollectionRepository, ROMsRepository, ROMsJsonFileRepository -from resources.lib.domain import ROM, AssetInfo, g_assetFactory +from resources.lib.repositories import UnitOfWork, ROMCollectionRepository, ROMsRepository, SourcesRepository +from resources.lib.domain import g_assetFactory, RuleSet, Rule, ROM, RuleOperator logger = logging.getLogger(__name__) + # ------------------------------------------------------------------------------------------------- # ROMCollection ROM management. # ------------------------------------------------------------------------------------------------- @@ -39,7 +39,7 @@ @AppMediator.register('ROMCOLLECTION_MANAGE_ROMS') def cmd_manage_roms(args): logger.debug('ROMCOLLECTION_MANAGE_ROMS: cmd_manage_roms() SHOW MENU') - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None if romcollection_id is None: logger.warning('cmd_manage_roms(): No romcollection id supplied.') @@ -56,19 +56,9 @@ def cmd_manage_roms(args): options = collections.OrderedDict() options['SET_ROMS_DEFAULT_ARTWORK'] = kodi.translate(42044) - options['SET_ROMS_ASSET_DIRS'] = kodi.translate(42045) - - if romcollection.has_scanners(): - options['SCAN_ROMS'] = kodi.translate(42046) - options['REMOVE_DEAD_ROMS'] = kodi.translate(42047) - options['EDIT_ROMCOLLECTION_SCANNERS'] = kodi.translate(42048) - else: options['ADD_SCANNER'] = kodi.translate(42049) - - options['IMPORT_ROMS'] = kodi.translate(42050) + options['IMPORT_ROMS'] = kodi.translate(42082) if has_roms: - options['EXPORT_ROMS'] = kodi.translate(42051) options['SCRAPE_ROMS'] = kodi.translate(42052) - options['DELETE_ROMS_NFO'] = kodi.translate(42053) options['CLEAR_ROMS'] = kodi.translate(42054) s = kodi.translate(41128).format(romcollection.get_name()) @@ -76,7 +66,8 @@ def cmd_manage_roms(args): if selected_option is None: # >> Exits context menu logger.debug('ROMCOLLECTION_MANAGE_ROMS: cmd_manage_roms() Selected None. Closing context menu') - if 'scraper_settings' in args: del args['scraper_settings'] + if 'scraper_settings' in args: + del args['scraper_settings'] AppMediator.async_cmd('EDIT_ROMCOLLECTION', args) return @@ -84,10 +75,11 @@ def cmd_manage_roms(args): logger.debug('ROMCOLLECTION_MANAGE_ROMS: cmd_manage_roms() Selected {}'.format(selected_option)) AppMediator.async_cmd(selected_option, args) + # --- Choose default ROMs assets/artwork --- @AppMediator.register('SET_ROMS_DEFAULT_ARTWORK') def cmd_set_roms_default_artwork(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: @@ -102,7 +94,7 @@ def cmd_set_roms_default_artwork(args): mapped_asset_info = romcollection.get_ROM_asset_mapping(default_asset_info) # --- Append to list of ListItems --- options[default_asset_info] = kodi.translate(42055).format( - kodi.translate(default_asset_info.name_id), + kodi.translate(default_asset_info.name_id), kodi.translate(mapped_asset_info.name_id)) dialog = kodi.OrdDictionaryDialog() @@ -114,7 +106,7 @@ def cmd_set_roms_default_artwork(args): AppMediator.async_cmd('ROMCOLLECTION_MANAGE_ROMS', args) return - logger.debug('Main select() returned {0}'.format(selected_asset_info.name)) + logger.debug(f'Main select() returned {selected_asset_info.name}') mapped_asset_info = romcollection.get_ROM_asset_mapping(selected_asset_info) mappable_asset_list = g_assetFactory.get_asset_list_by_IDs(constants.ROM_ASSET_ID_LIST, 'image') logger.debug(f'{selected_asset_info.name} currently is mapped to {mapped_asset_info.name}') @@ -126,7 +118,7 @@ def cmd_set_roms_default_artwork(args): options[mappable_asset_info] = kodi.translate(mappable_asset_info.name_id) dialog = kodi.OrdDictionaryDialog() - dialog_title_str = kodi.translate(41078).format(romcollection.get_object_name(), + dialog_title_str = kodi.translate(41078).format(romcollection.get_object_name(), kodi.translate(selected_asset_info.name_id)) new_selected_asset_info = dialog.select(dialog_title_str, options, mapped_asset_info) @@ -134,209 +126,330 @@ def cmd_set_roms_default_artwork(args): # >> Return to this method recursively to previous menu. logger.debug('Mapable selected NONE. Returning to previous menu.') AppMediator.async_cmd('ROMCOLLECTION_MANAGE_ROMS', args) - return + return logger.debug(f'Mapable selected {new_selected_asset_info.name}.') romcollection.set_mapped_ROM_asset(selected_asset_info, new_selected_asset_info) kodi.notify(kodi.translate(40983).format( - romcollection.get_object_name(), - kodi.translate(selected_asset_info.name_id), + romcollection.get_object_name(), + kodi.translate(selected_asset_info.name_id), kodi.translate(new_selected_asset_info.name_id) )) repository.update_romcollection(romcollection) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection.get_id()}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + + AppMediator.async_cmd('SET_ROMS_DEFAULT_ARTWORK', { + 'romcollection_id': romcollection.get_id(), + 'selected_asset': selected_asset_info.id}) + + +@AppMediator.register('IMPORT_ROMS') +def cmd_import_roms(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + + selected_option = None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = ROMCollectionRepository(uow) + romcollection = repository.find_romcollection(romcollection_id) + import_rules = repository.find_import_rules_by_collection(romcollection) - AppMediator.async_cmd('SET_ROMS_DEFAULT_ARTWORK', {'romcollection_id': romcollection.get_id(), 'selected_asset': selected_asset_info.id}) + options = collections.OrderedDict() + for import_rule in import_rules: + options[import_rule.get_ruleset_id()] = kodi.get_listitem( + label=kodi.translate(41174).format(import_rule.get_source_name()), + label2=f"{import_rule.get_rules_description()}", + art={'icon': 'DefaultPlaylist.png'}) -@AppMediator.register('SET_ROMS_ASSET_DIRS') -def cmd_set_rom_asset_dirs(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + options['NEW_IMPORT_RULESET'] = kodi.get_listitem(label=kodi.translate(40921), label2='', + art={'icon': 'DefaultAddSource.png'}) + + s = kodi.translate(41130).format(romcollection.get_name()) + selected_option = kodi.OrdDictionaryDialog().select(s, options, use_details=True) + if selected_option is None: + # >> Exits context menu + logger.debug('IMPORT_ROMS: Selected None. Closing context menu') + AppMediator.async_cmd('ROMCOLLECTION_MANAGE_ROMS', args) + return - list_items = collections.OrderedDict() - assets = g_assetFactory.get_assets_for_type(constants.KIND_ASSET_ROM) + if selected_option == 'NEW_IMPORT_RULESET': + AppMediator.async_cmd(selected_option, args) + return + + logger.debug(f'IMPORT_ROMS: Selected set {selected_option}') + args['ruleset_id'] = selected_option + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) + +@AppMediator.register('NEW_IMPORT_RULESET') +def cmd_new_import_ruleset(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) romcollection = repository.find_romcollection(romcollection_id) + + selected_source = _select_source_for_rules(uow) + if selected_source is None: + # >> Exits context menu + logger.debug('NEW_IMPORT_RULESET: No source selected. Closing context menu') + AppMediator.async_cmd('IMPORT_ROMS', args) + return + + logger.debug(f'NEW_IMPORT_RULESET: Selected source {selected_source.get_id()}') + + ruleset = RuleSet() + ruleset.apply_source(selected_source) + + repository.add_ruleset_to_romcollection(romcollection.get_id(), ruleset) + uow.commit() - root_path = romcollection.get_assets_root_path() - root_path_str = root_path.getPath() if root_path else kodi.translate(41158) - list_items[AssetInfo()] = kodi.translate(42083).format(root_path_str) - for asset_info in assets: - path = romcollection.get_asset_path(asset_info) - if path: - list_items[asset_info] = kodi.translate(42084).format(asset_info.plural, path.getPath()) + args['ruleset_id'] = ruleset.get_ruleset_id() + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) - dialog = kodi.OrdDictionaryDialog() - selected_asset: AssetInfo = dialog.select(kodi.translate(41129), list_items) - if selected_asset is None: - AppMediator.sync_cmd('ROMCOLLECTION_MANAGE_ROMS', args) +@AppMediator.register('EDIT_IMPORT_RULESET') +def cmd_edit_import_ruleset(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + ruleset_id: str = args['ruleset_id'] if 'ruleset_id' in args else None + + selected_option = None + next_command = None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = ROMCollectionRepository(uow) + ruleset = repository.find_ruleset(romcollection_id, ruleset_id) + + options = collections.OrderedDict() + options["EXECUTE_RULESET"] = kodi.get_listitem(kodi.translate(42089), "", + art={'icon': 'DefaultAddonsUpdates.png'}) + options["SET_RULESET_SOURCE"] = kodi.get_listitem(kodi.translate(42506), ruleset.get_source_name(), + art={'icon': 'DefaultPlaylist.png'}) + options["CHANGE_RULESET_OPERATOR"] = kodi.get_listitem(kodi.translate(41060), ruleset.get_set_operator_str(), + art={'icon': 'DefaultMimetypeInfo.png'}) + for rule in ruleset.get_rules(): + options[rule.get_id()] = kodi.get_listitem(kodi.translate(42511), rule.get_description(), + art={'icon': 'DefaultScript.png'}) + options["ADD_RULE_TO_RULESET"] = kodi.get_listitem(label=kodi.translate(42086), label2='', + art={'icon': 'DefaultAddSource.png'}) + if ruleset.has_rules(): + options["REMOVE_ALL_RULES"] = kodi.get_listitem(kodi.translate(41173), kodi.translate(41189).format( + ruleset.get_rules_shortdescription())) + + s = kodi.translate(41184) + selected_option = kodi.OrdDictionaryDialog().select(s, options, use_details=True) + if selected_option is None: + # >> Exits context menu + logger.debug('EDIT_IMPORT_RULESET: No action selected. Closing context menu') + args.pop('ruleset_id') + next_command = 'IMPORT_ROMS' return - - # rootpath? - if selected_asset.id == '': - dir_path = kodi.browse(type=0, text=kodi.translate(41159), preselected_path=root_path.getPath() if root_path else None) - if not dir_path or (root_path is not None and dir_path == root_path.getPath()): - AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) - return + + elif selected_option == 'SET_RULESET_SOURCE': + source = _select_source_for_rules(uow) + if source: + ruleset.apply_source(source) + repository.update_ruleset_in_romcollection(romcollection_id, ruleset) + uow.commit() + next_command = 'EDIT_IMPORT_RULESET' + + elif selected_option == 'CHANGE_RULESET_OPERATOR': + ruleset.change_operator() + repository.update_ruleset_in_romcollection(romcollection_id, ruleset) + uow.commit() + kodi.notify(kodi.translate(41180)) + next_command = 'EDIT_IMPORT_RULESET' + + elif selected_option == 'REMOVE_ALL_RULES': + if kodi.dialog_yesno(kodi.translate(41175)): + ruleset.clear_rules() + repository.delete_all_rules_from_ruleset(ruleset) + uow.commit() + kodi.notify(kodi.translate(41176)) + next_command = 'EDIT_IMPORT_RULESET' + + elif selected_option == 'EXECUTE_RULESET' or selected_option == 'ADD_RULE_TO_RULESET': + next_command = selected_option - root_path = io.FileName(dir_path) - apply_to_all = kodi.dialog_yesno(kodi.translate(41062)) - romcollection.set_assets_root_path(root_path, constants.ROM_ASSET_ID_LIST, create_default_subdirectories=apply_to_all) else: - selected_asset_path = romcollection.get_asset_path(selected_asset) - dir_path = kodi.browse(type=0, text=kodi.translate(41160).format(selected_asset.plural), preselected_path=selected_asset_path.getPath()) - if not dir_path or dir_path == selected_asset_path.getPath(): - AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) - return - romcollection.set_asset_path(selected_asset, dir_path) - - repository.update_romcollection(romcollection) - uow.commit() - - # >> Check for duplicate paths and warn user. - AppMediator.async_cmd('CHECK_DUPLICATE_ASSET_DIRS', args) + args['rule_id'] = selected_option + next_command = 'EDIT_RULE' + + AppMediator.sync_cmd(next_command, args) + - kodi.notify(kodi.translate(40984).format(selected_asset.name, dir_path)) - AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) +@AppMediator.register('ADD_RULE_TO_RULESET') +def cmd_add_rule_to_ruleset(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + ruleset_id: str = args['ruleset_id'] if 'ruleset_id' in args else None -@AppMediator.register('IMPORT_ROMS') -def cmd_import_roms(args): - logger.debug('IMPORT_ROMS: cmd_import_roms() SHOW MENU') - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + field_options = collections.OrderedDict() + fields = ROM.get_fields_with_translations() + for fieldkey, fieldname in fields.items(): + field_options[fieldkey] = kodi.translate(fieldname) + + operator_options = collections.OrderedDict() + operator_options[RuleOperator.Equals] = kodi.translate(30918) + operator_options[RuleOperator.NotEquals] = kodi.translate(30919) + operator_options[RuleOperator.Contains] = kodi.translate(30920) + operator_options[RuleOperator.DoesNotContain] = kodi.translate(30921) + operator_options[RuleOperator.MoreThan] = kodi.translate(30922) + operator_options[RuleOperator.LessThan] = kodi.translate(30923) + + wizard = kodi.WizardDialog_DictionarySelection(None, 'property', kodi.translate(41177), field_options) + wizard = kodi.WizardDialog_DictionarySelection(wizard, 'operator', kodi.translate(41178), operator_options) + wizard = kodi.WizardDialog_Keyboard(wizard, 'value', kodi.translate(41179)) - selected_option = None + rule = Rule() + rule.set_ruleset(ruleset_id) + + entity_data = rule.get_data_dic() + entity_data = wizard.runWizard(entity_data) + if entity_data is None: + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) + return + + rule.import_data_dic(entity_data) uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: repository = ROMCollectionRepository(uow) - romcollection = repository.find_romcollection(romcollection_id) + ruleset = repository.find_ruleset(romcollection_id, ruleset_id) + + ruleset.add_rule(rule) + repository.update_ruleset_in_romcollection(romcollection_id, ruleset) + uow.commit() - options = collections.OrderedDict() - options['IMPORT_ROMS_NFO'] = kodi.translate(42056) - options['IMPORT_ROMS_JSON'] = kodi.translate(42057) + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) + kodi.notify(kodi.translate(41180)) - s = kodi.translate(41130).format(romcollection.get_name()) - selected_option = kodi.OrdDictionaryDialog().select(s, options) - if selected_option is None: - # >> Exits context menu - logger.debug('IMPORT_ROMS: cmd_import_roms() Selected None. Closing context menu') - AppMediator.async_cmd('ROMCOLLECTION_MANAGE_ROMS', args) - return + +@AppMediator.register('EDIT_RULE') +def cmd_edit_rule(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + ruleset_id: str = args['ruleset_id'] if 'ruleset_id' in args else None + rule_id: str = args['rule_id'] if 'rule_id' in args else None + + dialog = kodi.ListDialog() + selected_action = dialog.select(kodi.translate(41182), [ + kodi.translate('42087'), + kodi.translate('42088') + ]) + + field_options = collections.OrderedDict() + fields = ROM.get_fields_with_translations() + for fieldkey, fieldname in fields.items(): + field_options[fieldkey] = kodi.translate(fieldname) - # >> Execute subcommand. May be atomic, maybe a submenu. - logger.debug('IMPORT_ROMS: cmd_import_roms() Selected {}'.format(selected_option)) - AppMediator.async_cmd(selected_option, args) + operator_options = collections.OrderedDict() + operator_options[RuleOperator.Equals] = kodi.translate(30918) + operator_options[RuleOperator.NotEquals] = kodi.translate(30919) + operator_options[RuleOperator.Contains] = kodi.translate(30920) + operator_options[RuleOperator.DoesNotContain] = kodi.translate(30921) + operator_options[RuleOperator.MoreThan] = kodi.translate(30922) + operator_options[RuleOperator.LessThan] = kodi.translate(30923) + + wizard = kodi.WizardDialog_DictionarySelection(None, 'property', kodi.translate(41177), field_options) + wizard = kodi.WizardDialog_DictionarySelection(wizard, 'operator', kodi.translate(41178), operator_options) + wizard = kodi.WizardDialog_Keyboard(wizard, 'value', kodi.translate(41179)) -# --- Import ROM metadata from NFO files --- -@AppMediator.register('IMPORT_ROMS_NFO') -def cmd_import_roms_nfo(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - - # >> Load ROMs, iterate and import NFO files uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMsRepository(uow) - collection_repository = ROMCollectionRepository(uow) + repository = ROMCollectionRepository(uow) + ruleset = repository.find_ruleset(romcollection_id, ruleset_id) + + rule = ruleset.get_rule(rule_id) + if rule is None: + kodi.notify_error(kodi.translate(41181)) + return + + if selected_action == 1: + repository.delete_rule_from_ruleset(ruleset, rule) + uow.commit() + + if selected_action == 0: + entity_data = rule.get_data_dic() + entity_data = wizard.runWizard(entity_data) + if entity_data is None: + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) + return + + rule.import_data_dic(entity_data) + repository.update_ruleset_in_romcollection(romcollection_id, ruleset) + uow.commit() - collection = collection_repository.find_romcollection(romcollection_id) - roms = repository.find_roms_by_romcollection(collection) - - pDialog = kodi.ProgressDialog() - pDialog.startProgress(kodi.translate(41153), num_steps=len(roms)) - num_read_NFO_files = 0 + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) + kodi.notify(kodi.translate(41180)) - step = 0 - for rom in roms: - step = step + 1 - nfo_filepath = rom.get_nfo_file() - pDialog.updateProgress(step) - if rom.update_with_nfo_file(nfo_filepath, verbose = False): - num_read_NFO_files += 1 - repository.update_rom(rom) - - # >> Save ROMs XML file / Launcher/timestamp saved at the end of function - pDialog.updateProgress(len(roms), kodi.translate(41154)) - uow.commit() - pDialog.close() + +def _select_source_for_rules(uow: UnitOfWork): + src_repository = SourcesRepository(uow) + sources = src_repository.find_all() - kodi.notify(kodi.translate(40985).format(num_read_NFO_files)) - AppMediator.async_cmd('IMPORT_ROMS', args) - -# --- Import ROM metadata from json config file --- -@AppMediator.register('IMPORT_ROMS_JSON') -def cmd_import_roms_json(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None - file_list = kodi.browse(text=kodi.translate(41155),mask='.json',multiple=True) + options = collections.OrderedDict() + for source in sources: + options[source] = source.get_name() + + s = kodi.translate(41172) + selected_option = kodi.OrdDictionaryDialog().select(s, options) + return selected_option + +@AppMediator.register('EXECUTE_RULESET') +def cmd_execute_ruleset(args): + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None + ruleset_id: str = args['ruleset_id'] if 'ruleset_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - repository = ROMsRepository(uow) - romcollection_repository = ROMCollectionRepository(uow) - - romcollection = romcollection_repository.find_romcollection(romcollection_id) - existing_roms = [*repository.find_roms_by_romcollection(romcollection)] - existing_rom_ids = map(lambda r: r.get_id(), existing_roms) - existing_rom_names = map(lambda r: r.get_name(), existing_roms) - - roms_to_insert:typing.List[ROM] = [] - roms_to_update:typing.List[ROM] = [] - - # >> Process file by file - for json_file in file_list: - logger.debug('cmd_import_roms_json() Importing "{0}"'.format(json_file)) - import_FN = io.FileName(json_file) - if not import_FN.exists(): continue - - json_file_repository = ROMsJsonFileRepository(import_FN) - imported_roms = json_file_repository.load_ROMs() - logger.debug("cmd_import_roms_json() Loaded {} roms".format(len(imported_roms))) + repository = ROMCollectionRepository(uow) + roms_repository = ROMsRepository(uow) + src_repository = SourcesRepository(uow) - for imported_rom in imported_roms: - if imported_rom.get_id() in existing_rom_ids: - # >> ROM exists (by id). Overwrite? - logger.debug('ROM found. Edit existing category.') - if kodi.dialog_yesno(kodi.translate(41063).format(imported_rom.get_name())): - roms_to_update.append(imported_rom) - elif imported_rom.get_name() in existing_rom_names: - # >> ROM exists (by name). Overwrite? - logger.debug('ROM found. Edit existing category.') - if kodi.dialog_yesno(kodi.translate(41063).format(imported_rom.get_name())): - roms_to_update.append(imported_rom) - else: - logger.debug('Add new ROM {}'.format(imported_rom.get_name())) - imported_rom.set_platform(romcollection.get_platform()) - roms_to_insert.append(imported_rom) - - for rom_to_insert in roms_to_insert: - repository.insert_rom(rom_to_insert) - romcollection_repository.add_rom_to_romcollection(romcollection.get_id(), rom_to_insert.get_id()) - - for rom_to_update in roms_to_update: - repository.update_rom(rom_to_update) + ruleset = repository.find_ruleset(romcollection_id, ruleset_id) + collection = repository.find_romcollection(romcollection_id) + source = src_repository.find(ruleset.get_source_id()) - uow.commit() - + roms_in_collection = roms_repository.find_roms_by_romcollection(collection) + collection_rom_ids = [rom.get_id() for rom in roms_in_collection] + roms = [*roms_repository.find_roms_by_source(source)] + counter = 0 + progress_dialog = kodi.ProgressDialog() + progress_dialog.startProgress(kodi.translate(41185), num_steps=len(roms)) + for rom in roms: + progress_dialog.incrementStep() + if rom.get_id() in collection_rom_ids: + continue + + if not ruleset.applies_to(rom): + continue + logger.debug(f"Adding ROM {rom.get_name()} to ROM Collection {collection.get_name()}") + repository.add_rom_to_romcollection(romcollection_id, rom.get_id()) + counter += 1 + + progress_dialog.endProgress() + progress_dialog.close() + + AppMediator.async_cmd('EDIT_IMPORT_RULESET', args) AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) - kodi.notify(kodi.translate(40978)) + kodi.notify(kodi.translate(41183)) + -# --- Empty Launcher ROMs --- +# --- Empty Launcher ROMs --- @AppMediator.register('CLEAR_ROMS') def cmd_clear_roms(args): - romcollection_id:str = args['romcollection_id'] if 'romcollection_id' in args else None + romcollection_id: str = args['romcollection_id'] if 'romcollection_id' in args else None uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - collection_repository = ROMCollectionRepository(uow) - roms_repository = ROMsRepository(uow) + collection_repository = ROMCollectionRepository(uow) + roms_repository = ROMsRepository(uow) romcollection = collection_repository.find_romcollection(romcollection_id) - roms = roms_repository.find_roms_by_romcollection(romcollection) + roms = roms_repository.find_roms_by_romcollection(romcollection) # If collection is empty (no ROMs) do nothing num_roms = len([*roms]) @@ -344,7 +457,7 @@ def cmd_clear_roms(args): kodi.dialog_OK(kodi.translate(41151)) return - # Confirm user wants to delete ROMs + # Confirm user wants to delete ROMs ret = kodi.dialog_yesno(kodi.translate(41142).format(romcollection.get_name(), num_roms)) if not ret: return @@ -355,12 +468,12 @@ def cmd_clear_roms(args): # Confirm if the user wants to remove the ROMs also when linked to other collections. delete_completely = kodi.dialog_yesno(kodi.translate(41064)) - if not delete_completely: + if not delete_completely: collection_repository.remove_all_roms_in_launcher(romcollection_id) else: - roms_repository.delete_roms_by_romcollection(romcollection_id) + roms_repository.delete_roms_by_romcollection(romcollection_id) uow.commit() AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': romcollection_id}) - AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) + AppMediator.async_cmd('RENDER_CATEGORY_VIEW', {'category_id': romcollection.get_parent_id()}) kodi.notify(kodi.translate(40977)) diff --git a/resources/lib/commands/source_commands.py b/resources/lib/commands/source_commands.py new file mode 100644 index 00000000..2fc57859 --- /dev/null +++ b/resources/lib/commands/source_commands.py @@ -0,0 +1,632 @@ +# -*- coding: utf-8 -*- +# +# Advanced Kodi Launcher: Commands (source roms management) +# +# Copyright (c) Chrisism +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# --- Python standard source --- +from __future__ import unicode_literals +from __future__ import division + +import logging +import collections +import typing + +import xbmcgui + +from akl import constants, platforms +from akl.utils import kodi, io + +from resources.lib.commands.mediator import AppMediator +from resources.lib import globals, editors +from resources.lib.repositories import UnitOfWork, SourcesRepository, ROMsRepository, ROMsJsonFileRepository +from resources.lib.repositories import AelAddonRepository, LaunchersRepository +from resources.lib.domain import ROM, Source, AelAddon, AssetInfo, g_assetFactory, ROMLauncherAddonFactory + +logger = logging.getLogger(__name__) + + +# --- Main menu commands --- +@AppMediator.register('ADD_SOURCE') +def cmd_add_source(args): + logger.debug('cmd_add_source() BEGIN') + + options = collections.OrderedDict() + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + addon_repository = AelAddonRepository(uow) + source_repository = SourcesRepository(uow) + + addons = addon_repository.find_all_scanner_addons() + for addon in addons: + options[addon] = addon.get_name() + options['STANDALONE_SOURCE'] = kodi.translate(40890) + + s = kodi.translate(41107) + selected_option: AelAddon = kodi.OrdDictionaryDialog().select(s, options) + + if selected_option is None: + # >> Exits context menu + logger.debug('ADD_SOURCE: cmd_add_source() Selected None. Closing context menu') + return + + if selected_option == "STANDALONE_SOURCE": + logger.debug(f'ADD_SOURCE: Selected {selected_option}') + AppMediator.sync_cmd(selected_option, args) + return + + logger.debug(f'ADD_SOURCE: Selected {selected_option.get_id()}') + source = Source(None, selected_option) + + wizard = kodi.WizardDialog_Selection(None, 'platform', kodi.translate(41099), platforms.AKL_platform_list) + wizard = kodi.WizardDialog_Dummy(wizard, 'name', '', _get_name_from_platform) + wizard = kodi.WizardDialog_Keyboard(wizard, 'name', kodi.translate(41166)) + wizard = kodi.WizardDialog_FileBrowse(wizard, 'assets_path', kodi.translate(42038), 0, '') + + source = Source(None, selected_option) + entity_data = source.get_data_dic() + entity_data = wizard.runWizard(entity_data) + if entity_data is None: + return + + source.import_data_dic(entity_data) + + # --- create assets directory --- + assets_path = entity_data['assets_path'] + assets_path_FN = io.FileName(assets_path) + source.set_assets_root_path(assets_path_FN, constants.ROM_ASSET_ID_LIST, create_default_subdirectories=True) + + # --- Determine box size based on platform -- + platform = platforms.get_AKL_platform(entity_data['platform']) + source.set_box_sizing(platform.default_box_size) + + source_repository.insert_source(source) + uow.commit() + + kodi.notify(kodi.translate(40980)) + kodi.run_script( + selected_option.get_addon_id(), + source.get_configure_command()) + + +@AppMediator.register('EDIT_SOURCE') +def cmd_edit_source(args): + logger.debug('EDIT_SOURCE: cmd_edit_source() BEGIN') + source_id: str = args['source_id'] if 'source_id' in args else None + + if source_id is None: + logger.warning('cmd_edit_source(): No source_id id supplied.') + kodi.notify_warn(kodi.translate(40951)) + return + + selected_option = None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + options = collections.OrderedDict() + options['SOURCE_EDIT_TITLE'] = kodi.translate(40863).format(source.get_name()) + options['SOURCE_EDIT_PLATFORM'] = kodi.translate(40864).format(source.get_platform()) + options['SOURCE_EDIT_BOXSIZE'] = kodi.translate(40875).format(source.get_box_sizing()) + options['SOURCE_EDIT_SCANNER'] = kodi.translate(42081) + if source.has_launchers(): + options['EDIT_SOURCE_LAUNCHERS'] = kodi.translate(42016) + else: + options['ADD_SOURCE_LAUNCHER'] = kodi.translate(42026) + options['SOURCE_MANAGE_ROMS'] = kodi.translate(42039) + options['DELETE_SOURCE'] = kodi.translate(42085) + + s = kodi.translate(41167).format(source.get_name()) + selected_option = kodi.OrdDictionaryDialog().select(s, options) + if selected_option is None: + # >> Exits context menu + logger.debug('EDIT_SOURCE: cmd_edit_source() Selected None. Closing context menu') + return + + # >> Execute subcommand. May be atomic, maybe a submenu. + logger.debug(f'EDIT_SOURCE: cmd_edit_source() Selected {selected_option}') + AppMediator.sync_cmd(selected_option, args) + + +@AppMediator.register('SOURCE_EDIT_TITLE') +def cmd_source_title(args): + source_id: str = args['source_id'] if 'source_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + s = kodi.translate(41137).format( + kodi.translate(constants.OBJ_SOURCE), + source.get_name(), + kodi.translate(40812)) + new_value = kodi.dialog_keyboard(s, source.get_name()) + if new_value is not None and source.get_name() != new_value: + source.set_name(new_value) + kodi.notify(kodi.translate(40986).format( + kodi.translate(constants.OBJ_SOURCE), + kodi.translate(40812), + new_value)) + + repository.update_source(source) + uow.commit() + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) + AppMediator.async_cmd('RENDER_SOURCES_VIEW') + else: + kodi.notify(kodi.translate(40987).format( + kodi.translate(constants.OBJ_SOURCE), + kodi.translate(40812))) + AppMediator.sync_cmd('EDIT_SOURCE', args) + + +@AppMediator.register('SOURCE_EDIT_PLATFORM') +def cmd_source_metadata_platform(args): + source_id: str = args['source_id'] if 'source_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + if editors.edit_field_by_list(source, kodi.translate(40807), platforms.AKL_platform_list, + source.get_platform, source.set_platform): + repository.update_source(source) + update_roms_too = kodi.dialog_yesno(kodi.translate(40982)) + + if update_roms_too: + roms_repository = ROMsRepository(uow) + roms_to_update = roms_repository.find_roms_by_source(source) + platform_to_apply = source.get_platform() + for rom in roms_to_update: + rom.set_platform(platform_to_apply) + roms_repository.update_rom(rom) + + uow.commit() + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) + AppMediator.async_cmd('RENDER_SOURCES_VIEW') + AppMediator.sync_cmd('EDIT_SOURCE', args) + + +@AppMediator.register('SOURCE_EDIT_BOXSIZE') +def cmd_source_boxsize(args): + source_id: str = args['source_id'] if 'source_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + if editors.edit_field_by_list(source, kodi.translate(40816), constants.BOX_SIZES, + source.get_box_sizing, source.set_box_sizing): + repository.update_source(source) + uow.commit() + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) + AppMediator.async_cmd('RENDER_SOURCES_VIEW') + AppMediator.sync_cmd('EDIT_SOURCE', args) + + +@AppMediator.register('SOURCE_EDIT_SCANNER') +def cmd_edit_source_scanner(args): + source_id: str = args['source_id'] if 'source_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + kodi.notify(kodi.translate(40980)) + kodi.run_script( + source.addon.get_addon_id(), + source.get_configure_command()) + + +@AppMediator.register('DELETE_SOURCE') +def cmd_source_delete(args): + source_id: str = args['source_id'] if 'source_id' in args else None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + source_name = source.get_name() + collection_ids = repository.find_romcollection_ids_by_source(source_id) + + if source.num_roms() > 0: + question = kodi.translate(41169).format(source_name, source.num_roms()) + \ + kodi.translate(41066).format(source_name) + else: + question = kodi.translate(41066).format(source_name) + + ret = kodi.dialog_yesno(question) + if not ret: + return + + logger.info(f'Deleting source "{source_name}" ID {source.get_id()}') + repository.delete_source(source.get_id()) + uow.commit() + AppMediator.async_cmd('RENDER_SOURCES_VIEW') + + kodi.notify(kodi.translate(41170).format(source_name)) + for collection_id in collection_ids: + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': collection_id}) + AppMediator.async_cmd('CLEANUP_VIEWS') + + +# ------------------------------------------------------------------------------------------------- +# ROM ADD +# ------------------------------------------------------------------------------------------------- +@AppMediator.register('STANDALONE_SOURCE') +def cmd_add_standalone_source(args): + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + roms_repository = ROMsRepository(uow) + addons_repository = AelAddonRepository(uow) + launchers_repository = LaunchersRepository(uow) + + launchers = launchers_repository.find_all() + + file_path = kodi.dialog_get_file(kodi.translate(40953)) + if file_path is not None: + path = io.FileName(file_path) + rom_name = path.getBaseNoExt() + + rom_name = kodi.dialog_keyboard(kodi.translate(40815), rom_name) + if rom_name is None: + return + + dialog = kodi.ListDialog() + selected_idx = dialog.select(kodi.translate(41099), platforms.AKL_platform_list) + platform = platforms.AKL_platform_list[selected_idx] + + rom_obj = ROM() + rom_obj.set_name(rom_name) + rom_obj.set_platform(platform) + if file_path: + rom_obj.set_scanned_data_element("file", file_path) + + options = collections.OrderedDict() + options["LAUNCH_DIRECTLY"] = kodi.translate(40952) + for launcher in launchers: + options[launcher] = launcher.get_name() + + s = kodi.translate(41106) + selected_option = kodi.OrdDictionaryDialog().select(s, options) + + if selected_option == "LAUNCH_DIRECTLY": + addon_id = "script.akl.defaults" + addon = addons_repository.find_by_addon_id(addon_id, constants.AddonType.LAUNCHER) + launcher = ROMLauncherAddonFactory.create(addon, None) + launcher.set_settings({ + "name": f"Standalone File Launcher: {rom_name}", + "application": file_path, + "args": "", + "secname": path.getBase() + }) + launchers_repository.insert_launcher(launcher) + selected_option = launcher + + rom_obj.add_launcher(selected_option, is_default=True) + roms_repository.insert_rom(rom_obj) + uow.commit() + + AppMediator.async_cmd('RENDER_SOURCES_VIEW') + kodi.notify(kodi.translate(41035).format(rom_name)) + kodi.refresh_container() + + +# ------------------------------------------------------------------------------------------------- +# Source ROM management. +# ------------------------------------------------------------------------------------------------- +# --- Submenu menu command --- +@AppMediator.register('SOURCE_MANAGE_ROMS') +def cmd_manage_source_roms(args): + logger.debug('SOURCE_MANAGE_ROMS: cmd_manage_source_roms() SHOW MENU') + source_id: str = args['source_id'] if 'source_id' in args else None + + if source_id is None: + logger.warning('cmd_manage_source_roms(): No source id supplied.') + kodi.notify_warn(kodi.translate(40951)) + return + + selected_option = None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + options = collections.OrderedDict() + options['SET_ROMS_ASSET_DIRS'] = kodi.translate(42045) + options['SCAN_ROMS'] = kodi.translate(42046) + options['SOURCE_IMPORT_ROMS'] = kodi.translate(42050) + options['REMOVE_DEAD_ROMS'] = kodi.translate(42047) + options['EXPORT_ROMS'] = kodi.translate(42051) + options['SCRAPE_SOURCE_ROMS'] = kodi.translate(42052) + options['DELETE_ROMS_NFO'] = kodi.translate(42053) + options['CLEAR_SOURCE_ROMS'] = kodi.translate(42080) + + s = kodi.translate(41162).format(source.get_name()) + selected_option = kodi.OrdDictionaryDialog().select(s, options) + if selected_option is None: + # >> Exits context menu + logger.debug('SOURCE_MANAGE_ROMS: cmd_manage_source_roms() Selected None. Closing context menu') + if 'scraper_settings' in args: + del args['scraper_settings'] + AppMediator.async_cmd('EDIT_SOURCE', args) + return + + logger.debug(f'SOURCE_MANAGE_ROMS: cmd_manage_source_roms() Selected {selected_option}') + AppMediator.async_cmd(selected_option, args) + + +@AppMediator.register('SET_ROMS_ASSET_DIRS') +def cmd_set_rom_asset_dirs(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + list_items = collections.OrderedDict() + assets = g_assetFactory.get_assets_for_type(constants.OBJ_ROM) + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + root_path = source.get_assets_root_path() + root_path_str = root_path.getPath() if root_path else kodi.translate(41158) + + gui_listitem = xbmcgui.ListItem(label=kodi.translate(42083), label2=root_path_str) + gui_listitem.setArt({'icon': 'DefaultFolder.png'}) + list_items[AssetInfo()] = gui_listitem + for asset_info in assets: + path = source.get_asset_path(asset_info) + if path: + gui_listitem = xbmcgui.ListItem(label=kodi.translate(42084).format(asset_info.plural), label2=path.getPath()) + gui_listitem.setArt({'icon': 'DefaultFolder.png'}) + list_items[asset_info] = gui_listitem + + dialog = kodi.OrdDictionaryDialog() + selected_asset: AssetInfo = dialog.select(kodi.translate(41129), list_items, use_details=True) + + if selected_asset is None: + AppMediator.sync_cmd('SOURCE_MANAGE_ROMS', args) + return + + # rootpath? + if selected_asset.id == '': + dir_path = kodi.browse(type=0, text=kodi.translate(41159), preselected_path=root_path.getPath() if root_path else None) + if not dir_path or (root_path is not None and dir_path == root_path.getPath()): + AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) + return + + root_path = io.FileName(dir_path) + apply_to_all = kodi.dialog_yesno(kodi.translate(41062)) + source.set_assets_root_path(root_path, constants.ROM_ASSET_ID_LIST, create_default_subdirectories=apply_to_all) + else: + selected_asset_path = source.get_asset_path(selected_asset) + dir_path = kodi.browse(type=0, text=kodi.translate(41160).format(selected_asset.plural), + preselected_path=selected_asset_path.getPath()) + if not dir_path or dir_path == selected_asset_path.getPath(): + AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) + return + source.set_asset_path(selected_asset, dir_path) + + repository.update_source(source) + uow.commit() + + # >> Check for duplicate paths and warn user. + AppMediator.async_cmd('CHECK_DUPLICATE_ASSET_DIRS', args) + + kodi.notify(kodi.translate(40984).format(selected_asset.name, dir_path)) + AppMediator.sync_cmd('SET_ROMS_ASSET_DIRS', args) + + +@AppMediator.register('REMOVE_DEAD_ROMS') +def cmd_remove_dead_roms(args): + # source_id: str = args['source_id'] if 'source_id' in args else None + kodi.notify("Not implemented yet") + + +@AppMediator.register('EXPORT_ROMS') +def cmd_export_roms(args): + # source_id: str = args['source_id'] if 'source_id' in args else None + kodi.notify("Not implemented yet") + + +@AppMediator.register('DELETE_ROMS_NFO') +def cmd_delete_rom_nfos(args): + # source_id: str = args['source_id'] if 'source_id' in args else None + kodi.notify("Not implemented yet") + + +@AppMediator.register('CLEAR_SOURCE_ROMS') +def cmd_clear_source_roms(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + source_repository = SourcesRepository(uow) + roms_repository = ROMsRepository(uow) + + source = source_repository.find(source_id) + roms = roms_repository.find_roms_by_source(source) + + # If source is empty (no ROMs) do nothing + num_roms = len([*roms]) + if num_roms == 0: + kodi.dialog_OK(kodi.translate(41163)) + return + + # Confirm user wants to delete ROMs + ret = kodi.dialog_yesno(kodi.translate(41164).format(source.get_name(), num_roms)) + if not ret: + return + + # --- If there is a No-Intro XML DAT configured remove it --- + # TODO fix + # romcollection.reset_nointro_xmldata() + + collection_ids = source_repository.find_romcollection_ids_by_source(source_id) + source_repository.remove_all_roms_in_source(source_id) + uow.commit() + + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) + for collection_id in collection_ids: + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': collection_id}) + kodi.notify(kodi.translate(41165)) + + +# ------------------------------------------------------------------------------------------------- +# Source Scanner executing +# ------------------------------------------------------------------------------------------------- +@AppMediator.register('SCAN_ROMS') +def cmd_execute_rom_scanner(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + sources_repository = SourcesRepository(uow) + source = sources_repository.find(source_id) + + logger.info(f'SCAN_ROMS: scanner for source "{source.get_name()}"') + kodi.notify(kodi.translate(40980)) + kodi.run_script( + source.addon.get_addon_id(), + source.get_scan_command()) + + +def _get_name_from_platform(input, item_key, entity_data): + title = entity_data['platform'] + return title + + +@AppMediator.register('SOURCE_IMPORT_ROMS') +def cmd_source_import_roms(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + selected_option = None + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = SourcesRepository(uow) + source = repository.find(source_id) + + options = collections.OrderedDict() + options['SOURCE_IMPORT_ROMS_NFO'] = kodi.translate(42056) + options['SOURCE_MPORT_ROMS_JSON'] = kodi.translate(42057) + + s = kodi.translate(41130).format(source.get_name()) + selected_option = kodi.OrdDictionaryDialog().select(s, options) + if selected_option is None: + # >> Exits context menu + logger.debug('SOURCE_IMPORT_ROMS: Selected None. Closing context menu') + AppMediator.async_cmd('SOURCE_MANAGE_ROMS', args) + return + + # >> Execute subcommand. May be atomic, maybe a submenu. + logger.debug(f'SOURCE_IMPORT_ROMS: Selected {selected_option}') + AppMediator.async_cmd(selected_option, args) + + +# --- Import ROM metadata from NFO files --- +@AppMediator.register('SOURCE_IMPORT_ROMS_NFO') +def cmd_import_roms_nfo(args): + source_id: str = args['source_id'] if 'source_id' in args else None + + # >> Load ROMs, iterate and import NFO files + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = ROMsRepository(uow) + src_repository = SourcesRepository(uow) + + source = src_repository.find(source_id) + roms = repository.find_roms_by_source(source) + + pDialog = kodi.ProgressDialog() + pDialog.startProgress(kodi.translate(41153), num_steps=len(roms)) + num_read_NFO_files = 0 + + step = 0 + for rom in roms: + step = step + 1 + nfo_filepath = rom.get_nfo_file() + pDialog.updateProgress(step) + if rom.update_with_nfo_file(nfo_filepath, verbose=False): + num_read_NFO_files += 1 + repository.update_rom(rom) + + # >> Save ROMs XML file / Launcher/timestamp saved at the end of function + pDialog.updateProgress(len(roms), kodi.translate(41154)) + uow.commit() + pDialog.close() + + kodi.notify(kodi.translate(40985).format(num_read_NFO_files)) + AppMediator.async_cmd('SOURCE_IMPORT_ROMS', args) + + +# --- Import ROM metadata from json config file --- +@AppMediator.register('IMPORT_ROMS_JSON') +def cmd_import_roms_json(args): + source_id: str = args['source_id'] if 'source_id' in args else None + file_list = kodi.browse(text=kodi.translate(41155), mask='.json', multiple=True) + collection_ids = [] + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + repository = ROMsRepository(uow) + src_repository = SourcesRepository(uow) + + source = src_repository.find(source_id) + collection_ids = src_repository.find_romcollection_ids_by_source(source_id) + + existing_roms = [*repository.find_roms_by_source(source)] + existing_rom_ids = map(lambda r: r.get_id(), existing_roms) + existing_rom_names = map(lambda r: r.get_name(), existing_roms) + + roms_to_insert: typing.List[ROM] = [] + roms_to_update: typing.List[ROM] = [] + + # >> Process file by file + for json_file in file_list: + logger.debug(f'Importing "{json_file}"') + import_FN = io.FileName(json_file) + if not import_FN.exists(): + continue + + json_file_repository = ROMsJsonFileRepository(import_FN) + imported_roms = json_file_repository.load_ROMs() + logger.debug(f"Loaded {len(imported_roms)} roms") + + for imported_rom in imported_roms: + if imported_rom.get_id() in existing_rom_ids: + # >> ROM exists (by id). Overwrite? + logger.debug('ROM found. Edit existing category.') + if kodi.dialog_yesno(kodi.translate(41063).format(imported_rom.get_name())): + roms_to_update.append(imported_rom) + elif imported_rom.get_name() in existing_rom_names: + # >> ROM exists (by name). Overwrite? + logger.debug('ROM found. Edit existing category.') + if kodi.dialog_yesno(kodi.translate(41063).format(imported_rom.get_name())): + roms_to_update.append(imported_rom) + else: + logger.debug(f'Add new ROM {imported_rom.get_name()}') + imported_rom.set_platform(source.get_platform()) + roms_to_insert.append(imported_rom) + + for rom_to_insert in roms_to_insert: + rom_to_insert.scanned_by(source.get_id()) + repository.insert_rom(rom_to_insert) + + for rom_to_update in roms_to_update: + rom_to_update.scanned_by(source.get_id()) + repository.update_rom(rom_to_update) + + uow.commit() + + AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) + for collection_id in collection_ids: + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': collection_id}) + kodi.notify(kodi.translate(40978)) diff --git a/resources/lib/commands/view_rendering_commands.py b/resources/lib/commands/view_rendering_commands.py index 8429a878..88cb6566 100644 --- a/resources/lib/commands/view_rendering_commands.py +++ b/resources/lib/commands/view_rendering_commands.py @@ -18,15 +18,17 @@ from __future__ import division import logging +import typing from akl import constants, settings from akl.utils import kodi from resources.lib.commands.mediator import AppMediator from resources.lib import globals -from resources.lib.repositories import UnitOfWork, CategoryRepository, ROMCollectionRepository, ROMsRepository, ViewRepository +from resources.lib.repositories import UnitOfWork, CategoryRepository, ROMCollectionRepository, ROMsRepository +from resources.lib.repositories import SourcesRepository, ViewRepository -from resources.lib.domain import ROM, ROMCollection, Category +from resources.lib.domain import ROM, ROMCollection, Category, Source from resources.lib.domain import VirtualCollectionFactory, VirtualCategoryFactory logger = logging.getLogger(__name__) @@ -42,9 +44,14 @@ def cmd_render_views_data(args): romcollections_repository = ROMCollectionRepository(uow) roms_repository = ROMsRepository(uow) views_repository = ViewRepository(globals.g_PATHS) + sources_repository = SourcesRepository(uow) - _render_root_view(categories_repository, romcollections_repository, roms_repository, views_repository, render_sub_views=True) - + _render_root_view(categories_repository, romcollections_repository, roms_repository, + sources_repository, views_repository, render_sub_views=True) + + # backwards compatibility + views_repository.cleanup_obsolete_views() + kodi.notify(kodi.translate(40969)) kodi.refresh_container() @@ -73,23 +80,24 @@ def cmd_render_view_data(args): kodi.notify(kodi.translate(40966)) kodi.refresh_container() + @AppMediator.register('RENDER_VIRTUAL_VIEWS') def cmd_render_virtual_views(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) do_notification = not settings.getSettingAsBool("display_hide_rendering_notifications") with uow: - categories_repository = CategoryRepository(uow) + categories_repository = CategoryRepository(uow) romcollections_repository = ROMCollectionRepository(uow) - roms_repository = ROMsRepository(uow) - views_repository = ViewRepository(globals.g_PATHS) + roms_repository = ROMsRepository(uow) + views_repository = ViewRepository(globals.g_PATHS) # cleanup first views_repository.cleanup_all_virtual_category_views() root_vcategory = VirtualCategoryFactory.create(constants.VCATEGORY_ROOT_ID) logger.debug('Processing root virtual category') - _render_category_view(root_vcategory, categories_repository, romcollections_repository, - roms_repository, views_repository, True) + _render_category_view(root_vcategory, categories_repository, romcollections_repository, + roms_repository, views_repository, True) for vcollection_id in constants.VCOLLECTIONS: vcollection = VirtualCollectionFactory.create(vcollection_id) @@ -108,15 +116,16 @@ def cmd_render_virtual_views(args): kodi.notify(kodi.translate(40965)) kodi.refresh_container() + @AppMediator.register('RENDER_VCATEGORY_VIEWS') -def cmd_render_vcategory(args): +def cmd_render_vcategories(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) do_notification = not settings.getSettingAsBool("display_hide_rendering_notifications") with uow: - categories_repository = CategoryRepository(uow) + categories_repository = CategoryRepository(uow) romcollections_repository = ROMCollectionRepository(uow) - roms_repository = ROMsRepository(uow) - views_repository = ViewRepository(globals.g_PATHS) + roms_repository = ROMsRepository(uow) + views_repository = ViewRepository(globals.g_PATHS) # cleanup first views_repository.cleanup_all_virtual_category_views() @@ -131,6 +140,7 @@ def cmd_render_vcategory(args): if do_notification: kodi.notify(kodi.translate(40971).format(vcategory.get_name())) kodi.refresh_container() + @AppMediator.register('RENDER_VCATEGORY_VIEW') def cmd_render_vcategory(args): @@ -161,6 +171,7 @@ def cmd_render_vcategory(args): kodi.notify(kodi.translate(40971).format(vcategory.get_name())) kodi.refresh_container() + @AppMediator.register('RENDER_ROMCOLLECTION_VIEW') def cmd_render_romcollection_view_data(args): romcollection_id = args['romcollection_id'] if 'romcollection_id' in args else None @@ -172,12 +183,57 @@ def cmd_render_romcollection_view_data(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: romcollections_repository = ROMCollectionRepository(uow) - roms_repository = ROMsRepository(uow) - views_repository = ViewRepository(globals.g_PATHS) + roms_repository = ROMsRepository(uow) + views_repository = ViewRepository(globals.g_PATHS) - romcollection = romcollections_repository.find_romcollection(romcollection_id) + romcollection = romcollections_repository.find_romcollection(romcollection_id) collection_view_data = _render_romcollection_view(romcollection, roms_repository) - views_repository.store_view(romcollection.get_id(), romcollection.get_type(), collection_view_data) + views_repository.store_view(romcollection.get_id(), romcollection.get_type(), collection_view_data) + + if do_notification: + kodi.notify(kodi.translate(40966)) + kodi.refresh_container() + + +@AppMediator.register('RENDER_SOURCES_VIEW') +def cmd_render_sources_view_data(args): + do_notification = not settings.getSettingAsBool("display_hide_rendering_notifications") + + if do_notification: + kodi.notify(kodi.translate(41161)) + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + sources_repository = SourcesRepository(uow) + roms_repository = ROMsRepository(uow) + views_repository = ViewRepository(globals.g_PATHS) + + sources = sources_repository.find_all() + sources_view_data = _render_sources_view(sources, roms_repository) + views_repository.store_sources_view(sources_view_data) + + if do_notification: + kodi.notify(kodi.translate(40966)) + kodi.refresh_container() + + +@AppMediator.register('RENDER_SOURCE_VIEW') +def cmd_render_source_view_data(args): + source_id = args['source_id'] if 'source_id' in args else None + do_notification = not settings.getSettingAsBool("display_hide_rendering_notifications") + + if do_notification: + kodi.notify(kodi.translate(41161)) + + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + with uow: + source_repository = SourcesRepository(uow) + roms_repository = ROMsRepository(uow) + views_repository = ViewRepository(globals.g_PATHS) + + source = source_repository.find(source_id) + source_view_data = _render_source_view(source, roms_repository) + views_repository.store_view(source.get_id(), source.get_type(), source_view_data) if do_notification: kodi.notify(kodi.translate(40966)) @@ -235,21 +291,26 @@ def cmd_render_rom_views(args): kodi.notify(kodi.translate(40964)) kodi.refresh_container() + @AppMediator.register('CLEANUP_VIEWS') -def cmd_cleanup_views(args): +def cmd_cleanup_views(args): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) with uow: - categories_repository = CategoryRepository(uow) - romcollections_repository = ROMCollectionRepository(uow) - views_repository = ViewRepository(globals.g_PATHS) + categories_repository = CategoryRepository(uow) + romcollections_repository = ROMCollectionRepository(uow) + sources_repository = SourcesRepository(uow) + views_repository = ViewRepository(globals.g_PATHS) categories = categories_repository.find_all_categories() romcollections = romcollections_repository.find_all_romcollections() + sources = sources_repository.find_all() category_ids = list(c.get_id() for c in categories) romcollection_ids = list(r.get_id() for r in romcollections) + source_ids = list(l.get_id() for l in sources) - views_repository.cleanup_views(category_ids + romcollection_ids) + views_repository.cleanup_views(category_ids + romcollection_ids + source_ids) + def cmd_render_virtual_collection(vcategory_id: str, collection_value: str) -> dict: uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) @@ -266,12 +327,13 @@ def cmd_render_virtual_collection(vcategory_id: str, collection_value: str) -> d # Rendering of views (containers) # ------------------------------------------------------------------------------------------------- def _render_root_view(categories_repository: CategoryRepository, romcollections_repository: ROMCollectionRepository, - roms_repository: ROMsRepository, views_repository: ViewRepository, - render_sub_views=False): + roms_repository: ROMsRepository, sources_repository: SourcesRepository, + views_repository: ViewRepository, render_sub_views=False): root_categories = categories_repository.find_root_categories() root_romcollections = romcollections_repository.find_root_romcollections() root_roms = roms_repository.find_root_roms() + sources = sources_repository.find_all() root_data = { 'id': constants.VCATEGORY_ADDONROOT_ID, @@ -298,6 +360,15 @@ def _render_root_view(categories_repository: CategoryRepository, romcollections_ collection_view_data = _render_romcollection_view(root_romcollection, roms_repository) views_repository.store_view(root_romcollection.get_id(), root_romcollection.get_type(), collection_view_data) + logger.debug('Processing sources') + sources_view_data = _render_sources_view(sources, roms_repository) + views_repository.store_sources_view(sources_view_data) + + for source in sources: + logger.debug(f'Processing source "{source.get_name()}"') + source_view_data = _render_source_view(source, roms_repository) + views_repository.store_view(source.get_id(), source.get_type(), source_view_data) + for rom in root_roms: try: root_items.append(render_rom_listitem(rom)) @@ -375,7 +446,7 @@ def _render_category_view(category_obj: Category, categories_repository: Categor logger.debug(f'Storing {len(view_items)} items for category "{category_obj.get_name()}" view.') view_data['items'] = view_items - views_repository.store_view(category_obj.get_id(), category_obj.get_type(), view_data) + views_repository.store_view(category_obj.get_id(), category_obj.get_type(), view_data) def _render_romcollection_view(romcollection_obj: ROMCollection, roms_repository: ROMsRepository) -> dict: @@ -404,6 +475,75 @@ def _render_romcollection_view(romcollection_obj: ROMCollection, roms_repository return view_data +def _render_sources_view(sources: typing.List[Source], roms_repository: ROMsRepository) -> dict: + standalone_roms = roms_repository.find_standalone_roms() + view_data = { + 'id': '', + 'name': kodi.translate(constants.OBJ_SOURCE), + 'obj_type': constants.OBJ_SOURCE, + 'items': [] + } + view_items = [] + + listitem_fanart = globals.g_PATHS.FANART_FILE_PATH.getPath() + + for source in sources: + listitem_name = source.get_name() + view_items.append({ + 'id': source.get_id(), + 'name': listitem_name, + 'url': globals.router.url_for_path(f'source/{source.get_id()}'), + 'is_folder': True, + 'type': 'video', + 'info': { + 'title': listitem_name, + 'plot': f'Source of type {source.addon.get_addon_type()}', + 'overlay': 4 + }, + 'art': { + 'fanart': listitem_fanart, + 'icon': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_icon.png').getPath(), + 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_poster.png').getPath() + }, + 'properties': { + 'obj_type': constants.OBJ_SOURCE + } + }) + + for rom in standalone_roms: + try: + view_items.append(render_rom_listitem(rom)) + except Exception: + logger.exception(f"Exception while rendering list item ROM '{rom.get_name()}'") + + logger.debug(f'Storing {len(view_items)} items for Sources view.') + view_data['items'] = view_items + return view_data + + +def _render_source_view(source: Source, roms_repository: ROMsRepository) -> dict: + roms = roms_repository.find_roms_by_source(source) + view_data = { + 'id': source.get_id(), + 'name': source.get_name(), + 'properties': { + 'boxsize': source.get_box_sizing() + }, + 'obj_type': source.get_type(), + 'items': [] + } + view_items = [] + for rom in roms: + try: + view_items.append(render_rom_listitem(rom)) + except Exception: + logger.exception(f'Exception while rendering list item ROM "{rom.get_name()}"') + + logger.debug(f'Found {len(view_items)} items for source "{source.get_name()}" view.') + view_data['items'] = view_items + return view_data + + # ------------------------------------------------------------------------------------------------- # Rendering of list items per view # ------------------------------------------------------------------------------------------------- @@ -480,7 +620,7 @@ def _render_romcollection_listitem(romcollection_obj: ROMCollection) -> dict: 'overlay': ICON_OVERLAY }, 'art': assets, - 'properties': { + 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_ROMCOLLECTION, 'platform': romcollection_obj.get_platform(), 'boxsize': romcollection_obj.get_box_sizing(), diff --git a/resources/lib/domain.py b/resources/lib/domain.py index a3e38ef4..9fa0b74f 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -26,6 +26,7 @@ import time import datetime import json +from enum import IntEnum # --- AKL packages --- from resources.lib import globals @@ -45,11 +46,13 @@ def _is_a_number(input: any): def _is_empty(input: any) -> bool: return input is None or (not _is_a_number(input) and len(input) == 0) + def _is_empty_or_default(input: any, default: any): if _is_empty(input): return True return input == default + # ------------------------------------------------------------------------------------------------- # Gets all required information about an asset: path, name, etc. # Returns an object with all the information @@ -68,7 +71,8 @@ class AssetInfo(object): path_key = '' def get_description(self): - if self.description == '': return self.name + if self.description == '': + return self.name return self.description @@ -89,7 +93,7 @@ class EntityABC(object): def __init__(self, entity_data: typing.Dict[str, typing.Any]): self.entity_data = entity_data - if not "extra" in self.entity_data or not self.entity_data["extra"]: + if "extra" not in self.entity_data or not self.entity_data["extra"]: self.entity_data["extra"] = {} elif isinstance(self.entity_data["extra"], str): self.entity_data["extra"] = json.loads(self.entity_data["extra"]) @@ -101,6 +105,14 @@ def set_id(self, id: str): def get_id(self) -> str: return self.entity_data['id'] if 'id' in self.entity_data else None + @abc.abstractmethod + def get_type(self) -> str: + return constants.OBJ_NONE + + @abc.abstractmethod + def get_object_name(self) -> str: + return "Entity" + def get_data_dic(self): return self.entity_data @@ -110,11 +122,12 @@ def copy_of_data_dic(self): def set_custom_attribute(self, key, value): self.entity_data[key] = value - def get_custom_attribute(self, key, default_value = None): + def get_custom_attribute(self, key, default_value=None): return self.entity_data[key] if key in self.entity_data else default_value def import_data_dic(self, data): - if data is None: return + if data is None: + return for key in data: self.entity_data[key] = data[key] @@ -125,28 +138,35 @@ def dump_data_dic_to_log(self): # helper method to convert a dictionary value to a FileName object def _get_filename_from_field(self, field) -> io.FileName: - if not field in self.entity_data: return None + if field not in self.entity_data: + return None return self._to_filename(self.entity_data[field]) def _get_directory_filename_from_field(self, field) -> io.FileName: - if not field in self.entity_data: return None + if field not in self.entity_data: + return None return self._to_filename(self.entity_data[field], isdir=True) # helper method to convert a value to filename - def _to_filename(self, value, isdir = False) -> io.FileName: - if not value or value == '': return None + def _to_filename(self, value, isdir=False) -> io.FileName: + if not value or value == '': + return None return io.FileName(value, isdir) + # Addons that can be used as AKL plugin (launchers, scrapers) class AelAddon(EntityABC): - def __init__(self, addon_dic=None): + def __init__(self, addon_dic=None): if addon_dic is None: addon_dic = {} - if 'associated_addon_id' in addon_dic: + if 'associated_addon_id' in addon_dic: addon_dic['id'] = addon_dic['associated_addon_id'] + if 'addon_name' in addon_dic: + addon_dic['name'] = addon_dic['addon_name'] + if 'id' not in addon_dic: addon_dic['id'] = text.misc_generate_random_SID() @@ -182,10 +202,11 @@ def get_extra_settings(self) -> dict: def set_extra_settings(self, settings: dict): self.entity_data['extra_settings'] = json.dumps(settings) + class Asset(EntityABC): def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): - self.asset_info:AssetInfo = None + self.asset_info: AssetInfo = None if entity_data is None: entity_data = _get_default_asset_data_model() @@ -209,7 +230,7 @@ def get_path_FN(self) -> io.FileName: def set_path(self, path_str): self.entity_data['filepath'] = path_str - def set_asset_info(self, info:AssetInfo): + def set_asset_info(self, info: AssetInfo): self.asset_info = info def is_assigned(self) -> bool: @@ -225,15 +246,16 @@ def create(asset_info_id): asset.set_asset_info(asset_info) return asset + class AssetPath(EntityABC): def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): - self.asset_info:AssetInfo = None + self.asset_info: AssetInfo = None if entity_data is None: - entity_data = { - 'id' : '', - 'path' : '', - 'asset_type' : '' + entity_data = { + 'id': '', + 'path': '', + 'asset_type': '' } if 'asset_type' in entity_data and entity_data['asset_type']: @@ -242,7 +264,7 @@ def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): super(AssetPath, self).__init__(entity_data) def get_asset_info_id(self) -> str: - return self.asset_info.id + return self.asset_info.id def get_asset_info(self) -> AssetInfo: return self.asset_info @@ -256,7 +278,7 @@ def get_path_FN(self) -> io.FileName: def set_path(self, path_str): self.entity_data['path'] = path_str - def set_asset_info(self, info:AssetInfo): + def set_asset_info(self, info: AssetInfo): self.asset_info = info def clear(self): @@ -266,14 +288,14 @@ def clear(self): class AssetMapping(EntityABC): def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): - self.asset_info:AssetInfo = None - self.to_asset_info:AssetInfo = None + self.asset_info: AssetInfo = None + self.to_asset_info: AssetInfo = None if entity_data is None: entity_data = { - 'id' : '', - 'mapped_asset_type' : '', - 'to_asset_type' : '' + 'id': '', + 'mapped_asset_type': '', + 'to_asset_type': '' } if 'mapped_asset_type' in entity_data and entity_data['mapped_asset_type']: @@ -284,7 +306,7 @@ def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): super(AssetMapping, self).__init__(entity_data) def get_asset_info_id(self) -> str: - return self.asset_info.id + return self.asset_info.id def get_asset_info(self) -> AssetInfo: return self.asset_info @@ -292,7 +314,7 @@ def get_asset_info(self) -> AssetInfo: def get_mapped_to_asset_info(self) -> str: return self.to_asset_info - def set_mapping(self, info:AssetInfo, to:AssetInfo): + def set_mapping(self, info: AssetInfo, to: AssetInfo): self.asset_info = info self.to_asset_info = to @@ -313,13 +335,10 @@ def is_mapped(self): if self.to_asset_info is None: return False - if self.asset_info.id == constants.ASSET_ICON_ID or \ - self.asset_info.id == constants.ASSET_POSTER_ID: - if self.asset_info.id == constants.ASSET_ICON_ID and \ - self.to_asset_info.id == constants.ASSET_BOXFRONT_ID: + if self.asset_info.id == constants.ASSET_ICON_ID or self.asset_info.id == constants.ASSET_POSTER_ID: + if self.asset_info.id == constants.ASSET_ICON_ID and self.to_asset_info.id == constants.ASSET_BOXFRONT_ID: return False - if self.asset_info.id == constants.ASSET_POSTER_ID and \ - self.to_asset_info.id == constants.ASSET_FLYER_ID: + if self.asset_info.id == constants.ASSET_POSTER_ID and self.to_asset_info.id == constants.ASSET_FLYER_ID: return False return True @@ -332,7 +351,7 @@ class ROMAddon(EntityABC): __metaclass__ = abc.ABCMeta def __init__(self, addon: AelAddon, entity_data: dict): - self.addon = addon + self.addon = addon super(ROMAddon, self).__init__(entity_data) def get_name(self): @@ -341,9 +360,12 @@ def get_name(self): return '{} ({})'.format(self.addon.get_name(), secondary_name) return self.addon.get_name() + def get_addon_name(self): + return self.addon.get_name() + def get_secondary_name(self): settings = self.get_settings() - return settings['secname'] if 'secname' in settings else None + return settings['secname'] if 'secname' in settings else None def get_settings_str(self) -> str: return self.entity_data['settings'] if 'settings' in self.entity_data else None @@ -354,18 +376,44 @@ def get_settings(self) -> dict: return {} return json.loads(settings) - def set_settings_str(self, addon_settings:str): + def get_setting(self, setting_key: str, default_value=None): + settings = self.get_settings() + return settings[setting_key] if setting_key in settings else default_value + + def set_settings_str(self, addon_settings: str): self.entity_data['settings'] = addon_settings + new_name = self.get_setting('name') + if new_name: + self.entity_data['name'] = new_name - def set_settings(self, addon_settings:dict): + def set_settings(self, addon_settings: dict): self.entity_data['settings'] = json.dumps(addon_settings) + new_name = self.get_setting('name') + if new_name: + self.entity_data['name'] = new_name def get_addon(self) -> AelAddon: return self.addon - + + class ROMLauncherAddon(ROMAddon): __metaclass__ = abc.ABCMeta - + + def __init__(self, + entity_data: dict = None, + addon: AelAddon = None): + + if entity_data is None: + entity_data = { + 'id': text.misc_generate_random_SID(), + 'name': '', + 'is_default': False + } + super(ROMLauncherAddon, self).__init__(addon, entity_data) + + def get_name(self): + return self.entity_data["name"] + def is_default(self) -> bool: return self.entity_data['is_default'] if 'is_default' in self.entity_data else False @@ -382,40 +430,26 @@ def get_launch_command(self, rom: ROM) -> dict: '--rom_id': rom.get_id() } - def get_configure_command(self, romcollection: ROMCollection) -> dict: - return { - '--cmd': 'configure', - '--type': constants.AddonType.LAUNCHER.name, - '--server_host': globals.WEBSERVER_HOST, - '--server_port': settings.getSettingAsInt('webserver_port'), - '--romcollection_id': romcollection.get_id(), - '--akl_addon_id': self.get_id() - } - - def get_configure_command_for_rom(self, rom: ROM) -> dict: + def get_configure_command(self, args: dict) -> dict: return { '--cmd': 'configure', '--type': constants.AddonType.LAUNCHER.name, '--server_host': globals.WEBSERVER_HOST, '--server_port': settings.getSettingAsInt('webserver_port'), - '--rom_id': rom.get_id(), - '--akl_addon_id': self.get_id() + '--akl_addon_id': self.get_id(), + '--entity_type': args['entity_type'] if 'entity_type' in args else '', + '--entity_id': args['entity_id'] if 'entity_id' in args else '' } - + def launch(self, rom: ROM): kodi.run_script( - self.addon.get_addon_id(), + self.addon.get_addon_id(), self.get_launch_command(rom)) - def configure(self, romcollection: ROMCollection): - kodi.run_script( - self.addon.get_addon_id(), - self.get_configure_command(romcollection)) - - def configure_for_rom(self, rom: ROM): + def configure(self, args: dict): kodi.run_script( - self.addon.get_addon_id(), - self.get_configure_command_for_rom(rom)) + self.addon.get_addon_id(), + self.get_configure_command(args)) class RetroplayerLauncherAddon(ROMLauncherAddon): @@ -435,9 +469,9 @@ def launch(self, rom: ROM): # >> How to fill gameclient = string (game.libretro.fceumm) ??? game_info = { - 'title' : rom.get_name(), + 'title': rom.get_name(), 'platform': rom.get_platform(), - 'genres' : [rom.get_genre()], + 'genres': [rom.get_genre()], 'developer': rom.get_developer(), 'overview': rom.get_plot(), 'year': rom.get_releaseyear() @@ -471,37 +505,190 @@ def configure_for_rom(self, rom: ROM): if not is_stored: kodi.notify_error(kodi.translate(40958)) -class ROMCollectionScanner(ROMAddon): + +class Source(ROMAddon): - def get_last_scan_timestamp(self): + def __init__(self, + entity_data: dict = None, + addon: AelAddon = None, + asset_paths_data: typing.List[AssetPath] = [], + launchers_data: typing.List[ROMLauncherAddon] = []): + + self.asset_paths: typing.Dict[str, AssetPath] = {} + self.launchers_data = launchers_data + if asset_paths_data is not None: + for path in asset_paths_data: + self.asset_paths[path.get_asset_info_id()] = path + + if entity_data is None: + entity_data = { + 'id': text.misc_generate_random_SID(), + 'name': '', + 'platform': '', + 'box_size': '', + 'assets_path': '', + 'num_roms': 0, + 'last_scan_timestamp': None, + 'settings': '{}' + } + super(Source, self).__init__(addon, entity_data) + + def get_name(self): + return self.entity_data["name"] + + def set_name(self, name): + self.entity_data["name"] = name + + def get_type(self): + return constants.OBJ_SOURCE # 42506 + + def get_platform(self): + return self.entity_data['platform'] if 'platform' in self.entity_data else None + + def set_platform(self, platform): + self.entity_data['platform'] = platform + + def get_box_sizing(self): + return self.entity_data['box_size'] if 'box_size' in self.entity_data else constants.BOX_SIZE_POSTER + + def set_box_sizing(self, box_size): + self.entity_data['box_size'] = box_size + + def num_roms(self) -> int: + return self.entity_data['num_roms'] if 'num_roms' in self.entity_data else 0 + + def has_roms(self) -> bool: + return self.num_roms() > 0 + + def get_assets_root_path(self) -> io.FileName: + return self._get_directory_filename_from_field('assets_path') + + def get_asset_path(self, asset_info: AssetInfo, fallback_to_root=True) -> io.FileName: + if not asset_info: + return None + if asset_info.id in self.asset_paths: + return self.asset_paths[asset_info.id].get_path_FN() + + if fallback_to_root and self.get_assets_root_path() is not None: + return self.get_assets_root_path().pjoin(asset_info.plural.lower(), isdir=True) return None + + def get_asset_paths(self) -> typing.List[AssetPath]: + return list(self.asset_paths.values()) + + def set_asset_path(self, asset_info: AssetInfo, path: str): + logger.debug(f'Setting "{asset_info.id}" to {path}') + asset_path = self.asset_paths[asset_info.id] if asset_info.id in self.asset_paths else AssetPath() + asset_path.set_path(path) + asset_path.set_asset_info(asset_info) + + self.asset_paths[asset_info.id] = asset_path + + def set_assets_root_path(self, path: io.FileName, asset_ids=[], create_default_subdirectories=False): + path_str = path.getPath() if path else '' + self.entity_data['assets_path'] = path_str + + if create_default_subdirectories: + asset_ids = constants.ROM_ASSET_ID_LIST if not asset_ids else asset_ids + for asset_info_id in asset_ids: + asset_info = g_assetFactory.get_asset_info(asset_info_id) + new_path = path.pjoin(asset_info.plural.lower(), isdir=True) + self.set_asset_path(asset_info, new_path.getPath()) + if not new_path.exists(): + new_path.makedirs() + + # + # Get a list of assets with duplicated paths. Refuse to do anything if duplicated paths found. + # + def get_duplicated_asset_dirs(self): + duplicated_bool_list = [False] * len(constants.ROM_ASSET_ID_LIST) + duplicated_name_list = [] + + # >> Check for duplicated asset paths + for i, asset_i in enumerate(constants.ROM_ASSET_ID_LIST[:-1]): + A_i = g_assetFactory.get_asset_info(asset_i) + for j, asset_j in enumerate(constants.ROM_ASSET_ID_LIST[i + 1:]): + A_j = g_assetFactory.get_asset_info(asset_j) + # >> Exclude unconfigured assets (empty strings). + if A_i.path_key not in self.entity_data or A_j.path_key not in self.entity_data \ + or not self.entity_data[A_i.path_key] or not self.entity_data[A_j.path_key]: + continue + + # logger.debug('asset_get_duplicated_asset_list() Checking {0:<9} vs {1:<9}'.format(A_i.name, A_j.name)) + if self.entity_data[A_i.path_key] == self.entity_data[A_j.path_key]: + duplicated_bool_list[i] = True + duplicated_name_list.append('{0} and {1}'.format(A_i.name, A_j.name)) + logger.info('asset_get_duplicated_asset_list() DUPLICATED {0} and {1}'.format(A_i.name, A_j.name)) + + return duplicated_name_list + + def has_launchers(self) -> bool: + return len(self.launchers_data) > 0 + + def add_launcher(self, launcher: ROMLauncherAddon, is_default: bool = False): + if is_default: + current_default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) + if current_default_launcher: + current_default_launcher.set_default(False) + + self.launchers_data.append(launcher) + logger.debug(f'Adding launcher "{launcher.get_id()}" to Source "{self.get_name()}"') + + def get_launchers(self) -> typing.List[ROMLauncherAddon]: + return self.launchers_data + + def get_launcher(self, id: str) -> ROMLauncherAddon: + return next((ld for ld in self.launchers_data if ld.get_id() == id), None) + + def get_default_launcher(self) -> ROMLauncherAddon: + if len(self.launchers_data) == 0: + return None + default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) + if default_launcher is None: + return self.launchers_data[0] + + return default_launcher + + def set_launcher_as_default(self, launcher_id): + if len(self.launchers_data) == 0: + return + + current_default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) + if current_default_launcher: + current_default_launcher.set_default(False) + + launcher_to_be_default = next((ld for ld in self.launchers_data if ld.get_id() == launcher_id), None) + if launcher_to_be_default: + launcher_to_be_default.set_default(True) + + def get_last_scan_timestamp(self): + return self.entity_data["last_scan_timestamp"] - def get_scan_command(self, rom_collection: ROMCollection) -> dict: + def get_scan_command(self) -> dict: return { '--cmd': 'scan', '--type': constants.AddonType.SCANNER.name, '--server_host': globals.WEBSERVER_HOST, '--server_port': settings.getSettingAsInt('webserver_port'), - '--romcollection_id': rom_collection.get_id(), - '--akl_addon_id': self.get_id() + '--source_id': self.get_id() } - def get_configure_command(self, romcollection: ROMCollection) -> dict: + def get_configure_command(self) -> dict: return { '--cmd': 'configure', '--type': constants.AddonType.SCANNER.name, '--server_host': globals.WEBSERVER_HOST, '--server_port': settings.getSettingAsInt('webserver_port'), - '--romcollection_id': romcollection.get_id(), - '--akl_addon_id': self.get_id() + '--source_id': self.get_id() } + class ScraperAddon(ROMAddon): - def __init__(self, addon: AelAddon, scraper_settings: ScraperSettings): + def __init__(self, addon: AelAddon, scraper_settings: ScraperSettings): entity_data = { 'settings': json.dumps(scraper_settings.get_data_dic()) - } + } super(ScraperAddon, self).__init__(addon, entity_data) def settings_are_applicable(self) -> bool: @@ -550,56 +737,219 @@ def get_scraper_settings(self) -> ScraperSettings: def set_scraper_settings(self, settings: ScraperSettings): self.entity_data['settings'] = json.dumps(settings.get_data_dic()) - - def get_scrape_command(self, rom: ROM)-> dict: - return { - '--cmd': 'scrape', - '--type': constants.AddonType.SCRAPER.name, - '--server_host': globals.WEBSERVER_HOST, - '--server_port': settings.getSettingAsInt('webserver_port'), - '--akl_addon_id': self.addon.get_id(), - '--rom_id': rom.get_id(), - '--settings': io.parse_to_json_arg(self.get_settings()) - } - - def get_scrape_command_for_collection(self, collection: ROMCollection) -> dict: + + def get_scrape_command(self, entity: EntityABC) -> dict: return { '--cmd': 'scrape', '--type': constants.AddonType.SCRAPER.name, '--server_host': globals.WEBSERVER_HOST, '--server_port': settings.getSettingAsInt('webserver_port'), + '--entity_id': entity.get_id(), + '--entity_type': entity.get_type(), '--akl_addon_id': self.addon.get_id(), - '--romcollection_id': collection.get_id(), '--settings': io.parse_to_json_arg(self.get_settings()) } + +class RuleSetOperator(IntEnum): + AND = 1 + OR = 2 + + +class RuleOperator(IntEnum): + Equals = 1 + NotEquals = 2 + Contains = 3 + DoesNotContain = 4 + MoreThan = 5 + LessThan = 6 + + +class Rule(EntityABC): + + def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): + + if entity_data is None: + entity_data = { + 'rule_id': '', + 'ruleset_id': '', + 'property': '', + 'value': '', + 'operator': 1 + } + + super(Rule, self).__init__(entity_data) + + def get_id(self): + return self.entity_data['rule_id'] if 'rule_id' in self.entity_data else None + + def set_id(self, id: str): + self.entity_data['rule_id'] = id + + def get_operator(self): + return RuleOperator(self.entity_data['operator']) if 'operator' in self.entity_data else RuleOperator.Equals + + def get_operator_str(self): + opr = self.get_operator() + if opr == RuleOperator.Equals: + return kodi.translate(30918) + if opr == RuleOperator.NotEquals: + return kodi.translate(30919) + if opr == RuleOperator.Contains: + return kodi.translate(30920) + if opr == RuleOperator.DoesNotContain: + return kodi.translate(30921) + if opr == RuleOperator.MoreThan: + return kodi.translate(30922) + if opr == RuleOperator.LessThan: + return kodi.translate(30923) + return kodi.translate(30918) + + def get_property(self): + return self.entity_data['property'] if 'property' in self.entity_data else '' + + def get_value(self): + return self.entity_data['value'] if 'value' in self.entity_data else '' + + def get_description(self): + fields = ROM.get_fields_with_translations() + property = self.get_property() + if property: + property = kodi.translate(fields[self.get_property()]) + return f"{property} {self.get_operator_str()} {self.get_value()}" + + def set_ruleset(self, ruleset_id): + self.entity_data['ruleset_id'] = ruleset_id + + def applies_to(self, rom: ROM): + operator = self.get_operator() + entity_property = self.get_property() + property_value = self.get_value() + + actual = rom.get_custom_attribute(entity_property) + if operator == RuleOperator.Equals: + return actual == property_value + if operator == RuleOperator.NotEquals: + return actual != property_value + if operator == RuleOperator.Contains: + return property_value in actual + if operator == RuleOperator.DoesNotContain: + return property_value not in actual + if operator == RuleOperator.MoreThan: + return property_value > actual + if operator == RuleOperator.LessThan: + return property_value < actual + return False + + +class RuleSet(object): + + def __init__(self, entity_data: typing.Dict[str, typing.Any] = None): + + if entity_data is None: + entity_data = { + 'ruleset_id': text.misc_generate_random_SID(), + 'source_id': '', + 'source_name': '', + 'collection_id': '', + 'set_operator': None, + 'rules': [] + } + + self.entity_data = entity_data + self.rules = [] + + if 'rules' in self.entity_data: + for rule_data in self.entity_data['rules']: + if rule_data['rule_id']: + self.rules.append(Rule(rule_data)) + + def get_ruleset_id(self): + return self.entity_data['ruleset_id'] if 'ruleset_id' in self.entity_data else None + + def get_source_id(self): + return self.entity_data['source_id'] if 'source_id' in self.entity_data else None + + def get_source_name(self): + return self.entity_data['source_name'] if 'source_name' in self.entity_data else "Unknown" + + def get_rules_description(self): + if len(self.rules) == 0: + return kodi.translate(42508) # All + + return f"{len(self.rules)} {kodi.translate(42510)} [{self.get_set_operator_str()}]" + + def get_rules_shortdescription(self): + if len(self.rules) == 0: + return kodi.translate(42508) # All + return kodi.translate(42510) # Rules + + def get_set_operator(self): + operator = self.entity_data['set_operator'] if 'set_operator' in self.entity_data else RuleSetOperator.OR + return operator if operator else RuleSetOperator.OR + + def get_set_operator_str(self): + set_operator = self.get_set_operator() + return kodi.translate(30916) if set_operator == RuleSetOperator.AND else kodi.translate(30917) + + def get_rules(self) -> typing.List[Rule]: + return self.rules + + def get_rule(self, rule_id: str) -> Rule: + return next((rule for rule in self.rules if rule.get_id() == rule_id), None) + + def add_rule(self, rule: Rule): + self.rules.append(rule) + + def apply_source(self, source: Source): + self.entity_data['source_id'] = source.get_id() + self.entity_data['source_name'] = source.get_name() + + def change_operator(self): + current = self.get_set_operator() + if current == RuleSetOperator.OR: + self.entity_data['set_operator'] = RuleSetOperator.AND + else: + self.entity_data['set_operator'] = RuleSetOperator.OR + + def clear_rules(self): + self.rules.clear() + + def has_rules(self): + return len(self.rules) > 0 + + def applies_to(self, rom: ROM): + # no rules, then all applied + if len(self.rules) == 0: + return True + + set_operator = self.get_set_operator() + if not set_operator: + set_operator = RuleSetOperator.OR + + for rule in self.rules: + if rule.applies_to(rom): + if set_operator == RuleSetOperator.OR: + return True + else: + if set_operator == RuleSetOperator.AND: + return False + + return set_operator == RuleSetOperator.AND + + # ------------------------------------------------------------------------------------------------- # Abstract base class for business objects which support the generic # metadata fields and assets. # -# --- Class hierarchy --- -# -# |-MetaDataItemABC(object) (abstract class) -# | -# |----- Category -# | | -# | |----- VirtualCategory -# | -# |----- ROMCollection (Collection) -# | | -# | |----- VirtualCollection -# | -# |----- ROM -# | -# class MetaDataItemABC(EntityABC): __metaclass__ = abc.ABCMeta - def __init__(self, - entity_data: typing.Dict[str, typing.Any], + def __init__(self, + entity_data: typing.Dict[str, typing.Any], assets: typing.List[Asset], asset_paths_data: typing.List[AssetPath] = None, - asset_mappings: typing.List[AssetMapping] = []): + asset_mappings: typing.List[AssetMapping] = []): self.assets: typing.Dict[str, Asset] = {} if assets is not None: for asset in assets: @@ -616,18 +966,7 @@ def __init__(self, # -------------------------------------------------------------------------------------------- # Core functions # -------------------------------------------------------------------------------------------- - @abc.abstractmethod - def get_object_name(self) -> str: - pass - - @abc.abstractmethod - def get_assets_kind(self) -> int: - pass - - @abc.abstractmethod - def get_type(self) -> str: - pass - + # --- Metadata -------------------------------------------------------------------------------- def get_metadata_id(self): return self.entity_data['metadata_id'] @@ -663,7 +1002,7 @@ def get_rating(self): def set_rating(self, rating): try: self.entity_data['m_rating'] = int(rating) - except: + except Exception: self.entity_data['m_rating'] = '' def get_plot(self): @@ -697,7 +1036,7 @@ def set_trailer(self, trailer_str): video_id = matches.groups()[-1] trailer_str = 'plugin://plugin.video.youtube/play/?video_id={}'.format(video_id) - trailer_asset = self.get_asset(constants.ASSET_TRAILER_ID) + trailer_asset = self.get_asset(constants.ASSET_TRAILER_ID) if trailer_asset is None: self.assets[constants.ASSET_TRAILER_ID] = Asset.create(constants.ASSET_TRAILER_ID) @@ -709,7 +1048,7 @@ def is_finished(self): def get_finished_str_code(self): finished = self.is_finished() - finished_display = 42014 if finished == True else 42015 + finished_display = 42014 if finished is True else 42015 return finished_display @@ -720,8 +1059,9 @@ def change_finished_status(self): # --- Assets/artwork -------------------------------------------------------------------------- def has_asset(self, asset_info: AssetInfo) -> bool: - if not asset_info.id in self.assets: return False - return self.assets[asset_info.id] != None and self.assets[asset_info.id].get_path() != '' + if asset_info.id not in self.assets: + return False + return self.assets[asset_info.id] is not None and self.assets[asset_info.id].get_path() != '' def get_asset(self, asset_id: str) -> Asset: return self.assets[asset_id] if asset_id in self.assets else None @@ -746,10 +1086,10 @@ def get_available_assets(self) -> typing.List[Asset]: return available_assets - # + # # Gets the asset path (str) of the given assetinfo type. # - def get_asset_str(self, asset_info=None, asset_id=None, fallback = '') -> str: + def get_asset_str(self, asset_info=None, asset_id=None, fallback='') -> str: if asset_info is None and asset_id is None: return None if asset_info is not None: @@ -758,7 +1098,8 @@ def get_asset_str(self, asset_info=None, asset_id=None, fallback = '') -> str: asset = self.get_asset(asset_id) if asset is not None: path = asset.get_path() - if path != '': return path + if path != '': + return path return fallback @@ -776,7 +1117,8 @@ def set_asset(self, asset_info: AssetInfo, path_FN: io.FileName): path = path_FN.getPath() if path_FN else '' asset = self.get_asset(asset_info.id) - if asset is None: self.assets[asset_info.id] = Asset.create(asset_info.id) + if asset is None: + self.assets[asset_info.id] = Asset.create(asset_info.id) self.assets[asset_info.id].set_path(path) @@ -785,34 +1127,13 @@ def clear_asset(self, asset_info: AssetInfo): if asset is None: self.assets[asset_info.id] = Asset.create(asset_info.id) asset.clear() - - def get_assets_root_path(self) -> io.FileName: - return self._get_directory_filename_from_field('assets_path') - - def set_assets_root_path(self, path: io.FileName, asset_ids = [], create_default_subdirectories = False): - path_str = path.getPath() if path else '' - self.entity_data['assets_path'] = path_str - - if create_default_subdirectories: - asset_ids = self.get_asset_ids_list() if not asset_ids else asset_ids - for asset_info_id in asset_ids: - asset_info = g_assetFactory.get_asset_info(asset_info_id) - new_path = path.pjoin(asset_info.plural.lower(), isdir=True) - self.set_asset_path(asset_info, new_path.getPath()) - if not new_path.exists(): new_path.makedirs() - - + def get_asset_paths(self) -> typing.List[AssetPath]: return list(self.asset_paths.values()) - def get_asset_path(self, asset_info: AssetInfo, fallback_to_root = True) -> io.FileName: - if not asset_info: - return None - if asset_info.id in self.asset_paths: + def get_asset_path(self, asset_info: AssetInfo) -> io.FileName: + if asset_info and asset_info.id in self.asset_paths: return self.asset_paths[asset_info.id].get_path_FN() - - if fallback_to_root and self.get_assets_root_path() is not None: - return self.get_assets_root_path().pjoin(asset_info.plural.lower(), isdir=True) return None def set_asset_path(self, asset_info: AssetInfo, path: str): @@ -833,13 +1154,13 @@ def get_mappable_asset_ids_list(self) -> typing.List[str]: @abc.abstractmethod def get_default_icon(self) -> str: - pass + pass def is_mappable_asset(self, asset_info) -> bool: return asset_info.id in self.get_mappable_asset_ids_list() # returns the complete set of assets as they are mapped for the view - def get_view_assets(self) -> typing.Dict[str,str]: + def get_view_assets(self) -> typing.Dict[str, str]: asset_ids = self.get_asset_ids_list() mappable_asset_ids = self.get_mappable_asset_ids_list() view_asset_ids = asset_ids + list(set(mappable_asset_ids) - set(asset_ids)) @@ -870,7 +1191,7 @@ def get_view_assets(self) -> typing.Dict[str,str]: # Get a list of the assets that can be mapped to a defaultable asset. # They must be images, no videos, no documents. # - def get_mappable_asset_list(self) -> typing.List[AssetInfo]: + def get_mappable_asset_list(self) -> typing.List[AssetInfo]: return g_assetFactory.get_asset_list_by_IDs(self.get_mappable_asset_ids_list(), 'image') # @@ -884,7 +1205,7 @@ def get_asset_mapping(self, asset_info: AssetInfo): if not mapped_asset or not mapped_asset.to_asset_info: return asset_info return mapped_asset.to_asset_info - + def set_mapped_asset(self, asset_info: AssetInfo, mapped_to_info: AssetInfo): mapped_asset = next((m for m in self.asset_mappings if m.asset_info.id == asset_info.id), None) if not mapped_asset: @@ -896,6 +1217,7 @@ def set_mapped_asset(self, asset_info: AssetInfo, mapped_to_info: AssetInfo): def __str__(self): return '{}#{}: {}'.format(self.get_object_name(), self.get_id(), self.get_name()) + # ------------------------------------------------------------------------------------------------- # Class representing an AKL Category. # Contains code to generate the context menus passed to Dialog.select() @@ -903,7 +1225,7 @@ def __str__(self): class Category(MetaDataItemABC): __metaclass__ = abc.ABCMeta - def __init__(self, + def __init__(self, category_dic: typing.Dict[str, typing.Any] = None, assets: typing.List[Asset] = None, asset_mappings: typing.List[AssetMapping] = []): @@ -917,11 +1239,8 @@ def __init__(self, def get_object_name(self): return "Category" - def get_assets_kind(self): - return constants.KIND_ASSET_CATEGORY - def get_type(self): - return constants.OBJ_CATEGORY # 42501 + return constants.OBJ_CATEGORY # 42501 # parent category / romcollection this item belongs to. def get_parent_id(self) -> str: @@ -946,7 +1265,7 @@ def get_default_icon(self) -> str: return 'DefaultFolder.png' def get_NFO_name(self) -> io.FileName: - nfo_dir = io.FileName(settings.getSetting('categories_asset_dir'), isdir = True) + nfo_dir = io.FileName(settings.getSetting('categories_asset_dir'), isdir=True) nfo_file_path = nfo_dir.pjoin(self.get_name() + '.nfo') logger.debug("Category.get_NFO_name() nfo_file_path = '{0}'".format(nfo_file_path.getPath())) return nfo_file_path @@ -973,7 +1292,7 @@ def import_NFO_file(self, nfo_FileName: io.FileName) -> bool: try: item_nfo = nfo_FileName.loadFileToStr() item_nfo = item_nfo.replace('\r', '').replace('\n', '') - except: + except Exception: kodi.notify_warn(kodi.translate(41044).format(nfo_FileName.getPath())) logger.error("Category.import_NFO_file() Exception reading NFO file '{0}'".format(nfo_FileName.getPath())) return False @@ -982,19 +1301,24 @@ def import_NFO_file(self, nfo_FileName: io.FileName) -> bool: logger.error("Category.import_NFO_file() NFO file not found '{0}'".format(nfo_FileName.getPath())) return False - item_year = re.findall('(.*?)', item_nfo) - item_genre = re.findall('(.*?)', item_nfo) + item_year = re.findall('(.*?)', item_nfo) + item_genre = re.findall('(.*?)', item_nfo) item_developer = re.findall('(.*?)', item_nfo) - item_rating = re.findall('(.*?)', item_nfo) - item_plot = re.findall('(.*?)', item_nfo) + item_rating = re.findall('(.*?)', item_nfo) + item_plot = re.findall('(.*?)', item_nfo) # >> Careful about object mutability! This should modify the dictionary # >> passed as argument outside this function. - if len(item_year) > 0: self.set_releaseyear(text.unescape_XML(item_year[0])) - if len(item_genre) > 0: self.set_genre(text.unescape_XML(item_genre[0])) - if len(item_developer) > 0: self.set_developer(text.unescape_XML(item_developer[0])) - if len(item_rating) > 0: self.set_rating(text.unescape_XML(item_rating[0])) - if len(item_plot) > 0: self.set_plot(text.unescape_XML(item_plot[0])) + if len(item_year) > 0: + self.set_releaseyear(text.unescape_XML(item_year[0])) + if len(item_genre) > 0: + self.set_genre(text.unescape_XML(item_genre[0])) + if len(item_developer) > 0: + self.set_developer(text.unescape_XML(item_developer[0])) + if len(item_rating) > 0: + self.set_rating(text.unescape_XML(item_rating[0])) + if len(item_plot) > 0: + self.set_plot(text.unescape_XML(item_plot[0])) logger.debug("Category.import_NFO_file() Imported '{0}'".format(nfo_FileName.getPath())) @@ -1009,11 +1333,11 @@ def export_to_NFO_file(self, nfo_FileName: io.FileName): nfo_content.append('\n') nfo_content.append('\n'.format(time.strftime("%Y-%m-%d %H:%M:%S"))) nfo_content.append('\n') - nfo_content.append(text.XML_line('year', self.get_releaseyear())) - nfo_content.append(text.XML_line('genre', self.get_genre())) + nfo_content.append(text.XML_line('year', self.get_releaseyear())) + nfo_content.append(text.XML_line('genre', self.get_genre())) nfo_content.append(text.XML_line('developer', self.get_developer())) - nfo_content.append(text.XML_line('rating', self.get_rating())) - nfo_content.append(text.XML_line('plot', self.get_plot())) + nfo_content.append(text.XML_line('rating', self.get_rating())) + nfo_content.append(text.XML_line('plot', self.get_plot())) nfo_content.append('\n') full_string = ''.join(nfo_content) @@ -1050,31 +1374,29 @@ def export_to_file(self, file: io.FileName): def __str__(self): return super().__str__() + class VirtualCategory(Category): def get_object_name(self): return "Virtual Category" - def get_assets_kind(self): - return constants.KIND_ASSET_CATEGORY - def get_type(self): - return constants.OBJ_CATEGORY_VIRTUAL # 42502 + return constants.OBJ_CATEGORY_VIRTUAL # 42502 + # ------------------------------------------------------------------------------------------------- # Class representing a collection of ROMs. # ------------------------------------------------------------------------------------------------- class ROMCollection(MetaDataItemABC): __metaclass__ = abc.ABCMeta - def __init__(self, - entity_data: dict = None, + def __init__(self, + entity_data: dict = None, assets_data: typing.List[Asset] = None, - asset_paths: typing.List[AssetPath] = None, asset_mappings: typing.List[AssetMapping] = [], rom_asset_mappings: typing.List[RomAssetMapping] = [], - launchers_data: typing.List[ROMLauncherAddon] = [], - scanners_data: typing.List[ROMCollectionScanner] = []): + launchers_data: typing.List[ROMLauncherAddon] = [], + source_data: typing.List[Source] = []): # Concrete classes are responsible of creating a default entity_data dictionary # with sensible defaults. if entity_data is None: @@ -1082,26 +1404,24 @@ def __init__(self, entity_data['id'] = text.misc_generate_random_SID() self.launchers_data = launchers_data - self.scanners_data = scanners_data + self.scanners_data = source_data self.rom_asset_mappings = rom_asset_mappings mappable_assets = self.get_ROM_mappable_asset_list() if len(rom_asset_mappings) != len(mappable_assets): - already_mapped_assets_ids = [m.asset_info.id for m in rom_asset_mappings] - for asset_info in [a for a in mappable_assets if a.id not in already_mapped_assets_ids]: - mapping = RomAssetMapping() - mapping.asset_info = asset_info - self.rom_asset_mappings.append(mapping) + already_mapped_assets_ids = [m.asset_info.id for m in rom_asset_mappings] + for asset_info in [a for a in mappable_assets if a.id not in already_mapped_assets_ids]: + mapping = RomAssetMapping() + mapping.asset_info = asset_info + self.rom_asset_mappings.append(mapping) - super(ROMCollection, self).__init__(entity_data, assets_data, asset_paths, asset_mappings) + super(ROMCollection, self).__init__(entity_data, assets_data, None, asset_mappings) def get_object_name(self): return "ROM Collection" - def get_assets_kind(self): - return constants.KIND_ASSET_LAUNCHER - - def get_type(self): return constants.OBJ_ROMCOLLECTION + def get_type(self): + return constants.OBJ_ROMCOLLECTION # parent category / romcollection this item belongs to. def get_parent_id(self) -> str: @@ -1126,7 +1446,7 @@ def get_mappable_asset_ids_list(self): return constants.MAPPABLE_LAUNCHER_ASSET_ID_LIST def get_default_icon(self) -> str: - return 'DefaultGameAddons.png' + return 'DefaultGameAddons.png' def get_ROM_mappable_asset_list(self) -> typing.List[AssetInfo]: return g_assetFactory.get_asset_list_by_IDs(constants.MAPPABLE_ROM_ASSET_ID_LIST) @@ -1137,7 +1457,7 @@ def get_ROM_mappable_asset_list(self) -> typing.List[AssetInfo]: # def get_ROM_asset_mapping(self, asset_info: AssetInfo): mapped_asset = next((m for m in self.rom_asset_mappings if m.asset_info.id == asset_info.id), None) - if not mapped_asset: + if not mapped_asset or not mapped_asset.is_mapped(): # exception cases if asset_info.id == constants.ASSET_ICON_ID: return g_assetFactory.get_asset_info(constants.ASSET_BOXFRONT_ID) @@ -1145,7 +1465,7 @@ def get_ROM_asset_mapping(self, asset_info: AssetInfo): return g_assetFactory.get_asset_info(constants.ASSET_FLYER_ID) return asset_info return mapped_asset.to_asset_info - + def set_mapped_ROM_asset(self, asset_info: AssetInfo, mapped_to_info: AssetInfo): mapped_asset = next((m for m in self.rom_asset_mappings if m.asset_info.id == asset_info.id), None) if not mapped_asset: @@ -1154,30 +1474,6 @@ def set_mapped_ROM_asset(self, asset_info: AssetInfo, mapped_to_info: AssetInfo) mapped_asset.set_mapping(asset_info, mapped_to_info) - # - # Get a list of assets with duplicated paths. Refuse to do anything if duplicated paths found. - # - def get_duplicated_asset_dirs(self): - duplicated_bool_list = [False] * len(constants.ROM_ASSET_ID_LIST) - duplicated_name_list = [] - - # >> Check for duplicated asset paths - for i, asset_i in enumerate(constants.ROM_ASSET_ID_LIST[:-1]): - A_i = g_assetFactory.get_asset_info(asset_i) - for j, asset_j in enumerate(constants.ROM_ASSET_ID_LIST[i+1:]): - A_j = g_assetFactory.get_asset_info(asset_j) - # >> Exclude unconfigured assets (empty strings). - if A_i.path_key not in self.entity_data or A_j.path_key not in self.entity_data \ - or not self.entity_data[A_i.path_key] or not self.entity_data[A_j.path_key]: continue - - # logger.debug('asset_get_duplicated_asset_list() Checking {0:<9} vs {1:<9}'.format(A_i.name, A_j.name)) - if self.entity_data[A_i.path_key] == self.entity_data[A_j.path_key]: - duplicated_bool_list[i] = True - duplicated_name_list.append('{0} and {1}'.format(A_i.name, A_j.name)) - logger.info('asset_get_duplicated_asset_list() DUPLICATED {0} and {1}'.format(A_i.name, A_j.name)) - - return duplicated_name_list - def num_roms(self) -> int: return self.entity_data['num_roms'] if 'num_roms' in self.entity_data else 0 @@ -1187,29 +1483,25 @@ def has_roms(self) -> bool: def has_launchers(self) -> bool: return len(self.launchers_data) > 0 - def add_launcher(self, addon: AelAddon, settings: dict, is_non_blocking = True, is_default: bool = False): - launcher = ROMLauncherAddonFactory.create(addon, { - 'settings': json.dumps(settings), - 'is_non_blocking': is_non_blocking, - 'is_default': is_default - }) + def add_launcher(self, launcher: ROMLauncherAddon, is_default: bool = False): if is_default: - current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) - if current_default_launcher: current_default_launcher.set_default(False) + current_default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) + if current_default_launcher: + current_default_launcher.set_default(False) self.launchers_data.append(launcher) - logger.debug(f'Adding addon "{addon.get_addon_id()}" to collection "{self.get_name()}"') - + logger.debug(f'Adding launcher "{launcher.get_id()}" to collection "{self.get_name()}"') + def get_launchers(self) -> typing.List[ROMLauncherAddon]: return self.launchers_data - def get_launcher(self, id:str) -> ROMLauncherAddon: - return next((l for l in self.launchers_data if l.get_id() == id), None) + def get_launcher(self, id: str) -> ROMLauncherAddon: + return next((ld for ld in self.launchers_data if ld.get_id() == id), None) def get_default_launcher(self) -> ROMLauncherAddon: if len(self.launchers_data) == 0: return None - default_launcher = next((l for l in self.launchers_data if l.is_default()), None) + default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) if default_launcher is None: return self.launchers_data[0] @@ -1219,29 +1511,16 @@ def set_launcher_as_default(self, launcher_id): if len(self.launchers_data) == 0: return - current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) - if current_default_launcher: current_default_launcher.set_default(False) + current_default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) + if current_default_launcher: + current_default_launcher.set_default(False) - launcher_to_be_default = next((l for l in self.launchers_data if l.get_id() == launcher_id), None) + launcher_to_be_default = next((ldd for ldd in self.launchers_data if ldd.get_id() == launcher_id), None) if launcher_to_be_default: launcher_to_be_default.set_default(True) - def has_scanners(self) -> bool: - return len(self.scanners_data) > 0 - - def add_scanner(self, addon: AelAddon, settings: dict): - scanner = ROMCollectionScanner(addon, {}) - scanner.set_settings(settings) - self.scanners_data.append(scanner) - - def get_scanners(self) -> typing.List[ROMCollectionScanner]: - return self.scanners_data - - def get_scanner(self, id:str) -> ROMCollectionScanner: - return next((s for s in self.scanners_data if s.get_id() == id), None) - def get_NFO_name(self) -> io.FileName: - nfo_dir = io.FileName(settings.getSetting('launchers_asset_dir'), isdir = True) + nfo_dir = io.FileName(settings.getSetting('launchers_asset_dir'), isdir=True) nfo_file_path = nfo_dir.pjoin(self.get_name() + '.nfo') logger.debug("ROMCollection.get_NFO_name() nfo_file_path = '{0}'".format(nfo_file_path.getPath())) return nfo_file_path @@ -1268,7 +1547,7 @@ def import_NFO_file(self, nfo_FileName: io.FileName) -> bool: try: item_nfo = nfo_FileName.loadFileToStr() item_nfo = item_nfo.replace('\r', '').replace('\n', '') - except: + except Exception: kodi.notify_warn(kodi.translate(41044).format(nfo_FileName.getPath())) logger.error("ROMCollection.import_NFO_file() Exception reading NFO file '{0}'".format(nfo_FileName.getPath())) return False @@ -1277,19 +1556,24 @@ def import_NFO_file(self, nfo_FileName: io.FileName) -> bool: logger.error("ROMCollection.import_NFO_file() NFO file not found '{0}'".format(nfo_FileName.getPath())) return False - item_year = re.findall('(.*?)', item_nfo) - item_genre = re.findall('(.*?)', item_nfo) + item_year = re.findall('(.*?)', item_nfo) + item_genre = re.findall('(.*?)', item_nfo) item_developer = re.findall('(.*?)', item_nfo) - item_rating = re.findall('(.*?)', item_nfo) - item_plot = re.findall('(.*?)', item_nfo) + item_rating = re.findall('(.*?)', item_nfo) + item_plot = re.findall('(.*?)', item_nfo) # >> Careful about object mutability! This should modify the dictionary # >> passed as argument outside this function. - if len(item_year) > 0: self.set_releaseyear(text.unescape_XML(item_year[0])) - if len(item_genre) > 0: self.set_genre(text.unescape_XML(item_genre[0])) - if len(item_developer) > 0: self.set_developer(text.unescape_XML(item_developer[0])) - if len(item_rating) > 0: self.set_rating(text.unescape_XML(item_rating[0])) - if len(item_plot) > 0: self.set_plot(text.unescape_XML(item_plot[0])) + if len(item_year) > 0: + self.set_releaseyear(text.unescape_XML(item_year[0])) + if len(item_genre) > 0: + self.set_genre(text.unescape_XML(item_genre[0])) + if len(item_developer) > 0: + self.set_developer(text.unescape_XML(item_developer[0])) + if len(item_rating) > 0: + self.set_rating(text.unescape_XML(item_rating[0])) + if len(item_plot) > 0: + self.set_plot(text.unescape_XML(item_plot[0])) logger.debug("ROMCollection.import_NFO_file() Imported '{0}'".format(nfo_FileName.getPath())) @@ -1304,11 +1588,11 @@ def export_to_NFO_file(self, nfo_FileName: io.FileName): nfo_content.append('\n') nfo_content.append('\n'.format(time.strftime("%Y-%m-%d %H:%M:%S"))) nfo_content.append('\n') - nfo_content.append(text.XML_line('year', self.get_releaseyear())) - nfo_content.append(text.XML_line('genre', self.get_genre())) + nfo_content.append(text.XML_line('year', self.get_releaseyear())) + nfo_content.append(text.XML_line('genre', self.get_genre())) nfo_content.append(text.XML_line('developer', self.get_developer())) - nfo_content.append(text.XML_line('rating', self.get_rating())) - nfo_content.append(text.XML_line('plot', self.get_plot())) + nfo_content.append(text.XML_line('rating', self.get_rating())) + nfo_content.append(text.XML_line('plot', self.get_plot())) nfo_content.append('\n') full_string = ''.join(nfo_content) @@ -1329,7 +1613,7 @@ def export_to_file(self, file: io.FileName): str_list.append(text.XML_line('developer', self.get_developer())) str_list.append(text.XML_line('rating', self.get_rating())) str_list.append(text.XML_line('plot', self.get_plot())) - #str_list.append(text.XML_line('Asset_Prefix', self.get_custom_attribute('Asset_Prefix'))) + # str_list.append(text.XML_line('Asset_Prefix', self.get_custom_attribute('Asset_Prefix'))) str_list.append(text.XML_line('s_icon', self.get_asset_str(asset_id=constants.ASSET_ICON_ID))) str_list.append(text.XML_line('s_fanart', self.get_asset_str(asset_id=constants.ASSET_FANART_ID))) str_list.append(text.XML_line('s_banner', self.get_asset_str(asset_id=constants.ASSET_BANNER_ID))) @@ -1346,10 +1630,11 @@ def export_to_file(self, file: io.FileName): def __str__(self): return super().__str__() + class VirtualCollection(ROMCollection): - def __init__(self, - entity_data: dict = None, - assets_data: typing.List[Asset] = None): + def __init__(self, + entity_data: dict = None, + assets_data: typing.List[Asset] = None): # Concrete classes are responsible of creating a default entity_data dictionary # with sensible defaults. if entity_data is None: @@ -1361,9 +1646,6 @@ def __init__(self, def get_object_name(self): return "Virtual Collection" - def get_assets_kind(self): - return constants.KIND_ASSET_COLLECTION - def get_type(self): return constants.OBJ_COLLECTION_VIRTUAL @@ -1376,12 +1658,13 @@ def get_mappable_asset_ids_list(self): def get_collection_value(self) -> str: return self.entity_data['collection_value'] if 'collection_value' in self.entity_data else None + # ------------------------------------------------------------------------------------------------- # Class representing a ROM file you can play through AKL. # ------------------------------------------------------------------------------------------------- class ROM(MetaDataItemABC): - def __init__(self, + def __init__(self, rom_data: dict = None, tag_data: dict = None, assets_data: typing.List[Asset] = None, @@ -1390,9 +1673,32 @@ def __init__(self, scanned_data: dict = {}, launchers_data: typing.List[ROMLauncherAddon] = []): if rom_data is None: - rom_data = _get_default_ROM_data_model() - rom_data['id'] = text.misc_generate_random_SID() - + rom_data = { + 'id': text.misc_generate_random_SID(), + 'm_name': '', + 'nplayers': 0, + 'nplayers_online': 0, + 'esrb': constants.ESRB_PENDING, + 'pegi': constants.DEFAULT_META_PEGI, + 'nointro_status': constants.AUDIT_STATUS_NONE, + 'pclone_status': constants.PCLONE_STATUS_NONE, + 'cloneof': '', + 'platform': '', + 'scanned_by_id': '', + 'box_size': '', + 'm_year': '', + 'm_genre': '', + 'm_developer': '', + 'm_rating': '', + 'm_plot': '', + 'extra': '', + 'finished': False, + 'rom_status': '', + 'is_favourite': False, + 'launch_count': 0, + 'last_launch_timestamp': None + } + self.tags = tag_data self.scanned_data = scanned_data self.launchers_data = launchers_data @@ -1403,21 +1709,19 @@ def __init__(self, mappable_assets = self.get_mappable_asset_list() if len(asset_mappings) != len(mappable_assets): - already_mapped_assets_ids = [m.asset_info.id for m in asset_mappings] - for asset_info in [a for a in mappable_assets if a.id not in already_mapped_assets_ids]: - mapping = RomAssetMapping() - mapping.asset_info = asset_info - asset_mappings.append(mapping) + already_mapped_assets_ids = [m.asset_info.id for m in asset_mappings] + for asset_info in [a for a in mappable_assets if a.id not in already_mapped_assets_ids]: + mapping = RomAssetMapping() + mapping.asset_info = asset_info + asset_mappings.append(mapping) super(ROM, self).__init__(rom_data, assets_data, asset_paths_data, asset_mappings) def get_object_name(self): return 'ROM' - def get_assets_kind(self): - return constants.KIND_ASSET_ROM - - def get_type(self): return constants.OBJ_ROM + def get_type(self): + return constants.OBJ_ROM def get_rom_identifier(self) -> str: identifier = self.get_scanned_data_element('identifier') @@ -1466,16 +1770,16 @@ def get_nfo_file(self): return None def get_number_of_players(self): - return self.entity_data['m_nplayers'] + return self.entity_data['nplayers'] def get_number_of_players_online(self): - return self.entity_data['m_nplayers_online'] + return self.entity_data['nplayers_online'] def get_esrb_rating(self): - return self.entity_data['m_esrb'] + return self.entity_data['esrb'] def get_pegi_rating(self): - return self.entity_data['m_pegi'] + return self.entity_data['pegi'] def get_rom_status(self): return self.entity_data['rom_status'] if 'rom_status' in self.entity_data else None @@ -1499,43 +1803,43 @@ def get_launch_count(self): def get_last_launch_date(self): return self.entity_data['last_launch_timestamp'] if 'last_launch_timestamp' in self.entity_data else None - def get_scanned_with(self) -> str: + def get_scanned_by(self) -> str: return self.entity_data['scanned_by_id'] if 'scanned_by_id' in self.entity_data else None def add_disk(self, disk): - if not 'disks' in self.entity_data or self.entity_data['disks'] is None: + if 'disks' not in self.entity_data or self.entity_data['disks'] is None: self.entity_data['disks'] = [] - disks:list = self.entity_data['disks'] + disks: list = self.entity_data['disks'] disks.append(disk) self.entity_data['disks'] = disks def set_number_of_players(self, amount): - self.entity_data['m_nplayers'] = amount + self.entity_data['nplayers'] = amount def set_number_of_players_online(self, amount): - self.entity_data['m_nplayers_online'] = amount + self.entity_data['nplayers_online'] = amount def set_esrb_rating(self, esrb): - self.entity_data['m_esrb'] = esrb + self.entity_data['esrb'] = esrb def set_pegi_rating(self, pegi): - self.entity_data['m_pegi'] = pegi + self.entity_data['pegi'] = pegi def set_platform(self, platform): self.entity_data['platform'] = platform - def add_tag(self, tag:str): + def add_tag(self, tag: str): if self.tags is None: self.tags = {} if tag in self.tags: return self.tags[tag] = '' - def remove_tag(self, tag:str): + def remove_tag(self, tag: str): if self.tags is None: return - if not tag in self.tags: + if tag not in self.tags: return del self.tags[tag] @@ -1551,20 +1855,20 @@ def set_pclone_status(self, status): def set_clone(self, clone): self.entity_data['cloneof'] = clone - def scanned_with(self, scanner_id: str): + def scanned_by(self, scanner_id: str): self.entity_data['scanned_by_id'] = scanner_id def get_scanned_data(self): return self.scanned_data - def get_scanned_data_element(self, key:str): + def get_scanned_data_element(self, key: str): return self.scanned_data[key] if key in self.scanned_data else None - def get_scanned_data_element_as_file(self, key:str) -> io.FileName: + def get_scanned_data_element_as_file(self, key: str) -> io.FileName: scanned_value = self.scanned_data[key] if key in self.scanned_data else None return self._to_filename(scanned_value) - def set_scanned_data_element(self, key:str, data): + def set_scanned_data_element(self, key: str, data): self.scanned_data[key] = data def set_rom_status(self, state): @@ -1588,30 +1892,25 @@ def set_box_sizing(self, box_size): def has_launchers(self) -> bool: return len(self.launchers_data) > 0 - def add_launcher(self, addon: AelAddon, settings: dict, is_non_blocking = True, is_default: bool = False): - launcher = ROMLauncherAddonFactory.create(addon, { - 'settings': json.dumps(settings), - 'is_non_blocking': is_non_blocking, - 'is_default': is_default - }) + def add_launcher(self, launcher: ROMLauncherAddon, is_default: bool = False): if is_default: - current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) + current_default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) if current_default_launcher: current_default_launcher.set_default(False) self.launchers_data.append(launcher) - logger.debug(f'Adding addon "{addon.get_addon_id()}" to ROM "{self.get_name()}"') + logger.debug(f'Adding launcher "{launcher.get_id()}" to ROM "{self.get_name()}"') def get_launchers(self) -> typing.List[ROMLauncherAddon]: return self.launchers_data - def get_launcher(self, id:str) -> ROMLauncherAddon: - return next((l for l in self.launchers_data if l.get_id() == id), None) + def get_launcher(self, id: str) -> ROMLauncherAddon: + return next((ld for ld in self.launchers_data if ld.get_id() == id), None) def get_default_launcher(self) -> ROMLauncherAddon: if len(self.launchers_data) == 0: return None - default_launcher = next((l for l in self.launchers_data if l.is_default()), None) + default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) if default_launcher is None: return self.launchers_data[0] @@ -1621,17 +1920,18 @@ def set_launcher_as_default(self, launcher_id): if len(self.launchers_data) == 0: return - current_default_launcher = next((l for l in self.launchers_data if l.is_default()), None) - if current_default_launcher: current_default_launcher.set_default(False) + current_default_launcher = next((ld for ld in self.launchers_data if ld.is_default()), None) + if current_default_launcher: + current_default_launcher.set_default(False) - launcher_to_be_default = next((l for l in self.launchers_data if l.get_id() == launcher_id), None) + launcher_to_be_default = next((ld for ld in self.launchers_data if ld.get_id() == launcher_id), None) if launcher_to_be_default: launcher_to_be_default.set_default(True) def copy(self): data = self.copy_of_data_dic() return ROM(data) - + def get_asset_ids_list(self): return constants.ROM_ASSET_ID_LIST @@ -1658,12 +1958,13 @@ def set_mapped_asset(self, asset_info: AssetInfo, mapped_to_info: AssetInfo): mapped_asset.set_mapping(asset_info, mapped_to_info) def get_default_icon(self) -> str: - return 'DefaultProgram.png' + return 'DefaultProgram.png' def create_dto(self) -> api.ROMObj: - dto_data:dict = api.ROMObj.get_data_template() + dto_data: dict = api.ROMObj.get_data_template() for key in list(dto_data.keys()): - if key in self.entity_data: dto_data[key] = self.entity_data[key] + if key in self.entity_data: + dto_data[key] = self.entity_data[key] dto_data['tags'] = self.get_tags() @@ -1684,7 +1985,7 @@ def create_dto(self) -> api.ROMObj: # About reading files in Unicode http://stackoverflow.com/questions/147741/character-reading-from-file-in-python # # todo: Replace with nfo_file_path.readXml() and just use XPath - def update_with_nfo_file(self, nfo_file_path:io.FileName, verbose = True): + def update_with_nfo_file(self, nfo_file_path: io.FileName, verbose=True): logger.debug('Rom.update_with_nfo_file() Loading "{0}"'.format(nfo_file_path.getPath())) if not nfo_file_path.exists(): if verbose: @@ -1705,28 +2006,38 @@ def update_with_nfo_file(self, nfo_file_path:io.FileName, verbose = True): # See https://docs.python.org/2/library/re.html#re.findall # If RE has no groups it returns a list of strings with the matches. # If RE has groups then it returns a list of groups. - item_title = re.findall('(.*?)', nfo_str) - item_year = re.findall('(.*?)', nfo_str) - item_genre = re.findall('(.*?)', nfo_str) + item_title = re.findall('(.*?)', nfo_str) + item_year = re.findall('(.*?)', nfo_str) + item_genre = re.findall('(.*?)', nfo_str) item_developer = re.findall('(.*?)', nfo_str) - item_nplayers = re.findall('(.*?)', nfo_str) - item_esrb = re.findall('(.*?)', nfo_str) - item_pegi = re.findall('(.*?)', nfo_str) - item_rating = re.findall('(.*?)', nfo_str) - item_plot = re.findall('(.*?)', nfo_str) - item_trailer = re.findall('(.*?)', nfo_str) + item_nplayers = re.findall('(.*?)', nfo_str) + item_esrb = re.findall('(.*?)', nfo_str) + item_pegi = re.findall('(.*?)', nfo_str) + item_rating = re.findall('(.*?)', nfo_str) + item_plot = re.findall('(.*?)', nfo_str) + item_trailer = re.findall('(.*?)', nfo_str) # >> Future work: ESRB and maybe nplayer fields must be sanitized. - if len(item_title) > 0: self.set_name(text.unescape_XML(item_title[0])) - if len(item_year) > 0: self.set_releaseyear(text.unescape_XML(item_year[0])) - if len(item_genre) > 0: self.set_genre(text.unescape_XML(item_genre[0])) - if len(item_developer) > 0: self.set_developer(text.unescape_XML(item_developer[0])) - if len(item_rating) > 0: self.set_rating(text.unescape_XML(item_rating[0])) - if len(item_plot) > 0: self.set_plot(text.unescape_XML(item_plot[0])) - if len(item_nplayers) > 0: self.set_number_of_players(text.unescape_XML(item_nplayers[0])) - if len(item_esrb) > 0: self.set_esrb_rating(text.unescape_XML(item_esrb[0])) - if len(item_pegi) > 0: self.set_pegi_rating(text.unescape_XML(item_pegi[0])) - if len(item_trailer) > 0: self.set_trailer(text.unescape_XML(item_trailer[0])) + if len(item_title) > 0: + self.set_name(text.unescape_XML(item_title[0])) + if len(item_year) > 0: + self.set_releaseyear(text.unescape_XML(item_year[0])) + if len(item_genre) > 0: + self.set_genre(text.unescape_XML(item_genre[0])) + if len(item_developer) > 0: + self.set_developer(text.unescape_XML(item_developer[0])) + if len(item_rating) > 0: + self.set_rating(text.unescape_XML(item_rating[0])) + if len(item_plot) > 0: + self.set_plot(text.unescape_XML(item_plot[0])) + if len(item_nplayers) > 0: + self.set_number_of_players(text.unescape_XML(item_nplayers[0])) + if len(item_esrb) > 0: + self.set_esrb_rating(text.unescape_XML(item_esrb[0])) + if len(item_pegi) > 0: + self.set_pegi_rating(text.unescape_XML(item_pegi[0])) + if len(item_trailer) > 0: + self.set_trailer(text.unescape_XML(item_trailer[0])) if verbose: kodi.notify(kodi.translate(41046).format(nfo_file_path.getPath())) @@ -1742,16 +2053,16 @@ def export_to_NFO_file(self, nfo_FileName: io.FileName): nfo_content.append('\n') nfo_content.append('\n'.format(time.strftime("%Y-%m-%d %H:%M:%S"))) nfo_content.append('\n') - nfo_content.append(text.XML_line('title', self.get_name())) - nfo_content.append(text.XML_line('year', self.get_releaseyear())) - nfo_content.append(text.XML_line('genre', self.get_genre())) + nfo_content.append(text.XML_line('title', self.get_name())) + nfo_content.append(text.XML_line('year', self.get_releaseyear())) + nfo_content.append(text.XML_line('genre', self.get_genre())) nfo_content.append(text.XML_line('developer', self.get_developer())) - nfo_content.append(text.XML_line('nplayers', self.get_number_of_players())) - nfo_content.append(text.XML_line('esrb', self.get_esrb_rating())) - nfo_content.append(text.XML_line('pegi', self.get_pegi_rating())) - nfo_content.append(text.XML_line('rating', self.get_rating())) - nfo_content.append(text.XML_line('plot', self.get_plot())) - nfo_content.append(text.XML_line('trailer', self.get_trailer())) + nfo_content.append(text.XML_line('nplayers', self.get_number_of_players())) + nfo_content.append(text.XML_line('esrb', self.get_esrb_rating())) + nfo_content.append(text.XML_line('pegi', self.get_pegi_rating())) + nfo_content.append(text.XML_line('rating', self.get_rating())) + nfo_content.append(text.XML_line('plot', self.get_plot())) + nfo_content.append(text.XML_line('trailer', self.get_trailer())) nfo_content.append('\n') full_string = ''.join(nfo_content) @@ -1762,12 +2073,12 @@ def export_to_NFO_file(self, nfo_FileName: io.FileName): # Flags indicate which elements are allowed to be updated/altered with the incoming data. # def update_with(self, - api_rom_obj: api.ROMObj, - metadata_to_update=[], - assets_to_update=[], - overwrite_existing_metadata=False, - overwrite_existing_assets=False, - update_scanned_data=False): + api_rom_obj: api.ROMObj, + metadata_to_update=[], + assets_to_update=[], + overwrite_existing_metadata=False, + overwrite_existing_assets=False, + update_scanned_data=False): logger.debug(f"Overwriting existing metadata in domain: {overwrite_existing_metadata}") logger.debug(f"Overwriting existing assets in domain: {overwrite_existing_assets}") @@ -1775,61 +2086,61 @@ def update_with(self, if constants.META_TITLE_ID in metadata_to_update \ and api_rom_obj.get_name() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_name(), constants.DEFAULT_META_TITLE)): + _is_empty_or_default(self.get_name(), constants.DEFAULT_META_TITLE)): self.set_name(api_rom_obj.get_name()) if constants.META_PLOT_ID in metadata_to_update \ and api_rom_obj.get_plot() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_plot(), constants.DEFAULT_META_PLOT)): + _is_empty_or_default(self.get_plot(), constants.DEFAULT_META_PLOT)): self.set_plot(api_rom_obj.get_plot()) if constants.META_YEAR_ID in metadata_to_update \ and api_rom_obj.get_releaseyear() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_releaseyear(), constants.DEFAULT_META_YEAR)): + _is_empty_or_default(self.get_releaseyear(), constants.DEFAULT_META_YEAR)): self.set_releaseyear(api_rom_obj.get_releaseyear()) if constants.META_GENRE_ID in metadata_to_update \ and api_rom_obj.get_genre() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_genre(), constants.DEFAULT_META_GENRE)): + _is_empty_or_default(self.get_genre(), constants.DEFAULT_META_GENRE)): self.set_genre(api_rom_obj.get_genre()) if constants.META_DEVELOPER_ID in metadata_to_update \ and api_rom_obj.get_developer() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_developer(), constants.DEFAULT_META_DEVELOPER)): + _is_empty_or_default(self.get_developer(), constants.DEFAULT_META_DEVELOPER)): self.set_developer(api_rom_obj.get_developer()) if constants.META_NPLAYERS_ID in metadata_to_update \ and api_rom_obj.get_number_of_players() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_number_of_players(), constants.DEFAULT_META_NPLAYERS)): + _is_empty_or_default(self.get_number_of_players(), constants.DEFAULT_META_NPLAYERS)): self.set_number_of_players(api_rom_obj.get_number_of_players()) if constants.META_NPLAYERS_ONLINE_ID in metadata_to_update \ and api_rom_obj.get_number_of_players_online() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_number_of_players_online(), constants.DEFAULT_META_NPLAYERS)): + _is_empty_or_default(self.get_number_of_players_online(), constants.DEFAULT_META_NPLAYERS)): self.set_number_of_players_online(api_rom_obj.get_number_of_players_online()) if constants.META_ESRB_ID in metadata_to_update\ and api_rom_obj.get_esrb_rating() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_esrb_rating(), constants.DEFAULT_META_ESRB)): + _is_empty_or_default(self.get_esrb_rating(), constants.DEFAULT_META_ESRB)): self.set_esrb_rating(api_rom_obj.get_esrb_rating()) if constants.META_PEGI_ID in metadata_to_update\ and api_rom_obj.get_pegi_rating() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_pegi_rating(), constants.DEFAULT_META_PEGI)): + _is_empty_or_default(self.get_pegi_rating(), constants.DEFAULT_META_PEGI)): self.set_pegi_rating(api_rom_obj.get_pegi_rating()) if constants.META_RATING_ID in metadata_to_update \ and api_rom_obj.get_rating() \ and (overwrite_existing_metadata or \ - _is_empty_or_default(self.get_rating(), constants.DEFAULT_META_RATING)): + _is_empty_or_default(self.get_rating(), constants.DEFAULT_META_RATING)): self.set_rating(api_rom_obj.get_rating()) if constants.META_TAGS_ID in metadata_to_update and api_rom_obj.get_tags() is not None: @@ -1846,7 +2157,7 @@ def update_with(self, existing_asset = self.get_asset(asset_id) new_asset = api_rom_obj.get_asset(asset_id) if new_asset is not None and \ - (overwrite_existing_assets or existing_asset is None or not existing_asset.is_assigned()): + (overwrite_existing_assets or existing_asset is None or not existing_asset.is_assigned()): if asset_id == constants.ASSET_TRAILER_ID: self.set_trailer(new_asset) else: @@ -1867,24 +2178,44 @@ def update_with(self, # and kodi.dialog_yesno('Do you want to overwrite collection metadata properties with values from the launcher?'): # romcollection.import_data_dic(launcher_settings['romcollection']) # metadata_updated = True - - - def apply_romcollection_asset_paths(self, romcollection: ROMCollection): - self.set_assets_root_path(romcollection.get_assets_root_path()) + + def apply_source_asset_paths(self, source: Source): + self.set_assets_root_path(source.get_assets_root_path()) self.asset_paths = {} - for assetpath in romcollection.get_asset_paths(): + for assetpath in source.get_asset_paths(): self.asset_paths[assetpath.get_asset_info_id()] = assetpath - + def apply_romcollection_asset_mapping(self, romcollection: ROMCollection): mappable_assets = romcollection.get_ROM_mappable_asset_list() for mappable_asset in mappable_assets: mapped_asset = romcollection.get_ROM_asset_mapping(mappable_asset) self.set_mapped_asset(mappable_asset, mapped_asset) + def get_fields_with_translations(): + return { + 'm_name': 40815, + 'nplayers': 40808, + 'nplayers_online': 40809, + 'esrb': 40804, + 'pegi': 40805, + 'platform': 40807, + 'box_size': 40816, + 'm_year': 40803, + 'm_genre': 40801, + 'm_developer': 40802, + 'm_rating': 40806, + 'm_plot': 40811, + 'finished': 42014, + 'is_favourite': 40818, + 'launch_count': 40819, + 'tags': 40810 + } + def __str__(self): """Overrides the default implementation""" return json.dumps(self.entity_data) + # ------------------------------------------------------------------------------------------------- # OBJECT FACTORIES # ------------------------------------------------------------------------------------------------- @@ -1896,7 +2227,7 @@ class AssetInfoFactory(object): def __init__(self): # default collections - self.ASSET_INFO_ID_DICT:typing.Dict[str,AssetInfo] = {} # ID -> object + self.ASSET_INFO_ID_DICT: typing.Dict[str, AssetInfo] = {} # ID -> object self._load_asset_data() # ------------------------------------------------------------------------------------------------- @@ -1925,35 +2256,34 @@ def get_asset_info_by_pathkey(self, path_key): return asset_info - def get_assets_for_type(self, asset_kind) -> typing.List[AssetInfo]: - if asset_kind == constants.KIND_ASSET_CATEGORY: + def get_assets_for_type(self, obj_type) -> typing.List[AssetInfo]: + if obj_type == constants.OBJ_CATEGORY: return self.get_asset_list_by_IDs(constants.CATEGORY_ASSET_ID_LIST) - if asset_kind == constants.KIND_ASSET_COLLECTION: + if obj_type == constants.OBJ_ROMCOLLECTION: return self.get_asset_list_by_IDs(constants.COLLECTION_ASSET_ID_LIST) - if asset_kind == constants.KIND_ASSET_LAUNCHER: - return self.get_asset_list_by_IDs(constants.LAUNCHER_ASSET_ID_LIST) - if asset_kind == constants.KIND_ASSET_ROM: + if obj_type == constants.OBJ_ROM: return self.get_asset_list_by_IDs(constants.ROM_ASSET_ID_LIST) return [] - def get_asset_kinds_for_roms(self) -> typing.List[AssetInfo]: - rom_asset_kinds = [] + def get_asset_for_roms(self) -> typing.List[AssetInfo]: + rom_assets = [] for rom_asset_id in constants.ROM_ASSET_ID_LIST: - rom_asset_kinds.append(self.ASSET_INFO_ID_DICT[rom_asset_id]) + rom_assets.append(self.ASSET_INFO_ID_DICT[rom_asset_id]) - return rom_asset_kinds + return rom_assets # IDs is a list (or an iterable that returns an asset ID # Returns a list of AssetInfo objects. # If the asset kind is given, it will filter out assets not corresponding to that kind. - def get_asset_list_by_IDs(self, IDs, kind = None) -> typing.List[AssetInfo]: + def get_asset_list_by_IDs(self, IDs, kind=None) -> typing.List[AssetInfo]: asset_info_list = [] for asset_ID in IDs: asset_info = self.ASSET_INFO_ID_DICT.get(asset_ID, None) if asset_info is None: logger.error('get_asset_list_by_IDs() Wrong asset_ID = {0}'.format(asset_ID)) continue - if kind is None or asset_info.kind_str == kind: asset_info_list.append(asset_info) + if kind is None or asset_info.kind_str == kind: + asset_info_list.append(asset_info) return asset_info_list @@ -1996,24 +2326,39 @@ def asset_get_dialog_extension_list(self, exts) -> str: # # Returns a FileName object # - def assets_get_path_noext_SUFIX(self, asset_ID, AssetPath, asset_base_noext, objectID = '000'): + def assets_get_path_noext_SUFIX(self, asset_ID, AssetPath, asset_base_noext, objectID='000'): objectID_str = '_' + objectID[0:3] - if asset_ID == constants.ASSET_ICON_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_icon') - elif asset_ID == constants.ASSET_FANART_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_fanart') - elif asset_ID == constants.ASSET_BANNER_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_banner') - elif asset_ID == constants.ASSET_POSTER_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_poster') - elif asset_ID == constants.ASSET_CLEARLOGO_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_clearlogo') - elif asset_ID == constants.ASSET_CONTROLLER_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_controller') - elif asset_ID == constants.ASSET_TRAILER_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_trailer') - elif asset_ID == constants.ASSET_TITLE_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_title') - elif asset_ID == constants.ASSET_SNAP_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_snap') - elif asset_ID == constants.ASSET_BOXFRONT_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_boxfront') - elif asset_ID == constants.ASSET_BOXBACK_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_boxback') - elif asset_ID == constants.ASSET_CARTRIDGE_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_cartridge') - elif asset_ID == constants.ASSET_FLYER_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_flyer') - elif asset_ID == constants.ASSET_MAP_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_map') - elif asset_ID == constants.ASSET_MANUAL_ID: asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_manual') + if asset_ID == constants.ASSET_ICON_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_icon') + elif asset_ID == constants.ASSET_FANART_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_fanart') + elif asset_ID == constants.ASSET_BANNER_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_banner') + elif asset_ID == constants.ASSET_POSTER_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_poster') + elif asset_ID == constants.ASSET_CLEARLOGO_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_clearlogo') + elif asset_ID == constants.ASSET_CONTROLLER_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_controller') + elif asset_ID == constants.ASSET_TRAILER_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_trailer') + elif asset_ID == constants.ASSET_TITLE_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_title') + elif asset_ID == constants.ASSET_SNAP_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_snap') + elif asset_ID == constants.ASSET_BOXFRONT_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_boxfront') + elif asset_ID == constants.ASSET_BOXBACK_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_boxback') + elif asset_ID == constants.ASSET_CARTRIDGE_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_cartridge') + elif asset_ID == constants.ASSET_FLYER_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_flyer') + elif asset_ID == constants.ASSET_MAP_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_map') + elif asset_ID == constants.ASSET_MANUAL_ID: + asset_path_noext_FN = AssetPath.pjoin(asset_base_noext + objectID_str + '_manual') else: asset_path_noext_FN = io.FileName('') logger.error('assets_get_path_noext_SUFIX() Wrong asset_ID = {0}'.format(asset_ID)) @@ -2296,7 +2641,7 @@ def create(vcollection_id: str) -> VirtualCollection: return VirtualCollection(dict(default_entity_data, **{ 'id' : vcollection_id, 'm_name' : kodi.translate(42063), - 'plot': kodi.translate(42005), + 'plot': kodi.translate(44005), 'finished': settings.getSettingAsBool('display_hide_favs') }), [ Asset({ @@ -2320,7 +2665,7 @@ def create(vcollection_id: str) -> VirtualCollection: return VirtualCollection(dict(default_entity_data, **{ 'id' : vcollection_id, 'm_name' : kodi.translate(42064), - 'plot': kodi.translate(42006), + 'plot': kodi.translate(44006), 'finished': settings.getSettingAsBool('display_hide_recent') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2332,7 +2677,7 @@ def create(vcollection_id: str) -> VirtualCollection: return VirtualCollection(dict(default_entity_data, **{ 'id' : vcollection_id, 'm_name' : kodi.translate(42065), - 'plot': kodi.translate(42007), + 'plot': kodi.translate(44007), 'finished': settings.getSettingAsBool('display_hide_mostplayed') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2350,7 +2695,7 @@ def create_by_category(vcategory_id:str, collection_value:str) -> VirtualCollect 'id' : f'{vcategory_id}_{collection_value}', 'parent_id': vcategory_id, 'm_name' : collection_value, - 'plot': kodi.translate(42008).format(collection_value), + 'plot': kodi.translate(44008).format(collection_value), 'collection_value': collection_value, 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ @@ -2367,7 +2712,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name' : kodi.translate(42066), - 'plot': kodi.translate(42009), + 'plot': kodi.translate(44009), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2379,7 +2724,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name' : kodi.translate(42067), - 'plot': kodi.translate(42010), + 'plot': kodi.translate(44010), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2391,7 +2736,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name' : kodi.translate(42068), - 'plot': kodi.translate(42011), + 'plot': kodi.translate(44011), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2403,7 +2748,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name' : kodi.translate(42069), - 'plot': kodi.translate(42012), + 'plot': kodi.translate(44012), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2415,7 +2760,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name' : kodi.translate(42070), - 'plot': kodi.translate(42013), + 'plot': kodi.translate(44013), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2427,7 +2772,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name' : kodi.translate(42071), - 'plot': kodi.translate(42014), + 'plot': kodi.translate(44014), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2439,7 +2784,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name': kodi.translate(42072), - 'plot': kodi.translate(42015), + 'plot': kodi.translate(44015), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2451,7 +2796,7 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory({ 'id' : vcategory_id, 'm_name': kodi.translate(42073), - 'plot': kodi.translate(42016), + 'plot': kodi.translate(44016), 'finished': settings.getSettingAsBool('display_hide_vcategories') }, [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), @@ -2463,24 +2808,26 @@ def create(vcategory_id: str) -> VirtualCategory: return VirtualCategory(dict(default_entity_data, **{ 'id' : vcategory_id, 'm_name': kodi.translate(42074), - 'plot': kodi.translate(42017), + 'plot': kodi.translate(44017), 'finished': settings.getSettingAsBool('display_hide_vcategories') }), [ Asset({'id' : '', 'asset_type' : constants.ASSET_FANART_ID, 'filepath' : globals.g_PATHS.FANART_FILE_PATH.getPath()}), Asset({'id' : '', 'asset_type' : constants.ASSET_ICON_ID, 'filepath' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Browse_by_User_Rating_icon.png').getPath()}), Asset({'id' : '', 'asset_type' : constants.ASSET_POSTER_ID, 'filepath' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Browse_by_User_Rating_poster.png').getPath()}), - ]) + ]) return None + class ROMLauncherAddonFactory(object): @staticmethod - def create(addon: AelAddon, data:dict) -> ROMLauncherAddon: + def create(addon: AelAddon, data: dict) -> ROMLauncherAddon: if addon.get_addon_id() == constants.RETROPLAYER_LAUNCHER_APP_NAME: - return RetroplayerLauncherAddon(addon, data) + return RetroplayerLauncherAddon(data, addon) - return ROMLauncherAddon(addon, data) + return ROMLauncherAddon(data, addon) + # ------------------------------------------------------------------------------------------------- # Data model used in the plugin @@ -2560,28 +2907,6 @@ def _get_default_ROMCollection_data_model(): 'path_trailer' : '' } -def _get_default_ROM_data_model(): - return { - 'id' : '', - 'type': constants.OBJ_ROM, - 'm_name' : '', - 'm_year' : '', - 'm_genre' : '', - 'm_developer' : '', - 'm_nplayers' : '', - 'm_nplayers_online' : '', - 'm_esrb' : constants.ESRB_PENDING, - 'm_pegi' : constants.DEFAULT_META_PEGI, - 'm_rating' : '', - 'm_plot' : '', - 'platform': '', - 'box_size': '', - 'disks' : [], - 'finished' : False, - 'nointro_status' : constants.AUDIT_STATUS_NONE, - 'pclone_status' : constants.PCLONE_STATUS_NONE, - 'cloneof' : '' - } def _get_default_asset_data_model(): return { diff --git a/resources/lib/editors.py b/resources/lib/editors.py index ac8e60c9..05bfa2aa 100644 --- a/resources/lib/editors.py +++ b/resources/lib/editors.py @@ -4,7 +4,7 @@ # # Copyright (c) Chrisism -# Portions (c) Wintermute0110 +# Portions (c) Wintermute0110 # Portions (c) 2010-2015 Angelscry # # This program is free software; you can redistribute it and/or modify @@ -28,10 +28,12 @@ from akl.utils import kodi, io, text from akl import constants, settings -from resources.lib.domain import MetaDataItemABC, AssetInfo, g_assetFactory +from resources.lib import globals +from resources.lib.domain import EntityABC, MetaDataItemABC, AssetInfo, g_assetFactory logger = logging.getLogger(__name__) + # Edits an object field which is a Unicode string. # # Example call: @@ -52,6 +54,7 @@ def edit_field_by_str(obj_instance: MetaDataItemABC, metadata_name, get_method, kodi.notify(kodi.translate(40986).format(object_name, metadata_name, new_value)) return True + # Edits an object field which is an integer. # # Example call: @@ -62,7 +65,8 @@ def edit_field_by_int(obj_instance: MetaDataItemABC, metadata_name, get_method, old_value = get_method() s = kodi.translate(41137).format(object_name, old_value, metadata_name) new_value = kodi.dialog_numeric(s, old_value) - if new_value is None: return False + if new_value is None: + return False if old_value == new_value: kodi.notify(kodi.translate(40987).format(object_name, metadata_name)) @@ -71,6 +75,7 @@ def edit_field_by_int(obj_instance: MetaDataItemABC, metadata_name, get_method, kodi.notify(kodi.translate(40986).format(object_name, metadata_name, new_value)) return True + # # The values of str_list are supposed to be unique. # @@ -78,7 +83,7 @@ def edit_field_by_int(obj_instance: MetaDataItemABC, metadata_name, get_method, # edit_field_by_list('Launcher', 'Platform', AEL_platform_list, # launcher.get_platform, launcher.set_platform) # -def edit_field_by_list(obj_instance: MetaDataItemABC, metadata_name:str, str_list: list, get_method, set_method) -> bool: +def edit_field_by_list(obj_instance: EntityABC, metadata_name: str, str_list: list, get_method, set_method) -> bool: object_name = obj_instance.get_object_name() old_value = get_method() if old_value in str_list: @@ -98,6 +103,7 @@ def edit_field_by_list(obj_instance: MetaDataItemABC, metadata_name:str, str_lis kodi.notify(kodi.translate(40986).format(object_name, metadata_name, new_value)) return True + # # Rating 'Not set' is stored as an empty string. # Rating from 0 to 10 is stored as a string, '0', '1', ..., '10' @@ -128,8 +134,9 @@ def edit_rating(obj_instance: MetaDataItemABC, get_method, set_method): else: preselected_value = int(current_rating_str) + 1 sel_value = kodi.ListDialog().select(kodi.translate(41079).format(object_name), - options_list, preselect_idx = preselected_value) - if sel_value is None: return + options_list, preselect_idx=preselected_value) + if sel_value is None: + return if sel_value == preselected_value: kodi.notify(kodi.translate(40988).format(object_name)) return False @@ -146,8 +153,9 @@ def edit_rating(obj_instance: MetaDataItemABC, get_method, set_method): kodi.notify(kodi.translate(40989).format(object_name, current_rating_str)) return True + # -# Reads a text file with category/launcher plot. +# Reads a text file with category/launcher plot. # Checks file size to avoid importing binary files! # def import_TXT_file(text_file: io.FileName): @@ -158,7 +166,8 @@ def import_TXT_file(text_file: io.FileName): logger.debug('import_TXT_file() File size is {0}'.format(file_size)) if file_size > 16384: ret = kodi.dialog_yesno(kodi.translate(41070).format(text_file.getPath(), file_size)) - if not ret: return '' + if not ret: + return '' # Import file logger.debug('import_TXT_file() Importing description from "{0}"'.format(text_file.getPath())) @@ -166,9 +175,11 @@ def import_TXT_file(text_file: io.FileName): return file_data + SCRAPE_CMD = 'SCRAPE_ROM_ASSETS' -def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) -> str: + +def edit_object_assets(obj_instance: MetaDataItemABC, preselected_asset=None) -> str: logger.debug('edit_object_assets() obj_instance {0}'.format(obj_instance.__class__.__name__)) logger.debug('edit_object_assets() preselected_asset {0}'.format(preselected_asset if preselected_asset is not None else 'NONE')) @@ -185,7 +196,7 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - # >> setArt('icon') is the asset picture. label1_str = kodi.translate(42003).format(asset_info_obj.name) label2_stt = asset_fname_str if asset_fname_str else 'Not set' - list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_stt) + list_item = xbmcgui.ListItem(label=label1_str, label2=label2_stt) if asset_fname_str: item_path = io.FileName(asset_fname_str) if item_path.isVideoFile(): @@ -196,7 +207,7 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - item_img = asset_fname_str else: item_img = 'DefaultAddonNone.png' - list_item.setArt({'icon' : item_img}) + list_item.setArt({'icon': item_img}) # --- Append to list of ListItems --- options[asset_info_obj.id] = list_item @@ -207,7 +218,7 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - # --- Customize function for each object type --- dialog_title_str = kodi.translate(41076). format(obj_instance.get_object_name()) dialog = kodi.OrdDictionaryDialog() - selected_option = dialog.select(dialog_title_str, options, preselect = preselected_asset, use_details = True) + selected_option = dialog.select(dialog_title_str, options, preselect=preselected_asset, use_details=True) if selected_option is None: # >> Return to parent menu. @@ -216,7 +227,8 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - logger.debug('edit_object_assets() select() returned {0}'.format(selected_option)) return selected_option - + + # # Edit category/collection/launcher/ROM asset. # asset_info is a AssetInfo() object instance. @@ -229,43 +241,36 @@ def edit_object_assets(obj_instance:MetaDataItemABC, preselected_asset = None) - # Command The cmd that was executed. (SCRAPE_ASSET cmd will not be executed directly) # None No changes were made. No necessary to refresh container # -def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: - # --- Get asset object information --- - assets_directory = obj_instance.get_assets_root_path() - if not assets_directory: - if kodi.dialog_yesno(kodi.translate(41047)): - path_str = kodi.dialog_get_directory(kodi.translate(41138).format(obj_instance.get_name())) - assets_directory = io.FileName(path_str, True) - obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) - else: - if obj_instance.get_assets_kind() == constants.KIND_ASSET_CATEGORY: - assets_directory = io.FileName(settings.getSetting('categories_asset_dir'), isdir = True) - obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) - elif obj_instance.get_assets_kind() == constants.KIND_ASSET_COLLECTION: - assets_directory = io.FileName(settings.getSetting('collections_asset_dir'), isdir = True) - obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) - elif obj_instance.get_assets_kind() == constants.KIND_ASSET_LAUNCHER: - assets_directory = io.FileName(settings.getSetting('launchers_asset_dir'), isdir = True) - obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) - elif obj_instance.get_assets_kind() == constants.KIND_ASSET_ROM: - assets_directory = io.FileName(settings.getSetting('launchers_asset_dir'), isdir = True) - obj_instance.set_assets_root_path(assets_directory, None, create_default_subdirectories=True) - else: - kodi.dialog_OK(kodi.translate(41140).format(obj_instance.get_assets_kind())) - return None - - asset_type_directory = obj_instance.get_asset_path(asset_info, False) +def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo, assets_directory: io.FileName = None) -> str: + addon_assets_directory = None + if obj_instance.get_type() == constants.OBJ_CATEGORY: + addon_assets_directory = settings.getSettingAsFilePath('categories_asset_dir', isdir=True, + fallback=globals.g_PATHS.DEFAULT_CAT_ASSET_DIR) + elif obj_instance.get_type() == constants.OBJ_ROMCOLLECTION: + addon_assets_directory = settings.getSettingAsFilePath('collections_asset_dir', isdir=True, + fallback=globals.g_PATHS.DEFAULT_COL_ASSET_DIR) + elif obj_instance.get_type() == constants.OBJ_ROM: + addon_assets_directory = settings.getSettingAsFilePath('launchers_asset_dir', isdir=True, + fallback=globals.g_PATHS.DEFAULT_ROM_ASSET_DIR) + else: + kodi.dialog_OK(kodi.translate(41140).format(obj_instance.get_type())) + return None + + if not assets_directory or not assets_directory.exists(): + if not addon_assets_directory.exists(): + addon_assets_directory.makedirs() + assets_directory = addon_assets_directory + + asset_type_directory = obj_instance.get_asset_path(asset_info) + if not asset_type_directory: + asset_type_directory = assets_directory.pjoin(asset_info.plural.lower(), isdir=True) logger.info(f'edit_asset() Editing {obj_instance.get_object_name()} {asset_info.name}') logger.info(f'edit_asset() Object ID {obj_instance.get_id()}') - logger.debug(f'edit_asset() assets_directory "{assets_directory.getPath()}"') + logger.debug(f'edit_asset() assets_directory "{assets_directory.getPath() if asset_type_directory else "None"}"') logger.debug(f'edit_asset() asset_type_directory "{asset_type_directory.getPath() if asset_type_directory else "None"}"') - - if not assets_directory.exists(): - logger.error(f'Directory not found "{assets_directory.getPath()}"') - kodi.dialog_OK(kodi.translate(41139)) - return None - + logger.debug(f'edit_asset() addon_assets_directory "{addon_assets_directory.getPath()}"') + dialog_title = kodi.translate(41074).format(obj_instance.get_name(), asset_info.name) # --- Show image editing options --- @@ -273,7 +278,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: options['LINK_LOCAL'] = kodi.translate(42004).format(asset_info.name) options['IMPORT_LOCAL'] = kodi.translate(42005).format(asset_info.name) options['UNSET'] = kodi.translate(42006) - if obj_instance.get_assets_kind() == constants.KIND_ASSET_ROM: + if obj_instance.get_type() == constants.OBJ_ROM: options['SCRAPE_ASSET'] = kodi.translate(42007).format(asset_info.name) selected_option = kodi.OrdDictionaryDialog().select(dialog_title, options) @@ -292,9 +297,9 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: if current_image_file is None: current_image_dir = obj_instance.get_asset_path(asset_info) if current_image_dir is None or not assets_directory.exists(): - current_image_dir = obj_instance.get_assets_root_path() - else: - current_image_dir = io.FileName(current_image_file.getDir(), isdir = True) + current_image_dir = assets_directory + else: + current_image_dir = io.FileName(current_image_file.getDir(), isdir=True) if current_image_dir is None: current_image_dir = io.FileName('/') @@ -335,7 +340,7 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: current_image_dir = asset_type_directory if not current_image_dir: logger.info("No local asset type path configured. Reverting to root.") - current_image_dir = obj_instance.get_assets_root_path() + current_image_dir = assets_directory title_str = kodi.translate(41141).format(obj_instance.get_object_name(), kodi.translate(asset_info.name_id)) ext_list = asset_info.exts_dialog @@ -431,12 +436,13 @@ def edit_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> str: return selected_option + # -# Generic function to edit the Object default assets for +# Generic function to edit the Object default assets for # icon/fanart/banner/poster/clearlogo context submenu. # Argument obj is an object instance of class Category, CollectionLauncher, etc. # -def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_id = None) -> AssetInfo: +def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_id=None) -> AssetInfo: logger.debug('edit_object_default_assets() obj {0}'.format(obj_instance.__class__.__name__)) logger.debug('edit_object_default_assets() preselected_asset_id {0}'.format(preselected_asset_id)) @@ -456,10 +462,10 @@ def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_ mapped_asset_info = obj_instance.get_asset_mapping(default_asset_info) mapped_asset_str = obj_instance.get_asset_str(mapped_asset_info) label1_str = kodi.translate(42055).format( - kodi.translate(default_asset_info.name_id), + kodi.translate(default_asset_info.name_id), kodi.translate(mapped_asset_info.name_id)) label2_str = mapped_asset_str - list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_str) + list_item = xbmcgui.ListItem(label=label1_str, label2=label2_str) if mapped_asset_str: item_path = io.FileName(mapped_asset_str) if item_path.isVideoFile(): @@ -470,7 +476,7 @@ def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_ item_img = mapped_asset_str else: item_img = 'DefaultAddonNone.png' - list_item.setArt({'icon' : item_img}) + list_item.setArt({'icon': item_img}) # --- Append to list of ListItems --- list_items.append(list_item) asset_info_list.append(default_asset_info) @@ -479,8 +485,8 @@ def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_ pre_select_idx = counter counter += 1 - selected_option = xbmcgui.Dialog().select( - dialog_title_str, list = list_items, useDetails = True, preselect = pre_select_idx) + selected_option = xbmcgui.Dialog().select(dialog_title_str, list=list_items, useDetails=True, + preselect=pre_select_idx) logger.debug(f'edit_object_default_assets() Main select() returned {selected_option}') if selected_option < 0: # >> Return to parent menu. @@ -490,9 +496,10 @@ def edit_object_default_assets(obj_instance: MetaDataItemABC, preselected_asset_ # >> Execute edit default asset submenu. Then, execute recursively this submenu again. # >> The menu dialog is instantiated again so it reflects the changes just edited. logger.debug('edit_object_default_assets() Executing mappable asset select() dialog.') - selected_asset_info:AssetInfo = asset_info_list[selected_option] + selected_asset_info: AssetInfo = asset_info_list[selected_option] logger.debug(f'edit_object_default_assets() Main selected {selected_asset_info.name}.') - return selected_asset_info + return selected_asset_info + def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> bool: selectable_asset_ids = obj_instance.get_asset_ids_list() @@ -509,7 +516,7 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> mapped_asset_str = obj_instance.get_asset_str(mappable_asset_info) label1_str = mappable_asset_info.name label2_str = mapped_asset_str if mapped_asset_str else kodi.translate(42001) - list_item = xbmcgui.ListItem(label = label1_str, label2 = label2_str) + list_item = xbmcgui.ListItem(label=label1_str, label2=label2_str) if mapped_asset_str: item_path = io.FileName(mapped_asset_str) if item_path.isVideoFile(): @@ -520,7 +527,7 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> item_img = mapped_asset_str else: item_img = 'DefaultAddonNone.png' - list_item.setArt({'icon' : item_img}) + list_item.setArt({'icon': item_img}) # --- Append to list of ListItems --- list_items.append(list_item) asset_info_list.append(mappable_asset_info) @@ -529,8 +536,8 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> counter += 1 dialog_title_str = kodi.translate(41078).format(obj_instance.get_object_name(), asset_info.name) - secondary_selected_option = xbmcgui.Dialog().select( - dialog_title_str, list = list_items, useDetails = True, preselect = secondary_pre_select_idx) + secondary_selected_option = xbmcgui.Dialog().select(dialog_title_str, list=list_items, useDetails=True, + preselect=secondary_pre_select_idx) logger.debug('edit_default_asset() Mapable select() returned {0}'.format(secondary_selected_option)) if secondary_selected_option < 0: # >> Return to parent menu. @@ -543,4 +550,4 @@ def edit_default_asset(obj_instance: MetaDataItemABC, asset_info: AssetInfo) -> kodi.notify(kodi.translate(40983).format( obj_instance.get_object_name(), asset_info.name, new_selected_asset_info.name )) - return True \ No newline at end of file + return True diff --git a/resources/lib/globals.py b/resources/lib/globals.py index 8d219fa8..01b6e917 100644 --- a/resources/lib/globals.py +++ b/resources/lib/globals.py @@ -75,6 +75,7 @@ def __init__(self, addon_id): self.DEFAULT_FAV_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-favourites') # --- Rendered views (normal and virtuals/generated) --- + self.SOURCES_VIEW_PATH = self.ADDON_DATA_DIR.pjoin('sources.json') self.GENERATED_VIEWS_DIR = self.ADDON_DATA_DIR.pjoin('db_generated_views') self.VIEWS_DIR = self.ADDON_DATA_DIR.pjoin('db_views') @@ -99,8 +100,8 @@ def build(self): self.DEFAULT_CAT_ASSET_DIR.makedirs() if not self.DEFAULT_COL_ASSET_DIR.exists(): self.DEFAULT_COL_ASSET_DIR.makedirs() - if not self.DEFAULT_LAUN_ASSET_DIR.exists(): - self.DEFAULT_LAUN_ASSET_DIR.makedirs() + if not self.DEFAULT_ROM_ASSET_DIR.exists(): + self.DEFAULT_ROM_ASSET_DIR.makedirs() if not self.DEFAULT_FAV_ASSET_DIR.exists(): self.DEFAULT_FAV_ASSET_DIR.makedirs() @@ -111,12 +112,11 @@ def build(self): return self - router: routing.Plugin = routing.Plugin() g_PATHS: AKL_Paths WEBSERVER_HOST = '127.0.0.1' -WEBSERVER_PORT = 5738 +WEBSERVER_PORT = 57300 # diff --git a/resources/lib/queries.py b/resources/lib/queries.py index b6bda8c8..c5f320a8 100644 --- a/resources/lib/queries.py +++ b/resources/lib/queries.py @@ -4,14 +4,18 @@ AKL_SELECT_MIGRATIONS = "SELECT * FROM akl_migrations" # Shared Queries -INSERT_METADATA = "INSERT INTO metadata (id,year,genre,developer,rating,plot,extra,assets_path,finished) VALUES (?,?,?,?,?,?,?,?,?)" +INSERT_METADATA = "INSERT INTO metadata (id,year,genre,developer,rating,plot,extra,finished) VALUES (?,?,?,?,?,?,?,?)" INSERT_ASSET = "INSERT INTO assets (id, filepath, asset_type) VALUES (?,?,?)" INSERT_ASSET_PATH = "INSERT INTO assetpaths (id, path, asset_type) VALUES (?,?,?)" -UPDATE_METADATA = "UPDATE metadata SET year=?, genre=?, developer=?, rating=?, plot=?, extra=?, assets_path=?, finished=? WHERE id=?" +UPDATE_METADATA = "UPDATE metadata SET year=?, genre=?, developer=?, rating=?, plot=?, extra=?, finished=? WHERE id=?" UPDATE_ASSET = "UPDATE assets SET filepath = ?, asset_type = ? WHERE id = ?" UPDATE_ASSET_PATH = "UPDATE assetpaths SET path = ?, asset_type = ? WHERE id = ?" -SELECT_ITEM_ASSET_MAPPINGS = "SELECT am.* FROM assetmappings AS am INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id WHERE mm.metadata_id = ?" +SELECT_ITEM_ASSET_MAPPINGS = """ + SELECT am.* FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + WHERE mm.metadata_id = ? +""" INSERT_ASSET_MAPPING = "INSERT INTO assetmappings (id, mapped_asset_type, to_asset_type) VALUES (?,?,?)" UPDATE_ASSET_MAPPING = "UPDATE assetmappings SET mapped_asset_type = ?, to_asset_type = ? WHERE id = ?" INSERT_MAPPING_WITH_METADATA = "INSERT INTO metadata_assetmappings (metadata_id, assetmapping_id) VALUES (?,?)" @@ -23,10 +27,10 @@ SELECT_CATEGORIES = "SELECT * FROM vw_categories ORDER BY m_name" SELECT_ALL_CATEGORY_ASSETS = "SELECT * FROM vw_category_assets" SELECT_ALL_CATEGORY_ASSET_MAPPINGS = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN categories AS c ON mm.metadata_id = c.metadata_id - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN categories AS c ON mm.metadata_id = c.metadata_id +""" SELECT_ROOT_CATEGORIES = "SELECT * FROM vw_categories WHERE parent_id IS NULL ORDER BY m_name" SELECT_ROOT_CATEGORY_ASSETS = "SELECT * FROM vw_category_assets WHERE parent_id IS NULL" SELECT_ROOT_CATEGORY_ASSET_MAPPINGS = """ @@ -38,20 +42,20 @@ SELECT_CATEGORIES_BY_PARENT = "SELECT * FROM vw_categories WHERE parent_id = ? ORDER BY m_name" SELECT_CATEGORY_ASSETS_BY_PARENT = "SELECT * FROM vw_category_assets WHERE parent_id = ?" SELECT_CATEGORY_ASSET_MAPPINGS_BY_PARENT = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN categories AS c ON mm.metadata_id = c.metadata_id - WHERE c.parent_id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN categories AS c ON mm.metadata_id = c.metadata_id + WHERE c.parent_id = ? + """ SELECT_CATEGORIES_BY_ROM = "SELECT c.* FROM vw_categories AS c INNER JOIN roms_in_category AS rc ON rc.category_id = c.id WHERE rc.rom_id = ?" SELECT_CATEGORIES_ASSETS_BY_ROM = "SELECT ca.* FROM vw_category_assets AS ca INNER JOIN roms_in_category AS rc ON rc.category_id = ca.category_id WHERE rc.rom_id = ?" SELECT_CATEGORY_ASSET_MAPPINGS_BY_ROM = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN categories AS c ON mm.metadata_id = c.metadata_id - INNER JOIN roms_in_category AS rc ON rc.category_id = c.id WHERE rc.rom_id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN categories AS c ON mm.metadata_id = c.metadata_id + INNER JOIN roms_in_category AS rc ON rc.category_id = c.id WHERE rc.rom_id = ? + """ INSERT_CATEGORY = "INSERT INTO categories (id,name,parent_id,metadata_id) VALUES (?,?,?,?)" UPDATE_CATEGORY = "UPDATE categories SET name=? WHERE id =?" @@ -73,15 +77,23 @@ SELECT_ROOT_ROMCOLLECTIONS = "SELECT * FROM vw_romcollections WHERE parent_id IS NULL ORDER BY m_name" SELECT_ROMCOLLECTIONS_BY_PARENT = "SELECT * FROM vw_romcollections WHERE parent_id = ? ORDER BY m_name" SELECT_ROMCOLLECTIONS_BY_ROM = "SELECT rs.* FROM vw_romcollections AS rs INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rs.id WHERE rr.rom_id = ?" +SELECT_ROMCOLLECTIONS_BY_SOURCE = """ + SELECT rs.* FROM vw_romcollections AS rs + WHERE rs.id IN ( + SELECT rr.romcollection_id FROM roms_in_romcollection AS rr + INNER JOIN roms ON rr.rom_id = r.id + WHERE r.scanned_by_id = ? + ) +""" -SELECT_VCOLLECTION_TITLES = "SELECT DISTINCT(SUBSTR(UPPER(m_name), 1,1)) AS option_value FROM vw_roms" -SELECT_VCOLLECTION_GENRES = "SELECT DISTINCT(m_genre) AS option_value FROM vw_roms" -SELECT_VCOLLECTION_DEVELOPER = "SELECT DISTINCT(m_developer) AS option_value FROM vw_roms" -SELECT_VCOLLECTION_ESRB = "SELECT DISTINCT(m_esrb) AS option_value FROM vw_roms" -SELECT_VCOLLECTION_PEGI = "SELECT DISTINCT(m_pegi) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_TITLES = "SELECT DISTINCT(SUBSTR(UPPER(m_name), 1,1)) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_GENRES = "SELECT DISTINCT(m_genre) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_DEVELOPER = "SELECT DISTINCT(m_developer) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_ESRB = "SELECT DISTINCT(esrb) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_PEGI = "SELECT DISTINCT(pegi) AS option_value FROM vw_roms" SELECT_VCOLLECTION_YEAR = "SELECT DISTINCT(m_year) AS option_value FROM vw_roms" -SELECT_VCOLLECTION_NPLAYERS = "SELECT DISTINCT(m_nplayers) AS option_value FROM vw_roms" -SELECT_VCOLLECTION_RATING = "SELECT DISTINCT(m_rating) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_NPLAYERS = "SELECT DISTINCT(nplayers) AS option_value FROM vw_roms" +SELECT_VCOLLECTION_RATING = "SELECT DISTINCT(m_rating) AS option_value FROM vw_roms" INSERT_ROMCOLLECTION = """ INSERT INTO romcollections (id,name,parent_id,metadata_id,platform,box_size) @@ -95,42 +107,59 @@ SELECT_ROOT_ROMCOLLECTION_ASSETS = "SELECT * FROM vw_romcollection_assets WHERE parent_id IS NULL" SELECT_ROMCOLLECTIONS_ASSETS_BY_PARENT = "SELECT * FROM vw_romcollection_assets WHERE parent_id = ?" SELECT_ROMCOLLECTION_ASSETS_BY_ROM = "SELECT ra.* FROM vw_romcollection_assets AS ra INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = ra.romcollection_id WHERE rr.rom_id = ?" +SELECT_ROMCOLLECTION_ASSETS_BY_SOURCE = """ + SELECT ra.* FROM vw_romcollection_assets AS ra + WHERE ra.romcollection_id IN ( + SELECT DISTINCT(rr.romcollection_id) FROM roms_in_romcollection AS rr + INNER JOIN roms ON rr.rom_id = r.id + WHERE r.scanned_by_id = ? + ) +""" + SELECT_ROMCOLLECTION_ASSETS = "SELECT * FROM vw_romcollection_assets" -SELECT_ROMCOLLECTION_ASSET_PATHS = "SELECT * FROM vw_romcollection_asset_paths WHERE romcollection_id = ?" -SELECT_ROMCOLLECTION_ASSETS_PATHS_BY_ROM = "SELECT rap.* FROM vw_romcollection_asset_paths AS rap INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rap.romcollection_id WHERE rr.rom_id = ?" SELECT_ROMCOLLECTION_ASSET_MAPPINGS = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id + """ SELECT_ROOT_ROMCOLLECTION_ASSET_MAPPINGS = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id - WHERE parent_id IS NULL - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id + WHERE parent_id IS NULL + """ SELECT_ROMCOLLECTION_ASSET_MAPPINGS_BY_PARENT = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id - WHERE parent_id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id + WHERE parent_id = ? + """ SELECT_ROMCOLLECTION_ASSET_MAPPINGS_BY_ROM = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id - INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rc.id WHERE rr.rom_id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id + INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rc.id WHERE rr.rom_id = ? + """ +SELECT_ROMCOLLECTION_ASSET_MAPPINGS_BY_SOURCE = """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON mm.metadata_id = rc.metadata_id + WHERE rc.id IN ( + SELECT DISTINCT(rr.romcollection)_id FROM roms_in_romcollection AS rr + INNER JOIN roms ON rr.rom_id = r.id + WHERE r.scanned_by_id = ? + ) +""" SELECT_SPECIFIC_ROMCOLLECTION_ROM_ASSET_MAPPINGS = """ - SELECT am.*, rm.romcollection_id FROM assetmappings AS am - INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id - AND rm.romcollection_id = ? - """ + SELECT am.*, rm.romcollection_id FROM assetmappings AS am + INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id + AND rm.romcollection_id = ? + """ SELECT_ROMCOLLECTION_ROM_ASSET_MAPPINGS = """ - SELECT am.*, rm.romcollection_id FROM assetmappings AS am - INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id - """ + SELECT am.*, rm.romcollection_id FROM assetmappings AS am + INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id + """ SELECT_ROOT_ROMCOLLECTION_ROM_ASSET_MAPPINGS = """ SELECT am.*, rm.romcollection_id FROM assetmappings AS am INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id @@ -138,78 +167,142 @@ WHERE parent_id IS NULL """ SELECT_ROMCOLLECTION_ROM_ASSET_MAPPINGS_BY_PARENT = """ - SELECT am.*, rm.romcollection_id FROM assetmappings AS am - INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id - WHERE parent_id = ? - """ + SELECT am.*, rm.romcollection_id FROM assetmappings AS am + INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id + WHERE parent_id = ? + """ SELECT_ROMCOLLECTION_ROM_ASSET_MAPPINGS_BY_ROM = """ - SELECT am.*, rm.romcollection_id FROM assetmappings AS am - INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id - INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id - INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rc.id WHERE rr.rom_id = ? - """ + SELECT am.*, rm.romcollection_id FROM assetmappings AS am + INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id + INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rc.id WHERE rr.rom_id = ? + """ +SELECT_ROMCOLLECTION_ROM_ASSET_MAPPINGS_BY_SOURCE = """ + SELECT am.*, rm.romcollection_id FROM assetmappings AS am + INNER JOIN romcollection_roms_assetmappings AS rm ON rm.assetmapping_id = am.id + INNER JOIN romcollections AS rc ON rm.romcollection_id = rc.id + WHERE rc.id IN ( + SELECT DISTINCT(rr.romcollection)_id FROM roms_in_romcollection AS rr + INNER JOIN roms ON rr.rom_id = r.id + WHERE r.scanned_by_id = ? + ) +""" INSERT_ROMCOLLECTION_ASSET = "INSERT INTO romcollection_assets (romcollection_id, asset_id) VALUES (?, ?)" -INSERT_ROMCOLLECTION_ASSET_PATH = "INSERT INTO romcollection_assetpaths (romcollection_id, assetpaths_id) VALUES (?, ?)" INSERT_ROMCOLLECTION_ROM_ASSET_MAPPING = "INSERT INTO romcollection_roms_assetmappings (romcollection_id, assetmapping_id) VALUES (?,?)" INSERT_ROM_IN_ROMCOLLECTION = "INSERT INTO roms_in_romcollection (rom_id, romcollection_id) VALUES (?,?)" REMOVE_ROM_FROM_ROMCOLLECTION = "DELETE FROM roms_in_romcollection WHERE rom_id = ? AND romcollection_id = ?" -REMOVE_ROMS_FROM_ROMCOLLECTION = "DELETE FROM roms_in_romcollection WHERE romcollection_id = ?" +REMOVE_ROMS_FROM_ROMCOLLECTION = "DELETE FROM roms_in_romcollection WHERE romcollection_id = ?" -SELECT_ROMCOLLECTION_LAUNCHERS = "SELECT * FROM vw_romcollection_launchers WHERE romcollection_id = ?" -INSERT_ROMCOLLECTION_LAUNCHER = "INSERT INTO romcollection_launchers (id, romcollection_id, akl_addon_id, settings, is_default) VALUES (?,?,?,?,?)" -UPDATE_ROMCOLLECTION_LAUNCHER = "UPDATE romcollection_launchers SET settings = ?, is_default = ? WHERE id = ?" -DELETE_ROMCOLLECTION_LAUNCHERS = "DELETE FROM romcollection_launchers WHERE romcollection_id = ?" -DELETE_ROMCOLLECTION_LAUNCHER = "DELETE FROM romcollection_launchers WHERE romcollection_id = ? AND id = ?" +SELECT_ROMCOLLECTION_LAUNCHERS_BY_ROM = """ + SELECT rl.* FROM vw_romcollection_launchers AS rl + INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rl.romcollection_id + WHERE rr.rom_id = ? + """ +SELECT_ROMCOLLECTION_LAUNCHERS_BY_SOURCE = """ + SELECT rl.* FROM vw_romcollection_launchers AS rl + WHERE rl.romcollection_id IN ( + SELECT DISTINCT(rr.romcollection_id) FROM roms_in_romcollection AS rr + INNER JOIN roms ON rr.rom_id = r.id + WHERE r.scanned_by_id = ? + ) + """ -SELECT_ROMCOLLECTION_SCANNERS = "SELECT * FROM vw_romcollection_scanners WHERE romcollection_id = ?" -INSERT_ROMCOLLECTION_SCANNER = "INSERT INTO romcollection_scanners (id, romcollection_id, akl_addon_id, settings) VALUES (?,?,?,?)" -UPDATE_ROMCOLLECTION_SCANNER = "UPDATE romcollection_scanners SET settings = ? WHERE id = ?" -DELETE_ROMCOLLECTION_SCANNER = "DELETE FROM romcollection_scanners WHERE romcollection_id = ? AND id = ?" - -SELECT_ROMCOLLECTION_LAUNCHERS_BY_ROM = "SELECT rl.* FROM vw_romcollection_launchers AS rl INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rl.romcollection_id WHERE rr.rom_id = ?" -SELECT_ROMCOLLECTION_SCANNERS_BY_ROM = "SELECT rs.* FROM vw_romcollection_scanners AS rs INNER JOIN roms_in_romcollection AS rr ON rr.romcollection_id = rs.romcollection_id WHERE rr.rom_id = ?" +SELECT_IMPORT_RULES_BY_COLLECTION = """ + SELECT r.*, rs.*, s.name AS source_name + FROM import_rule AS r + RIGHT JOIN collection_source_ruleset AS rs + ON r.ruleset_id = rs.ruleset_id + INNER JOIN sources AS s + ON rs.source_id = s.id + WHERE rs.collection_id = ? + """ +SELECT_IMPORT_RULE_BY_COLLECTION = """ + SELECT r.*, rs.*, s.name AS source_name + FROM import_rule AS r + RIGHT JOIN collection_source_ruleset AS rs + ON r.ruleset_id = rs.ruleset_id + INNER JOIN sources AS s + ON rs.source_id = s.id + WHERE rs.collection_id = ? AND rs.ruleset_id = ? + """ +INSERT_RULESET_FOR_ROMCOLLECTION = """ + INSERT INTO collection_source_ruleset (ruleset_id, source_id, collection_id, set_operator) VALUES (?,?,?,?) + """ +UPDATE_RULESET_FOR_ROMCOLLECTION = """ + UPDATE collection_source_ruleset SET source_id=?, collection_id=?, set_operator=? WHERE ruleset_id=? + """ +INSERT_RULE = "INSERT INTO import_rule (rule_id,ruleset_id,property,value,operator) VALUES (?,?,?,?,?)" +UPDATE_RULE = "UPDATE import_rule SET property=?, value=?, operator=? WHERE rule_id=?" +DELETE_RULE_FROM_RULESET = "DELETE FROM import_rule WHERE rule_id = ? AND ruleset_id = ?" +DELETE_ALL_RULES_FROM_RULESET = "DELETE FROM import_rule WHERE ruleset_id = ?" # # ROMsRepository -> ROMs from SQLite DB -# +# SELECT_ROM = "SELECT * FROM vw_roms WHERE id = ?" SELECT_ROM_ASSETS = "SELECT * FROM vw_rom_assets WHERE rom_id = ?" SELECT_ROM_ASSETPATHS = "SELECT * FROM vw_rom_asset_paths WHERE rom_id = ?" SELECT_ROM_TAGS = "SELECT * FROM vw_rom_tags WHERE rom_id = ?" SELECT_ROM_ASSET_MAPPINGS = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id - AND r.id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id + AND r.id = ? + """ SELECT_ROMS_BY_SET = "SELECT r.* FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ?" SELECT_ROM_ASSETS_BY_SET = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = ra.rom_id AND rs.romcollection_id = ?" SELECT_ROM_ASSETPATHS_BY_SET = "SELECT rap.* FROM vw_rom_asset_paths AS rap INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = rap.rom_id AND rs.romcollection_id = ?" SELECT_ROM_TAGS_BY_SET = "SELECT rt.* FROM vw_rom_tags AS rt INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = rt.rom_id AND rs.romcollection_id = ?" SELECT_ROM_ASSET_MAPPINGS_BY_SET = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id - INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id - AND rs.romcollection_id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id + INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id + AND rs.romcollection_id = ? +""" SELECT_ROMS_BY_CATEGORY = "SELECT r.* FROM vw_roms AS r INNER JOIN roms_in_category AS rc ON rc.rom_id = r.id AND rc.category_id = ?" SELECT_ROM_ASSETS_BY_CATEGORY = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms_in_category AS rc ON rc.rom_id = ra.rom_id AND rc.category_id = ?" SELECT_ROM_ASSETPATHS_BY_CATEGORY = "SELECT rap.* FROM vw_rom_asset_paths AS rap INNER JOIN roms_in_category AS rc ON rc.rom_id = rap.rom_id AND rc.category_id = ?" SELECT_ROM_TAGS_BY_CATEGORY = "SELECT rt.* FROM vw_rom_tags AS rt INNER JOIN roms_in_category AS rc ON rc.rom_id = rt.rom_id AND rc.category_id = ?" SELECT_ROM_ASSET_MAPPINGS_BY_CATEGORY = """ - SELECT am.*, mm.metadata_id FROM assetmappings AS am - INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id - INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id - INNER JOIN roms_in_category AS rc ON rc.rom_id = r.id - AND rc.category_id = ? - """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id + INNER JOIN roms_in_category AS rc ON rc.rom_id = r.id + AND rc.category_id = ? +""" +SELECT_ROMS_BY_SOURCE = "SELECT r.* FROM vw_roms AS r WHERE r.scanned_by_id = ?" +SELECT_ROM_ASSETS_BY_SOURCE = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id AND r.scanned_by_id = ?" +SELECT_ROM_ASSETPATHS_BY_SOURCE = """ + SELECT rap.* FROM vw_rom_asset_paths AS rap INNER JOIN roms AS r ON r.id = rap.rom_id AND r.scanned_by_id = ? +""" +SELECT_ROM_TAGS_BY_SOURCE = "SELECT rt.* FROM vw_rom_tags AS rt INNER JOIN roms AS r ON r.id = rt.rom_id AND r.scanned_by_id = ?" +SELECT_ROM_ASSET_MAPPINGS_BY_SOURCE = """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id + AND r.scanned_by_id = ? +""" + +SELECT_STANDALONE_ROMS = "SELECT r.* FROM vw_roms AS r WHERE r.scanned_by_id = ''" +SELECT_STANDALONE_ROM_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id AND r.scanned_by_id = ''" +SELECT_STANDALONE_ROM_ASSETPATHS = """ + SELECT rap.* FROM vw_rom_asset_paths AS rap INNER JOIN roms AS r ON r.id = rap.rom_id AND r.scanned_by_id = '' +""" +SELECT_STANDALONE_ROM_TAGS = "SELECT rt.* FROM vw_rom_tags AS rt INNER JOIN roms AS r ON r.id = rt.rom_id AND r.scanned_by_id = ''" +SELECT_STANDALONE_ROM_ASSET_MAPPINGS = """ + SELECT am.*, mm.metadata_id FROM assetmappings AS am + INNER JOIN metadata_assetmappings AS mm ON mm.assetmapping_id = am.id + INNER JOIN roms AS r ON mm.metadata_id = r.metadata_id + AND r.scanned_by_id = '' +""" + SELECT_ROMS_BY_ROOT_CATEGORY = "SELECT r.* FROM vw_roms AS r INNER JOIN roms_in_category AS rc ON rc.rom_id = r.id AND rc.category_id IS NULL" SELECT_ROM_ASSETS_BY_ROOT_CATEGORY = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms_in_category AS rc ON rc.rom_id = ra.rom_id AND rc.category_id IS NULL" SELECT_ROM_ASSETPATHS_BY_ROOT_CATEGORY = "SELECT rap.* FROM vw_rom_asset_paths AS rap INNER JOIN roms_in_category AS rc ON rc.rom_id = rap.rom_id AND rc.category_id IS NULL" @@ -223,88 +316,153 @@ """ # Filter values -SELECT_GENRES_BY_COLLECTION = "SELECT DISTINCT(r.m_genre) AS genre FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY genre" -SELECT_YEARS_BY_COLLECTION = "SELECT DISTINCT(r.m_year) AS year FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY year" -SELECT_DEVELOPER_BY_COLLECTION = "SELECT DISTINCT(r.m_developer) AS developer FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY developer" -SELECT_RATING_BY_COLLECTION = "SELECT DISTINCT(r.m_rating) AS rating FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY rating" - -INSERT_ROM = """ - INSERT INTO roms ( - id, metadata_id, name, num_of_players, num_of_players_online, esrb_rating, pegi_rating, - platform, box_size, nointro_status, cloneof, rom_status, scanned_by_id) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - """ - -SELECT_MY_FAVOURITES = "SELECT * FROM vw_roms WHERE is_favourite = 1" -SELECT_RECENTLY_PLAYED_ROMS = "SELECT * FROM vw_roms WHERE last_launch_timestamp IS NOT NULL ORDER BY last_launch_timestamp DESC LIMIT 100" -SELECT_MOST_PLAYED_ROMS = "SELECT * FROM vw_roms WHERE launch_count > 0 ORDER BY launch_count DESC LIMIT 100" -SELECT_FAVOURITES_ROM_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id WHERE r.is_favourite = 1" -SELECT_RECENTLY_PLAYED_ROM_ASSETS = """ - SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id - WHERE r.last_launch_timestamp IS NOT NULL ORDER BY last_launch_timestamp DESC LIMIT 100 - """ -SELECT_MOST_PLAYED_ROM_ASSETS = """ - SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id - WHERE r.launch_count > 0 ORDER BY launch_count DESC LIMIT 100 - """ +SELECT_GENRES_BY_COLLECTION = "SELECT DISTINCT(r.m_genre) AS genre FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY genre" +SELECT_YEARS_BY_COLLECTION = "SELECT DISTINCT(r.m_year) AS year FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY year" +SELECT_DEVELOPER_BY_COLLECTION = "SELECT DISTINCT(r.m_developer) AS developer FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY developer" +SELECT_RATING_BY_COLLECTION = "SELECT DISTINCT(r.m_rating) AS rating FROM vw_roms AS r INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = r.id AND rs.romcollection_id = ? ORDER BY rating" + +INSERT_ROM = """ + INSERT INTO roms ( + id, metadata_id, name, num_of_players, num_of_players_online, esrb_rating, pegi_rating, + platform, box_size, nointro_status, cloneof, rom_status, scanned_by_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + """ + +SELECT_MY_FAVOURITES = "SELECT * FROM vw_roms WHERE is_favourite = 1" +SELECT_RECENTLY_PLAYED_ROMS = "SELECT * FROM vw_roms WHERE last_launch_timestamp IS NOT NULL ORDER BY last_launch_timestamp DESC LIMIT 100" +SELECT_MOST_PLAYED_ROMS = "SELECT * FROM vw_roms WHERE launch_count > 0 ORDER BY launch_count DESC LIMIT 100" +SELECT_FAVOURITES_ROM_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id WHERE r.is_favourite = 1" +SELECT_RECENTLY_PLAYED_ROM_ASSETS = """ + SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id + WHERE r.last_launch_timestamp IS NOT NULL ORDER BY last_launch_timestamp DESC LIMIT 100 + """ +SELECT_MOST_PLAYED_ROM_ASSETS = """ + SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN roms AS r ON r.id = ra.rom_id + WHERE r.launch_count > 0 ORDER BY launch_count DESC LIMIT 100 + """ SELECT_BY_TITLE = "SELECT * FROM vw_roms WHERE m_name LIKE ? || '%'" SELECT_BY_GENRE = "SELECT * FROM vw_roms WHERE m_genre = ?" SELECT_BY_DEVELOPER = "SELECT * FROM vw_roms WHERE m_developer = ?" SELECT_BY_YEAR = "SELECT * FROM vw_roms WHERE m_year = ?" -SELECT_BY_NPLAYERS = "SELECT * FROM vw_roms WHERE m_nplayers = ?" -SELECT_BY_ESRB = "SELECT * FROM vw_roms WHERE m_esrb = ?" -SELECT_BY_PEGI = "SELECT * FROM vw_roms WHERE m_pegi = ?" +SELECT_BY_NPLAYERS = "SELECT * FROM vw_roms WHERE nplayers = ?" +SELECT_BY_ESRB = "SELECT * FROM vw_roms WHERE esrb = ?" +SELECT_BY_PEGI = "SELECT * FROM vw_roms WHERE pegi = ?" SELECT_BY_RATING = "SELECT * FROM vw_roms WHERE m_rating = ?" -SELECT_BY_TITLE_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE UPPER(r.m_name) LIKE ? || '%'" -SELECT_BY_GENRE_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_genre = ?" -SELECT_BY_DEVELOPER_ASSETS= "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_developer = ?" -SELECT_BY_YEAR_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_year = ?" -SELECT_BY_NPLAYERS_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_nplayers = ?" -SELECT_BY_ESRB_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_esrb = ?" -SELECT_BY_PEGI_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_pegi = ?" -SELECT_BY_RATING_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_rating = ?" +SELECT_BY_TITLE_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE UPPER(r.m_name) LIKE ? || '%'" +SELECT_BY_GENRE_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_genre = ?" +SELECT_BY_DEVELOPER_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_developer = ?" +SELECT_BY_YEAR_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_year = ?" +SELECT_BY_NPLAYERS_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.nplayers = ?" +SELECT_BY_ESRB_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.esrb = ?" +SELECT_BY_PEGI_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.pegi = ?" +SELECT_BY_RATING_ASSETS = "SELECT ra.* FROM vw_rom_assets AS ra INNER JOIN vw_roms AS r ON r.id = ra.rom_id WHERE r.m_rating = ?" -INSERT_ROM_ASSET = "INSERT INTO rom_assets (rom_id, asset_id) VALUES (?, ?)" -INSERT_ROM_ASSET_PATH = "INSERT INTO rom_assetpaths (rom_id, assetpaths_id) VALUES (?, ?)" -INSERT_ROM_SCANNED_DATA = "INSERT INTO scanned_roms_data (rom_id, data_key, data_value) VALUES (?, ?, ?)" - -UPDATE_ROM = """ - UPDATE roms - SET name=?, num_of_players=?, num_of_players_online=?, esrb_rating=?, pegi_rating=?, platform=?, box_size=?, - nointro_status=?, cloneof=?, rom_status=?, launch_count=?, last_launch_timestamp=?, - is_favourite=?, scanned_by_id=? WHERE id =? - """ -DELETE_ROM = "DELETE FROM roms WHERE id = ?" +INSERT_ROM_ASSET = "INSERT INTO rom_assets (rom_id, asset_id) VALUES (?, ?)" +INSERT_ROM_ASSET_PATH = "INSERT INTO rom_assetpaths (rom_id, assetpaths_id) VALUES (?, ?)" +INSERT_ROM_SCANNED_DATA = "INSERT INTO scanned_roms_data (rom_id, data_key, data_value) VALUES (?, ?, ?)" + +UPDATE_ROM = """ + UPDATE roms + SET name=?, num_of_players=?, num_of_players_online=?, esrb_rating=?, pegi_rating=?, platform=?, box_size=?, + nointro_status=?, cloneof=?, rom_status=?, launch_count=?, last_launch_timestamp=?, + is_favourite=?, scanned_by_id=? WHERE id =? + """ +DELETE_ROM = "DELETE FROM roms WHERE id = ?" DELETE_ROMS_BY_COLLECTION = "DELETE FROM roms WHERE id IN (SELECT rc.rom_id FROM roms_in_romcollection AS rc WHERE rc.romcollection_id = ?)" SELECT_ROM_SCANNED_DATA = "SELECT s.* FROM scanned_roms_data AS s WHERE s.rom_id = ?" SELECT_ROM_SCANNED_DATA_BY_SET = "SELECT s.* FROM scanned_roms_data AS s INNER JOIN roms_in_romcollection AS rs ON rs.rom_id = s.rom_id AND rs.romcollection_id = ?" SELECT_ROM_SCANNED_DATA_BY_CATEGORY = "SELECT s.* FROM scanned_roms_data AS s INNER JOIN roms_in_category AS rc ON rc.rom_id = s.rom_id AND rc.category_id = ?" +SELECT_ROM_SCANNED_DATA_BY_SOURCE = "SELECT s.* FROM scanned_roms_data AS s INNER JOIN roms AS r ON r.id = s.rom_id AND r.scanned_by_id = ?" SELECT_ROM_SCANNED_DATA_BY_ROOT_CATEGORY = "SELECT s.* FROM scanned_roms_data AS s INNER JOIN roms_in_category AS rc ON rc.rom_id = s.rom_id AND rc.category_id IS NULL" +SELECT_STANDALONE_ROM_SCANNED_DATA = """ + SELECT s.* FROM scanned_roms_data AS s INNER JOIN roms AS r ON r.id = s.rom_id AND r.scanned_by_id = '' +""" DELETE_SCANNED_DATA = "DELETE FROM scanned_roms_data WHERE rom_id = ?" -SELECT_ROM_LAUNCHERS = "SELECT * FROM vw_rom_launchers WHERE rom_id = ?" -INSERT_ROM_LAUNCHER = "INSERT INTO rom_launchers (id, rom_id, akl_addon_id, settings, is_default) VALUES (?,?,?,?,?)" -UPDATE_ROM_LAUNCHER = "UPDATE rom_launchers SET settings = ?, is_default = ? WHERE id = ?" -DELETE_ROM_LAUNCHERS = "DELETE FROM rom_launchers WHERE rom_id = ?" -DELETE_ROM_LAUNCHER = "DELETE FROM rom_launchers WHERE rom_id = ? AND id = ?" - -SELECT_TAGS = "SELECT * FROM tags" -INSERT_TAG = "INSERT INTO tags (id, tag) VALUES (?,?)" -ADD_TAG_TO_ROM = "INSERT INTO metatags (metadata_id, tag_id) VALUES (?,?)" -DELETE_EXISTING_ROM_TAGS = "DELETE FROM metatags WHERE metadata_id = ?" -DELETE_TAG = "DELETE FROM tags WHERE id = ?" +SELECT_TAGS = "SELECT * FROM tags" +INSERT_TAG = "INSERT INTO tags (id, tag) VALUES (?,?)" +ADD_TAG_TO_ROM = "INSERT INTO metatags (metadata_id, tag_id) VALUES (?,?)" +DELETE_EXISTING_ROM_TAGS = "DELETE FROM metatags WHERE metadata_id = ?" +DELETE_TAG = "DELETE FROM tags WHERE id = ?" # # AelAddonRepository -> AKL Adoon objects from SQLite DB -# -SELECT_ADDON = "SELECT * FROM akl_addon WHERE id = ?" -SELECT_ADDON_BY_ADDON_ID = "SELECT * FROM akl_addon WHERE addon_id = ? AND addon_type = ?" -SELECT_ADDONS = "SELECT * FROM akl_addon" -SELECT_LAUNCHER_ADDONS = "SELECT * FROM akl_addon WHERE addon_type = 'LAUNCHER' ORDER BY name" -SELECT_SCANNER_ADDONS = "SELECT * FROM akl_addon WHERE addon_type = 'SCANNER' ORDER BY name" -SELECT_SCRAPER_ADDONS = "SELECT * FROM akl_addon WHERE addon_type = 'SCRAPER' ORDER BY name" -INSERT_ADDON = "INSERT INTO akl_addon(id, name, addon_id, version, addon_type, extra_settings) VALUES(?,?,?,?,?,?)" -UPDATE_ADDON = "UPDATE akl_addon SET name = ?, addon_id = ?, version = ?, addon_type = ?, extra_settings = ? WHERE id = ?" +# +SELECT_ADDON = "SELECT * FROM akl_addon WHERE id = ?" +SELECT_ADDON_BY_ADDON_ID = "SELECT * FROM akl_addon WHERE addon_id = ? AND addon_type = ?" +SELECT_ADDONS = "SELECT * FROM akl_addon" +SELECT_LAUNCHER_ADDONS = "SELECT * FROM akl_addon WHERE addon_type = 'LAUNCHER' ORDER BY name" +SELECT_SCANNER_ADDONS = "SELECT * FROM akl_addon WHERE addon_type = 'SCANNER' ORDER BY name" +SELECT_SCRAPER_ADDONS = "SELECT * FROM akl_addon WHERE addon_type = 'SCRAPER' ORDER BY name" +INSERT_ADDON = "INSERT INTO akl_addon(id, name, addon_id, version, addon_type, extra_settings) VALUES(?,?,?,?,?,?)" +UPDATE_ADDON = "UPDATE akl_addon SET name = ?, addon_id = ?, version = ?, addon_type = ?, extra_settings = ? WHERE id = ?" + +# Source +SELECT_SOURCE = "SELECT * FROM vw_sources WHERE id = ?" +SELECT_SOURCES = "SELECT * FROM vw_sources" +SELECT_ROMCOLLECTION_IDS_BY_SOURCE = "SELECT collection_id FROM collection_source_ruleset WHERE source_id = ?" +SELECT_SOURCES_BY_ROMCOLLECTION = """ + SELECT s.* FROM vw_sources AS s WHERE s.id IN ( + SELECT DISTINCT(r.scanned_by_id) + FROM roms AS r + INNER JOIN roms_in_romcollection AS rrs ON r.id = rrs.rom_id AND rrs.romcollection_id = ?) + """ + +SELECT_SOURCE_ASSET_PATHS = "SELECT * FROM vw_source_asset_paths WHERE source_id = ?" + +INSERT_SOURCE = """ + INSERT INTO sources (id,name,platform,box_size,assets_path,last_scan_timestamp,settings,akl_addon_id) + VALUES (?,?,?,?,?,?,?,?) + """ +UPDATE_SOURCE = "UPDATE sources SET name=?, platform=?, box_size=?, assets_path=?, last_scan_timestamp=?, settings=? WHERE id =?" +DELETE_SOURCE = "DELETE FROM sources WHERE id = ?" + +INSERT_SOURCE_ASSET_PATH = "INSERT INTO source_assetpaths (source_id, assetpaths_id) VALUES (?, ?)" +REMOVE_ROMS_FROM_SOURCE = "DELETE FROM roms WHERE scanned_by_id = ?" + +# Launchers +SELECT_SOURCE_LAUNCHERS = "SELECT * FROM vw_source_launchers WHERE source_id = ?" +INSERT_SOURCE_LAUNCHER = "INSERT INTO source_launchers (launcher_id, source_id, is_default) VALUES (?,?,?)" +UPDATE_SOURCE_LAUNCHER = "UPDATE source_launchers SET is_default = ? WHERE source_id = ? AND launcher_id = ?" +DELETE_SOURCE_LAUNCHERS = "DELETE FROM source_launchers WHERE source_id = ?" +DELETE_SOURCE_LAUNCHER = "DELETE FROM source_launchers WHERE source_id = ? AND launcher_id = ?" + +SELECT_ROM_LAUNCHERS = "SELECT * FROM vw_rom_launchers WHERE rom_id = ?" +INSERT_ROM_LAUNCHER = "INSERT INTO rom_launchers (launcher_id, rom_id, is_default) VALUES (?,?,?)" +UPDATE_ROM_LAUNCHER = "UPDATE rom_launchers SET is_default = ? WHERE rom_id = ? AND launcher_id = ?" +DELETE_ROM_LAUNCHERS = "DELETE FROM rom_launchers WHERE rom_id = ?" +DELETE_ROM_LAUNCHER = "DELETE FROM rom_launchers WHERE rom_id = ? AND launcher_id = ?" + +SELECT_ROMCOLLECTION_LAUNCHERS = "SELECT * FROM vw_romcollection_launchers WHERE romcollection_id = ?" +INSERT_ROMCOLLECTION_LAUNCHER = "INSERT INTO romcollection_launchers (launcher_id, romcollection_id, is_default) VALUES (?,?,?)" +UPDATE_ROMCOLLECTION_LAUNCHER = "UPDATE romcollection_launchers SET is_default = ? WHERE romcollection_id = ? AND launcher_id = ?" +DELETE_ROMCOLLECTION_LAUNCHERS = "DELETE FROM romcollection_launchers WHERE romcollection_id = ?" +DELETE_ROMCOLLECTION_LAUNCHER = "DELETE FROM romcollection_launchers WHERE romcollection_id = ? AND launcher_id = ?" + +SELECT_LAUNCHER = """ + SELECT l.*, + a.id AS associated_addon_id, + a.name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings + FROM launchers AS l INNER JOIN akl_addon AS a on l.akl_addon_id = a.id + WHERE l.id = ? +""" +SELECT_LAUNCHERS = """ + SELECT l.*, + a.id AS associated_addon_id, + a.name AS addon_name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings + FROM launchers AS l INNER JOIN akl_addon AS a on l.akl_addon_id = a.id +""" +INSERT_LAUNCHER = "INSERT INTO launchers (id, name, akl_addon_id, settings) VALUES (?,?,?,?)" +UPDATE_LAUNCHER = "UPDATE launchers SET name = ?, settings = ? WHERE id = ?" +DELETE_LAUNCHER = "DELETE FROM launchers WHERE id = ?" diff --git a/resources/lib/report.py b/resources/lib/report.py index 17f2e2a8..d35fe465 100644 --- a/resources/lib/report.py +++ b/resources/lib/report.py @@ -31,7 +31,7 @@ def report_print_ROM(slist: list, rom: ROM): slist.append("[COLOR violet]m_year[/COLOR]: '{0}'".format(rom.get_releaseyear())) slist.append("[COLOR violet]m_genre[/COLOR]: '{0}'".format(rom.get_genre())) slist.append("[COLOR violet]m_developer[/COLOR]: '{0}'".format(rom.get_developer())) - slist.append("[COLOR violet]m_nplayers[/COLOR]: '{0}'".format(rom.get_number_of_players())) + slist.append("[COLOR violet]nplayers[/COLOR]: '{0}'".format(rom.get_number_of_players())) slist.append("[COLOR violet]m_esrb[/COLOR]: '{0}'".format(rom.get_esrb_rating())) slist.append("[COLOR violet]m_rating[/COLOR]: '{0}'".format(rom.get_rating())) slist.append("[COLOR violet]m_plot[/COLOR]: '{0}'".format(rom.get_plot())) @@ -47,7 +47,7 @@ def report_print_ROM(slist: list, rom: ROM): slist.append("[COLOR skyblue]i_extra_ROM[/COLOR]: {0}\n".format(rom.get_extra_ROM())) # >> Assets/artwork - asset_infos = g_assetFactory.get_asset_kinds_for_roms() + asset_infos = g_assetFactory.get_asset_for_roms() for asset_info in asset_infos: slist.append("[COLOR violet]{0}[/COLOR]: '{1}'".format(asset_info.id, rom.get_asset(asset_info))) diff --git a/resources/lib/repositories.py b/resources/lib/repositories.py index e56e4919..d848af1d 100644 --- a/resources/lib/repositories.py +++ b/resources/lib/repositories.py @@ -14,9 +14,10 @@ from resources.lib import globals from resources.lib import queries as qry -from resources.lib.domain import MetaDataItemABC, Category, ROMCollection, ROM, Asset, AssetPath, AssetMapping, RomAssetMapping, VirtualCollection +from resources.lib.domain import MetaDataItemABC, Category, ROMCollection, ROM, VirtualCollection, RuleSet, Rule +from resources.lib.domain import Asset, AssetPath, AssetMapping, RomAssetMapping from resources.lib.domain import VirtualCategoryFactory, VirtualCollectionFactory, ROMLauncherAddonFactory, g_assetFactory -from resources.lib.domain import ROMCollectionScanner, ROMLauncherAddon, AelAddon +from resources.lib.domain import Source, ROMLauncherAddon, AelAddon # ################################################################################################# @@ -56,11 +57,27 @@ def find_root_items(self): return item_data - def find_items(self, view_id, is_virtual=False) -> typing.Any: - repository_file = self.paths.VIEWS_DIR.pjoin('view_{}.json'.format(view_id)) - if is_virtual: - repository_file = self.paths.GENERATED_VIEWS_DIR.pjoin('view_{}.json'.format(view_id)) - + def find_sources_items(self): + repository_file = self.paths.SOURCES_VIEW_PATH + self.logger.debug(f'find_sources_items(): Loading path data from file {repository_file.getPath()}') + if not repository_file.exists(): + self.logger.debug(f'find_sources_items(): Path does not exist {repository_file.getPath()}') + return None + + try: + item_data = repository_file.readJson() + except ValueError as ex: + statinfo = repository_file.stat() + self.logger.error('find_sources_items(): ValueError exception in file.readJson() function', exc_info=ex) + self.logger.error('find_sources_items(): Dir {}'.format(repository_file.getPath())) + self.logger.error('find_sources_items(): Size {}'.format(statinfo.st_size)) + return None + + return item_data + + def find_items(self, view_id, obj_type: int) -> typing.Any: + + repository_file = self._assemble_view_file_name(view_id, obj_type) self.logger.debug('find_items(): Loading path data from file {}'.format(repository_file.getPath())) try: item_data = repository_file.readJson() @@ -72,46 +89,69 @@ def find_items(self, view_id, is_virtual=False) -> typing.Any: return None return item_data - + def store_root_view(self, view_data): repository_file = self.paths.ROOT_PATH - self.logger.debug('store_root_view(): Storing data in file {}'.format(repository_file.getPath())) + self.logger.debug(f'store_root_view(): Storing data in file {repository_file.getPath()}') repository_file.writeJson(view_data) - def store_view(self, view_id:str, object_type:str, view_data): - if object_type == constants.OBJ_CATEGORY_VIRTUAL or object_type == constants.OBJ_COLLECTION_VIRTUAL: - repository_file = self.paths.GENERATED_VIEWS_DIR.pjoin(f'view_{view_id}.json') - else: - repository_file = self.paths.VIEWS_DIR.pjoin(f'view_{view_id}.json') - - if view_data is None: + def store_sources_view(self, view_data): + repository_file = self.paths.SOURCES_VIEW_PATH + self.logger.debug(f'store_sources_view(): Storing data in file {repository_file.getPath()}') + repository_file.writeJson(view_data) + + def store_view(self, view_id: str, object_type: int, view_data): + repository_file = self._assemble_view_file_name(view_id, object_type) + if view_data is None: if repository_file.exists(): - self.logger.debug('store_view(): No data for file {}. Removing file'.format(repository_file.getPath())) + self.logger.debug(f'store_view(): No data for file {repository_file.getPath()}. Removing file') repository_file.unlink() return self.logger.debug(f'store_view(): Storing data in file {repository_file.getPath()}') repository_file.writeJson(view_data) - def cleanup_views(self, view_ids_to_keep:typing.List[str]): - view_files = self.paths.VIEWS_DIR.scanFilesInPath('view_*.json') + def cleanup_views(self, view_ids_to_keep: typing.List[str]): + view_files = self.paths.VIEWS_DIR.scanFilesInPath('*.json') for view_file in view_files: - view_id = view_file.getBaseNoExt().replace('view_', '') - if not view_id in view_ids_to_keep: + view_id = view_file.getBaseNoExt().replace('collection_', '').replace('category_', '').replace('source_', '') + if view_id not in view_ids_to_keep: self.logger.info(f'Removing file for view "{view_id}"') view_file.unlink() + def cleanup_obsolete_views(self): + view_files = self.paths.VIEWS_DIR.scanFilesInPath('view*.json') + for view_file in view_files: + self.logger.info(f'Removing file: "{view_file}"') + view_file.unlink() + def cleanup_virtual_category_views(self, view_id): - view_files = self.paths.GENERATED_VIEWS_DIR.scanFilesInPath(f'view_{view_id}_*.json') + view_files = self.paths.GENERATED_VIEWS_DIR.scanFilesInPath(f'category_{view_id}_*.json') self.logger.info(f'Removing {len(view_files)} files for virtual category "{view_id}"') for view_file in view_files: view_file.unlink() def cleanup_all_virtual_category_views(self): - view_files = self.paths.GENERATED_VIEWS_DIR.scanFilesInPath('view_vcategory*.json') + view_files = self.paths.GENERATED_VIEWS_DIR.scanFilesInPath('category_*.json') self.logger.info(f'Removing {len(view_files)} files for all virtual categories') for view_file in view_files: view_file.unlink() + + def _assemble_view_file_name(self, view_id, obj_type): + + if obj_type == constants.OBJ_CATEGORY: + return self.paths.VIEWS_DIR.pjoin(f'category_{view_id}.json') + elif obj_type == constants.OBJ_ROMCOLLECTION: + return self.paths.VIEWS_DIR.pjoin(f'collection_{view_id}.json') + elif obj_type == constants.OBJ_SOURCE: + return self.paths.VIEWS_DIR.pjoin(f'source_{view_id}.json') + elif obj_type == constants.OBJ_CATEGORY_VIRTUAL: + return self.paths.GENERATED_VIEWS_DIR.pjoin(f'category_{view_id}.json') + elif obj_type == constants.OBJ_COLLECTION_VIRTUAL: + return self.paths.GENERATED_VIEWS_DIR.pjoin(f'collection_{view_id}.json') + + return self.paths.VIEWS_DIR.pjoin(f'view_{view_id}.json') + # # XmlConfigurationRepository works with original XML configuration files, which contained the @@ -119,7 +159,7 @@ def cleanup_all_virtual_category_views(self): # class XmlConfigurationRepository(object): - def __init__(self, file_path: io.FileName, debug = False): + def __init__(self, file_path: io.FileName, debug=False): self.file_path = file_path self.debug = debug self.logger = logging.getLogger(__name__) @@ -216,10 +256,11 @@ def get_launchers(self) -> typing.Iterator[ROMCollection]: # ------------------------------------------------------------------------------------------------- class ROMsJsonFileRepository(object): - def __init__(self, file_path: io.FileName, debug = False): + def __init__(self, file_path: io.FileName, debug=False): self.file_path = file_path self.debug = debug self.logger = logging.getLogger(__name__) + # # Loads ROM databases from disk # @@ -248,7 +289,7 @@ def load_ROMs(self) -> typing.List[ROM]: if roms_data and isinstance(roms_data, list) and 'control' in roms_data[0]: control_str = roms_data[0]['control'] version_int = roms_data[0]['version'] - roms_data = roms_data[1] + roms_data = roms_data[1] roms = [] if isinstance(roms_data, list): @@ -353,11 +394,11 @@ def reset_database(self, schema_file_path: io.FileName): self.create_empty_database(schema_file_path) - def migrate_database(self, migration_files:typing.List[io.FileName], new_db_version, skip_scripts_execution=False): + def migrate_database(self, migration_files: typing.List[io.FileName], new_db_version, skip_scripts_execution=False): if not skip_scripts_execution: # make copy of existing database file to execute migration on. temp_filepath = self._db_path.changeExtension(f".{new_db_version}.db") - backup_filepath = self._db_path.changeExtension(f".db.bak") + backup_filepath = self._db_path.changeExtension(".db.bak") if temp_filepath.exists(): temp_filepath.unlink() else: @@ -381,7 +422,7 @@ def migrate_database(self, migration_files:typing.List[io.FileName], new_db_vers if not skip_scripts_execution: self.execute_script(sql_statements) self.commit() - except: + except Exception: self.logger.exception(f"Failure with database migration '{migration_file.getBase()}'") kodi.notify_error(kodi.translate(40954)) failed = True @@ -391,11 +432,11 @@ def migrate_database(self, migration_files:typing.List[io.FileName], new_db_vers self.close_session() if file_version > check_version: - self.execute_single_session(temp_filepath, qry.AKL_INSERT_MIGRATION,[ - migration_file.getBase(), str(new_db_version), - datetime.datetime.now(), not failed]) + self.execute_single_session(temp_filepath, qry.AKL_INSERT_MIGRATION, [ + migration_file.getBase(), str(new_db_version), + datetime.datetime.now(), not failed]) - self.logger.info(f'Updating database schema version of app {globals.addon_id} to {new_db_version}') + self.logger.info(f'Updating database schema version of app {globals.addon_id} to {new_db_version}') self.execute_single_session(temp_filepath, qry.AKL_UPDATE_VERSION, [ str(new_db_version), globals.addon_id]) @@ -413,7 +454,7 @@ def get_migrations_history(self): try: self.execute(qry.AKL_SELECT_MIGRATIONS) migrations_data_set = self.result_set() - except: + except Exception: self.logger.error("Failure getting executed migrations") finally: self.close_session() @@ -423,14 +464,14 @@ def get_migration_files(self, db_version): if not globals.g_PATHS.DATABASE_MIGRATIONS_PATH.exists(): globals.g_PATHS.DATABASE_MIGRATIONS_PATH.makedirs() - migrations_files_available = globals.g_PATHS.DATABASE_MIGRATIONS_PATH.scanFilesInPath("*.sql") + migrations_files_available = globals.g_PATHS.DATABASE_MIGRATIONS_PATH.scanFilesInPath("*.sql") migrations_files_to_execute = [] for migration_file in migrations_files_available: file_version = self.get_version_from_migration_file(migration_file) if file_version > db_version: migrations_files_to_execute.append(migration_file) - migrations_files_to_execute.sort(key = lambda f: f.getBaseNoExt()) + migrations_files_to_execute.sort(key=lambda f: f.getBaseNoExt()) return migrations_files_to_execute def get_version_from_migration_file(self, file: io.FileName): @@ -494,7 +535,8 @@ def __enter__(self): self.open_session() def __exit__(self, type, value, traceback): - if type is not None: # errors raised + if type is not None: + # errors raised self.logger.error("type: %s value: %s", type, value) self.close_session() @@ -629,25 +671,23 @@ def find_categories_by_rom(self, rom_id: str) -> typing.Iterator[Category]: def insert_category(self, category_obj: Category, parent_obj: Category = None): self.logger.info("CategoryRepository.insert_category(): Inserting new category '{}'".format(category_obj.get_name())) metadata_id = text.misc_generate_random_SID() - assets_path = category_obj.get_assets_root_path() parent_category_id = parent_obj.get_id() if parent_obj is not None and parent_obj.get_id() != constants.VCATEGORY_ADDONROOT_ID else None self._uow.execute(qry.INSERT_METADATA, - metadata_id, - category_obj.get_releaseyear(), - category_obj.get_genre(), - category_obj.get_developer(), - category_obj.get_rating(), - category_obj.get_plot(), - json.dumps(category_obj.get_extras()), - assets_path.getPath() if assets_path is not None else None, - category_obj.is_finished()) + metadata_id, + category_obj.get_releaseyear(), + category_obj.get_genre(), + category_obj.get_developer(), + category_obj.get_rating(), + category_obj.get_plot(), + json.dumps(category_obj.get_extras()), + category_obj.is_finished()) self._uow.execute(qry.INSERT_CATEGORY, - category_obj.get_id(), - category_obj.get_name(), - parent_category_id, - metadata_id) + category_obj.get_id(), + category_obj.get_name(), + parent_category_id, + metadata_id) category_assets = category_obj.get_assets() for asset in category_assets: @@ -658,22 +698,20 @@ def insert_category(self, category_obj: Category, parent_obj: Category = None): def update_category(self, category_obj: Category): self.logger.info(f" Updating category '{category_obj.get_name()}'") - assets_path = category_obj.get_assets_root_path() self._uow.execute(qry.UPDATE_METADATA, - category_obj.get_releaseyear(), - category_obj.get_genre(), - category_obj.get_developer(), - category_obj.get_rating(), - category_obj.get_plot(), - json.dumps(category_obj.get_extras()), - assets_path.getPath() if assets_path is not None else None, - category_obj.is_finished(), - category_obj.get_custom_attribute('metadata_id')) + category_obj.get_releaseyear(), + category_obj.get_genre(), + category_obj.get_developer(), + category_obj.get_rating(), + category_obj.get_plot(), + json.dumps(category_obj.get_extras()), + category_obj.is_finished(), + category_obj.get_custom_attribute('metadata_id')) self._uow.execute(qry.UPDATE_CATEGORY, - category_obj.get_name(), - category_obj.get_id()) + category_obj.get_name(), + category_obj.get_id()) for asset in category_obj.get_assets(): if asset.get_id() == '': @@ -743,6 +781,8 @@ def count_collections(self) -> int: return int(count_data['count']) def find_romcollection(self, romcollection_id: str) -> ROMCollection: + if romcollection_id is None: + return None self._uow.execute(qry.SELECT_ROMCOLLECTION, romcollection_id) romcollection_data = self._uow.single_result() @@ -751,13 +791,7 @@ def find_romcollection(self, romcollection_id: str) -> ROMCollection: assets = [] for asset_data in assets_result_set: assets.append(Asset(asset_data)) - - self._uow.execute(qry.SELECT_ROMCOLLECTION_ASSET_PATHS, romcollection_id) - asset_paths_result_set = self._uow.result_set() - asset_paths = [] - for asset_paths_data in asset_paths_result_set: - asset_paths.append(AssetPath(asset_paths_data)) - + self._uow.execute(qry.SELECT_ITEM_ASSET_MAPPINGS, romcollection_data['metadata_id']) asset_mappings_result_set = self._uow.result_set() asset_mappings = [] @@ -777,15 +811,8 @@ def find_romcollection(self, romcollection_id: str) -> ROMCollection: addon = AelAddon(launcher_data.copy()) launcher = ROMLauncherAddonFactory.create(addon, launcher_data) launchers.append(launcher) - - self._uow.execute(qry.SELECT_ROMCOLLECTION_SCANNERS, romcollection_id) - scanners_data = self._uow.result_set() - scanners = [] - for scanner_data in scanners_data: - addon = AelAddon(scanner_data.copy()) - scanners.append(ROMCollectionScanner(addon, scanner_data)) - - return ROMCollection(romcollection_data, assets, asset_paths, asset_mappings, rom_asset_mappings, launchers, scanners) + + return ROMCollection(romcollection_data, assets, asset_mappings, rom_asset_mappings, launchers) def find_all_romcollections(self) -> typing.Iterator[ROMCollection]: self._uow.execute(qry.SELECT_ROMCOLLECTIONS) @@ -843,7 +870,7 @@ def find_root_romcollections(self) -> typing.Iterator[ROMCollection]: yield ROMCollection(romcollection_data, assets, asset_mappings=asset_mappings, rom_asset_mappings=rom_asset_mappings) - def find_romcollections_by_parent(self, category_id:str) -> typing.Iterator[ROMCollection]: + def find_romcollections_by_parent(self, category_id: str) -> typing.Iterator[ROMCollection]: if category_id in constants.VCATEGORIES: for collection in self.find_virtualcollections_by_category(category_id): @@ -876,28 +903,27 @@ def find_romcollections_by_parent(self, category_id:str) -> typing.Iterator[ROMC yield ROMCollection(romcollection_data, assets, asset_mappings=asset_mappings, rom_asset_mappings=rom_asset_mappings) - def find_virtualcollections_by_category(self, vcategory_id:str) -> typing.Iterator[VirtualCollection]: + def find_virtualcollections_by_category(self, vcategory_id: str) -> typing.Iterator[VirtualCollection]: query = self._get_collections_query_by_vcategory_id(vcategory_id) - if query is None: return [] + if query is None: + return [] self._uow.execute(query) result_set = self._uow.result_set() for result in result_set: option_value = str(result['option_value']) - if not option_value: option_value = 'Undefined' + if not option_value: + option_value = 'Undefined' yield VirtualCollectionFactory.create_by_category(vcategory_id, option_value) - def find_romcollections_by_rom(self, rom_id:str) -> typing.Iterator[ROMCollection]: + def find_romcollections_by_rom(self, rom_id: str) -> typing.Iterator[ROMCollection]: self._uow.execute(qry.SELECT_ROMCOLLECTIONS_BY_ROM, rom_id) result_set = self._uow.result_set() self._uow.execute(qry.SELECT_ROMCOLLECTION_ASSETS_BY_ROM, rom_id) assets_result_set = self._uow.result_set() - self._uow.execute(qry.SELECT_ROMCOLLECTION_ASSETS_PATHS_BY_ROM, rom_id) - asset_paths_result_set = self._uow.result_set() - self._uow.execute(qry.SELECT_ROMCOLLECTION_ASSET_MAPPINGS_BY_ROM, rom_id) asset_mappings_result_set = self._uow.result_set() @@ -907,18 +933,48 @@ def find_romcollections_by_rom(self, rom_id:str) -> typing.Iterator[ROMCollectio self._uow.execute(qry.SELECT_ROMCOLLECTION_LAUNCHERS_BY_ROM, rom_id) launchers_data = self._uow.result_set() - self._uow.execute(qry.SELECT_ROMCOLLECTION_SCANNERS_BY_ROM, rom_id) - scanners_data = self._uow.result_set() - for romcollection_data in result_set: assets = [] for asset_data in filter(lambda a: a['romcollection_id'] == romcollection_data['id'], assets_result_set): - assets.append(Asset(asset_data)) - - asset_paths = [] - for asset_path_data in filter(lambda a: a['romcollection_id'] == romcollection_data['id'], asset_paths_result_set): - asset_paths.append(AssetPath(asset_path_data)) + assets.append(Asset(asset_data)) + + asset_mappings = [] + for mapping_data in filter(lambda a: a['metadata_id'] == romcollection_data['metadata_id'], asset_mappings_result_set): + asset_mappings.append(AssetMapping(mapping_data)) + rom_asset_mappings = [] + for mapping_data in filter(lambda a: a['romcollection_id'] == romcollection_data['id'], rom_asset_mappings_result_set): + rom_asset_mappings.append(RomAssetMapping(mapping_data)) + + launchers = [] + for launcher_data in launchers_data: + addon = AelAddon(launcher_data.copy()) + launcher = ROMLauncherAddonFactory.create(addon, launcher_data) + launchers.append(launcher) + + yield ROMCollection(romcollection_data, assets, asset_mappings, rom_asset_mappings, launchers) + + def find_romcollections_by_source(self, source_id: str) -> typing.Iterator[ROMCollection]: + self._uow.execute(qry.SELECT_ROMCOLLECTIONS_BY_SOURCE, source_id) + result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROMCOLLECTION_ASSETS_BY_SOURCE, source_id) + assets_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROMCOLLECTION_ASSET_MAPPINGS_BY_SOURCE, source_id) + asset_mappings_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROMCOLLECTION_ROM_ASSET_MAPPINGS_BY_SOURCE, source_id) + rom_asset_mappings_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROMCOLLECTION_LAUNCHERS_BY_SOURCE, source_id) + launchers_data = self._uow.result_set() + + for romcollection_data in result_set: + assets = [] + for asset_data in filter(lambda a: a['romcollection_id'] == romcollection_data['id'], assets_result_set): + assets.append(Asset(asset_data)) + asset_mappings = [] for mapping_data in filter(lambda a: a['metadata_id'] == romcollection_data['metadata_id'], asset_mappings_result_set): asset_mappings.append(AssetMapping(mapping_data)) @@ -932,42 +988,56 @@ def find_romcollections_by_rom(self, rom_id:str) -> typing.Iterator[ROMCollectio addon = AelAddon(launcher_data.copy()) launcher = ROMLauncherAddonFactory.create(addon, launcher_data) launchers.append(launcher) + + yield ROMCollection(romcollection_data, assets, asset_mappings, rom_asset_mappings, launchers) + + def find_import_rules_by_collection(self, romcollection: ROMCollection) -> typing.Iterator[RuleSet]: + self._uow.execute(qry.SELECT_IMPORT_RULES_BY_COLLECTION, romcollection.get_id()) + result_set = self._uow.result_set() + rulesets = {} + for rule_data in result_set: + rulesets.setdefault(rule_data["ruleset_id"], []).append(rule_data) + + for ruleset_data in rulesets.values(): + entity_data = ruleset_data[0] + entity_data['rules'] = ruleset_data + yield RuleSet(entity_data) + + def find_ruleset(self, romcollection_id, ruleset_id): + self._uow.execute(qry.SELECT_IMPORT_RULE_BY_COLLECTION, romcollection_id, ruleset_id) + result_set = self._uow.result_set() + + entity_data = result_set[0] + entity_data['rules'] = result_set - scanners = [] - for scanner_data in scanners_data: - addon = AelAddon(scanner_data.copy()) - scanners.append(ROMCollectionScanner(addon, scanner_data)) - - yield ROMCollection(romcollection_data, assets, asset_paths, asset_mappings, rom_asset_mappings, launchers, scanners) + return RuleSet(entity_data) def insert_romcollection(self, romcollection_obj: ROMCollection, parent_obj: Category = None): - self.logger.info("ROMCollectionRepository.insert_romcollection(): Inserting new romcollection '{}'".format(romcollection_obj.get_name())) + self.logger.info(f"ROMCollectionRepository: Inserting new romcollection '{romcollection_obj.get_name()}'") metadata_id = text.misc_generate_random_SID() - assets_path = romcollection_obj.get_assets_root_path() parent_category_id = parent_obj.get_id() if parent_obj is not None and parent_obj.get_id() != constants.VCATEGORY_ADDONROOT_ID else None self._uow.execute(qry.INSERT_METADATA, - metadata_id, - romcollection_obj.get_releaseyear(), - romcollection_obj.get_genre(), - romcollection_obj.get_developer(), - romcollection_obj.get_rating(), - romcollection_obj.get_plot(), - json.dumps(romcollection_obj.get_extras()), - assets_path.getPath() if assets_path is not None else None, - romcollection_obj.is_finished()) + metadata_id, + romcollection_obj.get_releaseyear(), + romcollection_obj.get_genre(), + romcollection_obj.get_developer(), + romcollection_obj.get_rating(), + romcollection_obj.get_plot(), + json.dumps(romcollection_obj.get_extras()), + romcollection_obj.is_finished()) self._uow.execute(qry.INSERT_ROMCOLLECTION, - romcollection_obj.get_id(), - romcollection_obj.get_name(), - parent_category_id, - metadata_id, - romcollection_obj.get_platform(), - romcollection_obj.get_box_sizing()) + romcollection_obj.get_id(), + romcollection_obj.get_name(), + parent_category_id, + metadata_id, + romcollection_obj.get_platform(), + romcollection_obj.get_box_sizing()) romcollection_assets = romcollection_obj.get_assets() for asset in romcollection_assets: - self._insert_asset(asset, romcollection_obj) + self._insert_asset(asset, romcollection_obj) asset_paths = romcollection_obj.get_asset_paths() for asset_path in asset_paths: @@ -983,79 +1053,55 @@ def insert_romcollection(self, romcollection_obj: ROMCollection, parent_obj: Cat for romcollection_launcher in romcollection_launchers: romcollection_launcher.set_id(text.misc_generate_random_SID()) self._uow.execute(qry.INSERT_ROMCOLLECTION_LAUNCHER, - romcollection_launcher.get_id(), - romcollection_obj.get_id(), - romcollection_launcher.addon.get_id(), - romcollection_launcher.get_settings_str(), - romcollection_launcher.is_default()) - - romcollection_scanners = romcollection_obj.get_scanners() - for romcollection_scanner in romcollection_scanners: - romcollection_scanner.set_id(text.misc_generate_random_SID()) - self._uow.execute(qry.INSERT_ROMCOLLECTION_SCANNER, - romcollection_scanner.get_id(), - romcollection_obj.get_id(), - romcollection_scanner.addon.get_id(), - romcollection_scanner.get_settings_str()) - + romcollection_launcher.get_id(), + romcollection_obj.get_id(), + romcollection_launcher.addon.get_id(), + romcollection_launcher.get_settings_str(), + romcollection_launcher.is_default()) + def update_romcollection(self, romcollection_obj: ROMCollection): - self.logger.info("ROMCollectionRepository.update_romcollection(): Updating romcollection '{}'".format(romcollection_obj.get_name())) - assets_path = romcollection_obj.get_assets_root_path() + self.logger.info(f"ROMCollectionRepository.update_romcollection(): Updating romcollection '{romcollection_obj.get_name()}'") self._uow.execute(qry.UPDATE_METADATA, - romcollection_obj.get_releaseyear(), - romcollection_obj.get_genre(), - romcollection_obj.get_developer(), - romcollection_obj.get_rating(), - romcollection_obj.get_plot(), - json.dumps(romcollection_obj.get_extras()), - assets_path.getPath() if assets_path is not None else None, - romcollection_obj.is_finished(), - romcollection_obj.get_custom_attribute('metadata_id')) + romcollection_obj.get_releaseyear(), + romcollection_obj.get_genre(), + romcollection_obj.get_developer(), + romcollection_obj.get_rating(), + romcollection_obj.get_plot(), + json.dumps(romcollection_obj.get_extras()), + romcollection_obj.is_finished(), + romcollection_obj.get_custom_attribute('metadata_id')) self._uow.execute(qry.UPDATE_ROMCOLLECTION, - romcollection_obj.get_name(), - romcollection_obj.get_platform(), - romcollection_obj.get_box_sizing(), - romcollection_obj.get_id()) - + romcollection_obj.get_name(), + romcollection_obj.get_platform(), + romcollection_obj.get_box_sizing(), + romcollection_obj.get_id()) + romcollection_launchers = romcollection_obj.get_launchers() for romcollection_launcher in romcollection_launchers: - if romcollection_launcher.get_id() is None: - romcollection_launcher.set_id(text.misc_generate_random_SID()) + if romcollection_launcher.get_custom_attribute("romcollection_id") is None: self._uow.execute(qry.INSERT_ROMCOLLECTION_LAUNCHER, - romcollection_launcher.get_id(), - romcollection_obj.get_id(), - romcollection_launcher.addon.get_id(), - romcollection_launcher.get_settings_str(), - romcollection_launcher.is_default()) + romcollection_launcher.get_id(), + romcollection_obj.get_id(), + romcollection_launcher.is_default()) else: self._uow.execute(qry.UPDATE_ROMCOLLECTION_LAUNCHER, - romcollection_launcher.get_settings_str(), - romcollection_launcher.is_default(), - romcollection_launcher.get_id()) + romcollection_launcher.is_default(), + romcollection_obj.get_id(), + romcollection_launcher.get_id()) - romcollection_scanners = romcollection_obj.get_scanners() - for romcollection_scanner in romcollection_scanners: - if romcollection_scanner.get_id() is None: - romcollection_scanner.set_id(text.misc_generate_random_SID()) - self._uow.execute(qry.INSERT_ROMCOLLECTION_SCANNER, - romcollection_scanner.get_id(), - romcollection_obj.get_id(), - romcollection_scanner.addon.get_id(), - romcollection_scanner.get_settings_str()) - else: - self._uow.execute(qry.UPDATE_ROMCOLLECTION_SCANNER, - romcollection_scanner.get_settings_str(), - romcollection_scanner.get_id()) - for asset in romcollection_obj.get_assets(): - if asset.get_id() == '': self._insert_asset(asset, romcollection_obj) - else: self._update_asset(asset, romcollection_obj) + if asset.get_id() == '': + self._insert_asset(asset, romcollection_obj) + else: + self._update_asset(asset, romcollection_obj) for asset_path in romcollection_obj.get_asset_paths(): - if asset_path.get_id() == '': self._insert_asset_path(asset_path, romcollection_obj) - else: self._update_asset_path(asset_path, romcollection_obj) + if asset_path.get_id() == '': + self._insert_asset_path(asset_path, romcollection_obj) + else: + self._update_asset_path(asset_path, romcollection_obj) for mapping in romcollection_obj.asset_mappings: if mapping.get_id() == '': @@ -1070,7 +1116,7 @@ def update_romcollection(self, romcollection_obj: ROMCollection): self._update_rom_asset_mapping(mapping, romcollection_obj) def update_romcollection_parent_reference(self, romcollection_obj: ROMCollection, parent_obj: Category = None): - self.logger.info(f"ROMCollectionRepository.update_romcollection_parent_reference(): Updating romcollection '{romcollection_obj.get_name()}'") + self.logger.info(f"ROMCollectionRepository: Updating romcollection '{romcollection_obj.get_name()}'") parent_category_id = parent_obj.get_id() if parent_obj is not None and parent_obj.get_id() != constants.VCATEGORY_ADDONROOT_ID else None self._uow.execute(qry.UPDATE_ROMCOLLECTION_PARENT, parent_category_id, romcollection_obj.get_id()) @@ -1087,12 +1133,35 @@ def delete_romcollection(self, romcollection_id: str): self.logger.info("ROMCollectionRepository.delete_romcollection(): Deleting romcollection '{}'".format(romcollection_id)) self._uow.execute(qry.DELETE_ROMCOLLECTION, romcollection_id) - def remove_launcher(self, romcollection_id: str, launcher_id:str): + def remove_launcher(self, romcollection_id: str, launcher_id: str): self._uow.execute(qry.DELETE_ROMCOLLECTION_LAUNCHER, romcollection_id, launcher_id) - def remove_scanner(self, romcollection_id: str, scanner_id:str): - self._uow.execute(qry.DELETE_ROMCOLLECTION_SCANNER, romcollection_id, scanner_id) + def add_ruleset_to_romcollection(self, romcollection_id: str, ruleset: RuleSet): + self._uow.execute(qry.INSERT_RULESET_FOR_ROMCOLLECTION, + ruleset.get_ruleset_id(), + ruleset.get_source_id(), + romcollection_id, + int(ruleset.get_set_operator())) + + def update_ruleset_in_romcollection(self, romcollection_id: str, ruleset: RuleSet): + self._uow.execute(qry.UPDATE_RULESET_FOR_ROMCOLLECTION, + ruleset.get_source_id(), + romcollection_id, + int(ruleset.get_set_operator()), + ruleset.get_ruleset_id()) + + for rule in ruleset.get_rules(): + if rule.get_id() == '': + self._insert_rule(rule, ruleset) + else: + self._update_rule(rule, ruleset) + + def delete_all_rules_from_ruleset(self, ruleset: RuleSet): + self._uow.execute(qry.DELETE_ALL_RULES_FROM_RULESET, ruleset.get_ruleset_id()) + def delete_rule_from_ruleset(self, ruleset: RuleSet, rule: Rule): + self._uow.execute(qry.DELETE_RULE_FROM_RULESET, rule.get_id(), ruleset.get_ruleset_id()) + def _insert_asset(self, asset: Asset, romcollection_obj: ROMCollection): asset_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET, asset_db_id, asset.get_path(), asset.get_asset_info_id()) @@ -1101,45 +1170,58 @@ def _insert_asset(self, asset: Asset, romcollection_obj: ROMCollection): def _update_asset(self, asset: Asset, romcollection_obj: ROMCollection): self._uow.execute(qry.UPDATE_ASSET, asset.get_path(), asset.get_asset_info_id(), asset.get_id()) if asset.get_custom_attribute('romcollection_id') is None: - self._uow.execute(qry.INSERT_ROMCOLLECTION_ASSET, romcollection_obj.get_id(), asset.get_id()) + self._uow.execute(qry.INSERT_ROMCOLLECTION_ASSET, romcollection_obj.get_id(), asset.get_id()) def _insert_asset_path(self, asset_path: AssetPath, romcollection_obj: ROMCollection): asset_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET_PATH, asset_db_id, asset_path.get_path(), asset_path.get_asset_info_id()) - self._uow.execute(qry.INSERT_ROMCOLLECTION_ASSET_PATH, romcollection_obj.get_id(), asset_db_id) + self._uow.execute(qry.INSERT_ROMCOLLECTION_ASSET_PATH, romcollection_obj.get_id(), asset_db_id) def _update_asset_path(self, asset_path: AssetPath, romcollection_obj: ROMCollection): self._uow.execute(qry.UPDATE_ASSET_PATH, asset_path.get_path(), asset_path.get_asset_info_id(), asset_path.get_id()) if asset_path.get_custom_attribute('romcollection_id') is None: - self._uow.execute(qry.INSERT_ROMCOLLECTION_ASSET_PATH, romcollection_obj.get_id(), asset_path.get_id()) + self._uow.execute(qry.INSERT_ROMCOLLECTION_ASSET_PATH, romcollection_obj.get_id(), asset_path.get_id()) def _insert_asset_mapping(self, mapping: AssetMapping, obj: MetaDataItemABC): if not mapping.is_mapped(): return mapping_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET_MAPPING, mapping_db_id, mapping.get_asset_info().id, mapping.get_mapped_to_asset_info().id) - self._uow.execute(qry.INSERT_MAPPING_WITH_METADATA, obj.get_metadata_id(), mapping_db_id) + self._uow.execute(qry.INSERT_MAPPING_WITH_METADATA, obj.get_metadata_id(), mapping_db_id) def _update_asset_mapping(self, mapping: AssetMapping, obj: MetaDataItemABC): if mapping.is_mapped(): self._uow.execute(qry.UPDATE_ASSET_MAPPING, mapping.get_asset_info().id, mapping.get_mapped_to_asset_info().id, mapping.get_id()) return - self._uow.execute(qry.DELETE_ASSET_MAPPING, mapping.get_id()) + self._uow.execute(qry.DELETE_ASSET_MAPPING, mapping.get_id()) def _insert_rom_asset_mapping(self, mapping: RomAssetMapping, obj: ROMCollection): if not mapping.is_mapped(): return mapping_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET_MAPPING, mapping_db_id, mapping.get_asset_info().id, mapping.get_mapped_to_asset_info().id) - self._uow.execute(qry.INSERT_ROMCOLLECTION_ROM_ASSET_MAPPING, obj.get_id(), mapping_db_id) + self._uow.execute(qry.INSERT_ROMCOLLECTION_ROM_ASSET_MAPPING, obj.get_id(), mapping_db_id) def _update_rom_asset_mapping(self, mapping: RomAssetMapping, obj: MetaDataItemABC): if mapping.is_mapped(): - self._uow.execute(qry.UPDATE_ASSET_MAPPING, mapping.get_asset_info().id, mapping.get_mapped_to_asset_info().id, mapping.get_id()) + self._uow.execute(qry.UPDATE_ASSET_MAPPING, + mapping.get_asset_info().id, + mapping.get_mapped_to_asset_info().id, + mapping.get_id()) return - self._uow.execute(qry.DELETE_ASSET_MAPPING, mapping.get_id()) + self._uow.execute(qry.DELETE_ASSET_MAPPING, mapping.get_id()) + + def _insert_rule(self, rule: Rule, ruleset: RuleSet): + rule_id = text.misc_generate_random_SID() + rule.set_id(rule_id) + self._uow.execute(qry.INSERT_RULE, rule.get_id(), ruleset.get_ruleset_id(), + rule.get_property(), rule.get_value(), rule.get_operator()) - def _get_collections_query_by_vcategory_id(self, vcategory_id:str) -> str: + def _update_rule(self, rule: Rule, ruleset: RuleSet): + self._uow.execute(qry.UPDATE_RULE, rule.get_property(), rule.get_value(), + rule.get_operator(), rule.get_id()) + + def _get_collections_query_by_vcategory_id(self, vcategory_id: str) -> str: if vcategory_id == constants.VCATEGORY_TITLE_ID: return qry.SELECT_VCOLLECTION_TITLES if vcategory_id == constants.VCATEGORY_GENRE_ID: @@ -1157,7 +1239,8 @@ def _get_collections_query_by_vcategory_id(self, vcategory_id:str) -> str: if vcategory_id == constants.VCATEGORY_RATING_ID: return qry.SELECT_VCOLLECTION_RATING - return None + return None + class ROMsRepository(object): @@ -1165,7 +1248,7 @@ def __init__(self, uow: UnitOfWork): self._uow = uow self.logger = logging.getLogger(__name__) - def find_root_roms(self)-> typing.Iterator[ROM]: + def find_root_roms(self) -> typing.Iterator[ROM]: self._uow.execute(qry.SELECT_ROMS_BY_ROOT_CATEGORY) result_set = self._uow.result_set() @@ -1183,26 +1266,9 @@ def find_root_roms(self)-> typing.Iterator[ROM]: self._uow.execute(qry.SELECT_ROM_TAGS_BY_ROOT_CATEGORY) tags_data_set = self._uow.result_set() - - for rom_data in result_set: - assets = [] - asset_paths = [] - asset_mappings = [] - tags = {} - for asset_data in filter(lambda a: a['rom_id'] == rom_data['id'], assets_result_set): - assets.append(Asset(asset_data)) - for asset_paths_data in filter(lambda a: a['rom_id'] == rom_data['id'], asset_paths_result_set): - asset_paths.append(AssetPath(asset_paths_data)) - for mapping_data in filter(lambda a: a['metadata_id'] == rom_data['metadata_id'], asset_mappings_result_set): - asset_mappings.append(RomAssetMapping(mapping_data)) - for tag in filter(lambda t: t['rom_id'] == rom_data['id'], tags_data_set): - tags[tag['tag']] = tag['id'] - - scanned_data = { - entry['data_key']: entry['data_value'] - for entry in filter(lambda s: s['rom_id'] == rom_data['id'], scanned_data_result_set) - } - yield ROM(rom_data, tags, assets, asset_paths, asset_mappings, scanned_data) + + return self._process_roms_data(result_set, assets_result_set, asset_paths_result_set, asset_mappings_result_set, + scanned_data_result_set, tags_data_set) def find_roms_by_category(self, category: Category) -> typing.Iterator[ROM]: category_id = category.get_id() if category else None @@ -1225,50 +1291,33 @@ def find_roms_by_category(self, category: Category) -> typing.Iterator[ROM]: self._uow.execute(qry.SELECT_ROM_TAGS_BY_CATEGORY, category_id) tags_data_set = self._uow.result_set() - for rom_data in result_set: - assets = [] - asset_paths = [] - asset_mappings = [] - tags = {} - for asset_data in filter(lambda a: a['rom_id'] == rom_data['id'], assets_result_set): - assets.append(Asset(asset_data)) - for asset_paths_data in filter(lambda a: a['rom_id'] == rom_data['id'], asset_paths_result_set): - asset_paths.append(AssetPath(asset_paths_data)) - for mapping_data in filter(lambda a: a['metadata_id'] == rom_data['metadata_id'], asset_mappings_result_set): - asset_mappings.append(RomAssetMapping(mapping_data)) - for tag in filter(lambda t: t['rom_id'] == rom_data['id'], tags_data_set): - tags[tag['tag']] = tag['id'] - - scanned_data = { - entry['data_key']: entry['data_value'] - for entry in filter(lambda s: s['rom_id'] == rom_data['id'], scanned_data_result_set) - } - yield ROM(rom_data, tags, assets, asset_paths, asset_mappings, scanned_data) + return self._process_roms_data(result_set, assets_result_set, asset_paths_result_set, asset_mappings_result_set, + scanned_data_result_set, tags_data_set) def find_roms_by_romcollection(self, romcollection: ROMCollection) -> typing.Iterator[ROM]: is_virtual = romcollection.get_type() == constants.OBJ_COLLECTION_VIRTUAL romcollection_id = romcollection.get_id() if is_virtual: - vcollection:VirtualCollection = romcollection + vcollection: VirtualCollection = romcollection query_param = vcollection.get_collection_value() roms_query, rom_assets_query = self._get_queries_by_vcollection_type(romcollection) if query_param is not None: - self._uow.execute(roms_query, query_param) - result_set = self._uow.result_set() + self._uow.execute(roms_query, query_param) + result_set = self._uow.result_set() self._uow.execute(rom_assets_query, query_param) - assets_result_set = self._uow.result_set() + assets_result_set = self._uow.result_set() else: - self._uow.execute(roms_query) - result_set = self._uow.result_set() + self._uow.execute(roms_query) + result_set = self._uow.result_set() self._uow.execute(rom_assets_query) - assets_result_set = self._uow.result_set() + assets_result_set = self._uow.result_set() - asset_paths_result_set = [] + asset_paths_result_set = [] asset_mappings_result_set = [] scanned_data_result_set = [] - tags_data_set = {} + tags_data_set = {} else: self._uow.execute(qry.SELECT_ROMS_BY_SET, romcollection_id) result_set = self._uow.result_set() @@ -1288,27 +1337,57 @@ def find_roms_by_romcollection(self, romcollection: ROMCollection) -> typing.Ite self._uow.execute(qry.SELECT_ROM_TAGS_BY_SET, romcollection_id) tags_data_set = self._uow.result_set() - for rom_data in result_set: - assets = [] - asset_paths = [] - asset_mappings = [] - tags = {} - for asset_data in filter(lambda a: a['rom_id'] == rom_data['id'], assets_result_set): - assets.append(Asset(asset_data)) - for asset_paths_data in filter(lambda a: a['rom_id'] == rom_data['id'], asset_paths_result_set): - asset_paths.append(AssetPath(asset_paths_data)) - for mapping_data in filter(lambda a: a['metadata_id'] == rom_data['metadata_id'], asset_mappings_result_set): - asset_mappings.append(RomAssetMapping(mapping_data)) - for tag in filter(lambda t: t['rom_id'] == rom_data['id'], tags_data_set): - tags[tag['tag']] = tag['id'] - - scanned_data = { - entry['data_key']: entry['data_value'] - for entry in filter(lambda s: s['rom_id'] == rom_data['id'], scanned_data_result_set) - } - yield ROM(rom_data, tags, assets, asset_paths, asset_mappings, scanned_data) + return self._process_roms_data(result_set, assets_result_set, asset_paths_result_set, asset_mappings_result_set, + scanned_data_result_set, tags_data_set) + + def find_roms_by_source(self, source: Source) -> typing.Iterator[ROM]: + source_id = source.get_id() + + self._uow.execute(qry.SELECT_ROMS_BY_SOURCE, source_id) + result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROM_ASSETS_BY_SOURCE, source_id) + assets_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROM_ASSETPATHS_BY_SOURCE, source_id) + asset_paths_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROM_ASSET_MAPPINGS_BY_SOURCE, source_id) + asset_mappings_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROM_SCANNED_DATA_BY_SOURCE, source_id) + scanned_data_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_ROM_TAGS_BY_SOURCE, source_id) + tags_data_set = self._uow.result_set() + + return self._process_roms_data(result_set, assets_result_set, asset_paths_result_set, asset_mappings_result_set, + scanned_data_result_set, tags_data_set) + + def find_standalone_roms(self) -> typing.Iterator[ROM]: + + self._uow.execute(qry.SELECT_STANDALONE_ROMS) + result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_STANDALONE_ROM_ASSETS) + assets_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_STANDALONE_ROM_ASSETPATHS) + asset_paths_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_STANDALONE_ROM_ASSET_MAPPINGS) + asset_mappings_result_set = self._uow.result_set() + + self._uow.execute(qry.SELECT_STANDALONE_ROM_SCANNED_DATA) + scanned_data_result_set = self._uow.result_set() - def find_rom(self, rom_id:str) -> ROM: + self._uow.execute(qry.SELECT_STANDALONE_ROM_TAGS) + tags_data_set = self._uow.result_set() + + return self._process_roms_data(result_set, assets_result_set, asset_paths_result_set, asset_mappings_result_set, + scanned_data_result_set, tags_data_set) + + def find_rom(self, rom_id: str) -> ROM: self._uow.execute(qry.SELECT_ROM, rom_id) rom_data = self._uow.single_result() @@ -1332,7 +1411,7 @@ def find_rom(self, rom_id:str) -> ROM: self._uow.execute(qry.SELECT_ROM_SCANNED_DATA, rom_id) scanned_data_result_set = self._uow.result_set() - scanned_data = { entry['data_key']: entry['data_value'] for entry in scanned_data_result_set } + scanned_data = {entry['data_key']: entry['data_value'] for entry in scanned_data_result_set} self._uow.execute(qry.SELECT_ROM_LAUNCHERS, rom_id) launchers_data = self._uow.result_set() @@ -1361,41 +1440,41 @@ def find_all_tags(self) -> dict: def insert_rom(self, rom_obj: ROM): self.logger.info(f"Inserting new ROM '{rom_obj.get_rom_identifier()}'") metadata_id = text.misc_generate_random_SID() - assets_path = rom_obj.get_assets_root_path() self._uow.execute(qry.INSERT_METADATA, - metadata_id, - rom_obj.get_releaseyear(), - rom_obj.get_genre(), - rom_obj.get_developer(), - rom_obj.get_rating(), - rom_obj.get_plot(), - json.dumps(rom_obj.get_extras()), - assets_path.getPath() if assets_path is not None else None, - rom_obj.is_finished()) - + metadata_id, + rom_obj.get_releaseyear(), + rom_obj.get_genre(), + rom_obj.get_developer(), + rom_obj.get_rating(), + rom_obj.get_plot(), + json.dumps(rom_obj.get_extras()), + rom_obj.is_finished()) + self._uow.execute(qry.INSERT_ROM, - rom_obj.get_id(), - metadata_id, - rom_obj.get_name(), - rom_obj.get_number_of_players(), - rom_obj.get_number_of_players_online(), - rom_obj.get_esrb_rating(), - rom_obj.get_pegi_rating(), - rom_obj.get_platform(), - rom_obj.get_box_sizing(), - rom_obj.get_nointro_status(), - rom_obj.get_clone(), - rom_obj.get_rom_status(), - rom_obj.get_scanned_with()) + rom_obj.get_id(), + metadata_id, + rom_obj.get_name(), + rom_obj.get_number_of_players(), + rom_obj.get_number_of_players_online(), + rom_obj.get_esrb_rating(), + rom_obj.get_pegi_rating(), + rom_obj.get_platform(), + rom_obj.get_box_sizing(), + rom_obj.get_nointro_status(), + rom_obj.get_clone(), + rom_obj.get_rom_status(), + rom_obj.get_scanned_by()) rom_assets = rom_obj.get_assets() for asset in rom_assets: self._insert_asset(asset, rom_obj) for asset_path in rom_obj.get_asset_paths(): - if not asset_path.get_id(): self._insert_asset_path(asset_path, rom_obj) - else: self._update_asset_path(asset_path, rom_obj) + if not asset_path.get_id(): + self._insert_asset_path(asset_path, rom_obj) + else: + self._update_asset_path(asset_path, rom_obj) for mapping in rom_obj.asset_mappings: self._insert_asset_mapping(mapping, rom_obj) @@ -1408,43 +1487,45 @@ def insert_rom(self, rom_obj: ROM): def update_rom(self, rom_obj: ROM): self.logger.info(f"Updating ROM '{rom_obj.get_rom_identifier()}'") - assets_path = rom_obj.get_assets_root_path() self._uow.execute(qry.UPDATE_METADATA, - rom_obj.get_releaseyear(), - rom_obj.get_genre(), - rom_obj.get_developer(), - rom_obj.get_rating(), - rom_obj.get_plot(), - json.dumps(rom_obj.get_extras()), - assets_path.getPath() if assets_path is not None else None, - rom_obj.is_finished(), - rom_obj.get_custom_attribute('metadata_id')) + rom_obj.get_releaseyear(), + rom_obj.get_genre(), + rom_obj.get_developer(), + rom_obj.get_rating(), + rom_obj.get_plot(), + json.dumps(rom_obj.get_extras()), + rom_obj.is_finished(), + rom_obj.get_custom_attribute('metadata_id')) self._uow.execute(qry.UPDATE_ROM, - rom_obj.get_name(), - rom_obj.get_number_of_players(), - rom_obj.get_number_of_players_online(), - rom_obj.get_esrb_rating(), - rom_obj.get_pegi_rating(), - rom_obj.get_platform(), - rom_obj.get_box_sizing(), - rom_obj.get_nointro_status(), - rom_obj.get_clone(), - rom_obj.get_rom_status(), - rom_obj.get_launch_count(), - rom_obj.get_last_launch_date(), - rom_obj.is_favourite(), - rom_obj.get_scanned_with(), - rom_obj.get_id()) + rom_obj.get_name(), + rom_obj.get_number_of_players(), + rom_obj.get_number_of_players_online(), + rom_obj.get_esrb_rating(), + rom_obj.get_pegi_rating(), + rom_obj.get_platform(), + rom_obj.get_box_sizing(), + rom_obj.get_nointro_status(), + rom_obj.get_clone(), + rom_obj.get_rom_status(), + rom_obj.get_launch_count(), + rom_obj.get_last_launch_date(), + rom_obj.is_favourite(), + rom_obj.get_scanned_by(), + rom_obj.get_id()) for asset in rom_obj.get_assets(): - if not asset.get_id(): self._insert_asset(asset, rom_obj) - else: self._update_asset(asset, rom_obj) + if not asset.get_id(): + self._insert_asset(asset, rom_obj) + else: + self._update_asset(asset, rom_obj) for asset_path in rom_obj.get_asset_paths(): - if not asset_path.get_id(): self._insert_asset_path(asset_path, rom_obj) - else: self._update_asset_path(asset_path, rom_obj) + if not asset_path.get_id(): + self._insert_asset_path(asset_path, rom_obj) + else: + self._update_asset_path(asset_path, rom_obj) for mapping in rom_obj.asset_mappings: if mapping.get_id() == '': @@ -1455,130 +1536,148 @@ def update_rom(self, rom_obj: ROM): tag_data = rom_obj.get_tag_data() self._update_tags(tag_data, rom_obj.get_custom_attribute('metadata_id')) - self._update_scanned_data(rom_obj.get_id(),rom_obj.scanned_data) + self._update_scanned_data(rom_obj.get_id(), rom_obj.scanned_data) self._update_launchers(rom_obj.get_id(), rom_obj.get_launchers()) def delete_rom(self, rom_id: str): self.logger.info("ROMsRepository.delete_rom(): Deleting ROM '{}'".format(rom_id)) self._uow.execute(qry.DELETE_ROM, rom_id) - def delete_roms_by_romcollection(self, romcollection_id:str): + def delete_roms_by_romcollection(self, romcollection_id: str): self._uow.execute(qry.DELETE_ROMS_BY_COLLECTION, romcollection_id) - def remove_launcher(self, rom_id: str, launcher_id:str): + def remove_launcher(self, rom_id: str, launcher_id: str): self._uow.execute(qry.DELETE_ROM_LAUNCHER, rom_id, launcher_id) - def insert_tag(self, tag:str) -> str: + def insert_tag(self, tag: str) -> str: db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_TAG, db_id, tag) return db_id - def delete_tag(self, tag_id:str): + def delete_tag(self, tag_id: str): self._uow.execute(qry.DELETE_TAG, tag_id) + def _process_roms_data(self, result_set, assets_result_set, asset_paths_result_set, + asset_mappings_result_set, scanned_data_result_set, tags_data_set) -> typing.Iterator[ROM]: + + for rom_data in result_set: + assets = [] + asset_paths = [] + asset_mappings = [] + tags = {} + for asset_data in filter(lambda a: a['rom_id'] == rom_data['id'], assets_result_set): + assets.append(Asset(asset_data)) + for asset_paths_data in filter(lambda a: a['rom_id'] == rom_data['id'], asset_paths_result_set): + asset_paths.append(AssetPath(asset_paths_data)) + for mapping_data in filter(lambda a: a['metadata_id'] == rom_data['metadata_id'], asset_mappings_result_set): + asset_mappings.append(RomAssetMapping(mapping_data)) + for tag in filter(lambda t: t['rom_id'] == rom_data['id'], tags_data_set): + tags[tag['tag']] = tag['id'] + + scanned_data = { + entry['data_key']: entry['data_value'] + for entry in filter(lambda s: s['rom_id'] == rom_data['id'], scanned_data_result_set) + } + yield ROM(rom_data, tags, assets, asset_paths, asset_mappings, scanned_data) + def _insert_asset(self, asset: Asset, rom_obj: ROM): asset_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET, asset_db_id, asset.get_path(), asset.get_asset_info_id()) - self._uow.execute(qry.INSERT_ROM_ASSET, rom_obj.get_id(), asset_db_id) + self._uow.execute(qry.INSERT_ROM_ASSET, rom_obj.get_id(), asset_db_id) def _update_asset(self, asset: Asset, rom_obj: ROM): self._uow.execute(qry.UPDATE_ASSET, asset.get_path(), asset.get_asset_info_id(), asset.get_id()) if asset.get_custom_attribute('rom_id') is None: - self._uow.execute(qry.INSERT_ROM_ASSET, rom_obj.get_id(), asset.get_id()) + self._uow.execute(qry.INSERT_ROM_ASSET, rom_obj.get_id(), asset.get_id()) def _insert_asset_path(self, asset_path: AssetPath, rom_obj: ROM): asset_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET_PATH, asset_db_id, asset_path.get_path(), asset_path.get_asset_info_id()) - self._uow.execute(qry.INSERT_ROM_ASSET_PATH, rom_obj.get_id(), asset_db_id) + self._uow.execute(qry.INSERT_ROM_ASSET_PATH, rom_obj.get_id(), asset_db_id) def _update_asset_path(self, asset_path: AssetPath, rom_obj: ROM): self._uow.execute(qry.UPDATE_ASSET_PATH, asset_path.get_path(), asset_path.get_asset_info_id(), asset_path.get_id()) if asset_path.get_custom_attribute('rom_id') is None: - self._uow.execute(qry.INSERT_ROM_ASSET_PATH, rom_obj.get_id(), asset_path.get_id()) + self._uow.execute(qry.INSERT_ROM_ASSET_PATH, rom_obj.get_id(), asset_path.get_id()) def _insert_asset_mapping(self, mapping: AssetMapping, obj: MetaDataItemABC): if not mapping.is_mapped(): return mapping_db_id = text.misc_generate_random_SID() self._uow.execute(qry.INSERT_ASSET_MAPPING, mapping_db_id, mapping.get_asset_info().id, mapping.get_mapped_to_asset_info().id) - self._uow.execute(qry.INSERT_MAPPING_WITH_METADATA, obj.get_metadata_id(), mapping_db_id) + self._uow.execute(qry.INSERT_MAPPING_WITH_METADATA, obj.get_metadata_id(), mapping_db_id) def _update_asset_mapping(self, mapping: AssetMapping, obj: MetaDataItemABC): if mapping.is_mapped(): - self._uow.execute(qry.UPDATE_ASSET_MAPPING, mapping.get_asset_info().id, mapping.get_mapped_to_asset_info().id, mapping.get_id()) + self._uow.execute(qry.UPDATE_ASSET_MAPPING, + mapping.get_asset_info().id, + mapping.get_mapped_to_asset_info().id, + mapping.get_id()) return - self._uow.execute(qry.DELETE_ASSET_MAPPING, mapping.get_id()) + self._uow.execute(qry.DELETE_ASSET_MAPPING, mapping.get_id()) - def _update_launchers(self, rom_id:str, rom_launchers:typing.List[ROMLauncherAddon]): + def _update_launchers(self, rom_id: str, rom_launchers: typing.List[ROMLauncherAddon]): for rom_launcher in rom_launchers: - if rom_launcher.get_id() is None: - rom_launcher.set_id(text.misc_generate_random_SID()) - self._uow.execute(qry.INSERT_ROM_LAUNCHER, - rom_launcher.get_id(), - rom_id, - rom_launcher.addon.get_id(), - rom_launcher.get_settings_str(), - rom_launcher.is_default()) + if rom_launcher.get_custom_attribute("rom_id") is None: + self._uow.execute(qry.INSERT_ROM_LAUNCHER, rom_launcher.get_id(), rom_id, rom_launcher.is_default()) else: - self._uow.execute(qry.UPDATE_ROM_LAUNCHER, - rom_launcher.get_settings_str(), - rom_launcher.is_default(), - rom_launcher.get_id()) - - def _update_scanned_data(self, rom_id:str, scanned_data:dict): + self._uow.execute(qry.UPDATE_ROM_LAUNCHER, rom_launcher.is_default(), rom_id, rom_launcher.get_id()) + + def _update_scanned_data(self, rom_id: str, scanned_data: dict): self._uow.execute(qry.DELETE_SCANNED_DATA, rom_id) for key, value in scanned_data.items(): self._uow.execute(qry.INSERT_ROM_SCANNED_DATA, rom_id, key, value) - def _update_tags(self, tag_data:dict, metadata_id:str): + def _update_tags(self, tag_data: dict, metadata_id: str): self._uow.execute(qry.DELETE_EXISTING_ROM_TAGS, metadata_id) self._insert_tags(tag_data, metadata_id) - def _insert_tags(self, tag_data:dict, metadata_id:str): - if tag_data is None: return + def _insert_tags(self, tag_data: dict, metadata_id: str): + if tag_data is None: + return existing_tags = self.find_all_tags() for tag_name, tag_id in tag_data.items(): if tag_id == '': - if not tag_name in existing_tags.keys(): + if tag_name not in existing_tags.keys(): tag_id = self.insert_tag(tag_name) else: tag_id = existing_tags[tag_name] self._uow.execute(qry.ADD_TAG_TO_ROM, metadata_id, tag_id) - def _get_queries_by_vcollection_type(self, vcollection:VirtualCollection) -> typing.Tuple[str, str]: + def _get_queries_by_vcollection_type(self, vcollection: VirtualCollection) -> typing.Tuple[str, str]: - vcollection_id = vcollection.get_id() - vcategory_id = vcollection.get_parent_id() + vcollection_id = vcollection.get_id() + vcategory_id = vcollection.get_parent_id() - if vcategory_id is not None: + if vcategory_id is not None: if vcategory_id == constants.VCATEGORY_TITLE_ID: - return qry.SELECT_BY_TITLE, qry.SELECT_BY_TITLE_ASSETS + return qry.SELECT_BY_TITLE, qry.SELECT_BY_TITLE_ASSETS if vcategory_id == constants.VCATEGORY_GENRE_ID: - return qry.SELECT_BY_GENRE, qry.SELECT_BY_GENRE_ASSETS + return qry.SELECT_BY_GENRE, qry.SELECT_BY_GENRE_ASSETS if vcategory_id == constants.VCATEGORY_DEVELOPER_ID: - return qry.SELECT_BY_DEVELOPER, qry.SELECT_BY_DEVELOPER_ASSETS + return qry.SELECT_BY_DEVELOPER, qry.SELECT_BY_DEVELOPER_ASSETS if vcategory_id == constants.VCATEGORY_ESRB_ID: return qry.SELECT_BY_ESRB, qry.SELECT_BY_ESRB_ASSETS if vcategory_id == constants.VCATEGORY_PEGI_ID: - return qry.SELECT_BY_PEGI, qry.SELECT_BY_PEGI_ASSETS + return qry.SELECT_BY_PEGI, qry.SELECT_BY_PEGI_ASSETS if vcategory_id == constants.VCATEGORY_YEARS_ID: - return qry.SELECT_BY_YEAR, qry.SELECT_BY_YEAR_ASSETS + return qry.SELECT_BY_YEAR, qry.SELECT_BY_YEAR_ASSETS if vcategory_id == constants.VCATEGORY_NPLAYERS_ID: - return qry.SELECT_BY_NPLAYERS, qry.SELECT_BY_NPLAYERS_ASSETS + return qry.SELECT_BY_NPLAYERS, qry.SELECT_BY_NPLAYERS_ASSETS if vcategory_id == constants.VCATEGORY_RATING_ID: return qry.SELECT_BY_RATING, qry.SELECT_BY_RATING_ASSETS else: if vcollection_id == constants.VCOLLECTION_FAVOURITES_ID: return qry.SELECT_MY_FAVOURITES, qry.SELECT_FAVOURITES_ROM_ASSETS if vcollection_id == constants.VCOLLECTION_RECENT_ID: - return qry.SELECT_RECENTLY_PLAYED_ROMS,qry.SELECT_RECENTLY_PLAYED_ROM_ASSETS + return qry.SELECT_RECENTLY_PLAYED_ROMS, qry.SELECT_RECENTLY_PLAYED_ROM_ASSETS if vcollection_id == constants.VCOLLECTION_MOST_PLAYED_ID: return qry.SELECT_MOST_PLAYED_ROMS, qry.SELECT_MOST_PLAYED_ROM_ASSETS return None, None - def _get_query_by_filter(self, filter:str) -> typing.Tuple[str, str]: + def _get_query_by_filter(self, filter: str) -> typing.Tuple[str, str]: if filter == constants.META_GENRE_ID: return qry.SELECT_GENRES_BY_COLLECTION if filter == constants.META_YEAR_ID: @@ -1588,19 +1687,20 @@ def _get_query_by_filter(self, filter:str) -> typing.Tuple[str, str]: if filter == constants.META_RATING_ID: return qry.SELECT_RATING_BY_COLLECTION return None - + + class AelAddonRepository(object): def __init__(self, uow: UnitOfWork): self._uow = uow self.logger = logging.getLogger(__name__) - def find(self, id:str) -> AelAddon: + def find(self, id: str) -> AelAddon: self._uow.execute(qry.SELECT_ADDON, id) result_set = self._uow.single_result() return AelAddon(result_set) - def find_by_addon_id(self, addon_id:str, type: constants.AddonType) -> AelAddon: + def find_by_addon_id(self, addon_id: str, type: constants.AddonType) -> AelAddon: self._uow.execute(qry.SELECT_ADDON_BY_ADDON_ID, addon_id, type.name) result_set = self._uow.single_result() if result_set is None: @@ -1613,41 +1713,201 @@ def find_all(self) -> typing.Iterator[AelAddon]: for addon_data in result_set: yield AelAddon(addon_data) - def find_all_launchers(self) -> typing.Iterator[AelAddon]: + def find_all_launcher_addons(self) -> typing.Iterator[AelAddon]: self._uow.execute(qry.SELECT_LAUNCHER_ADDONS) result_set = self._uow.result_set() for addon_data in result_set: yield AelAddon(addon_data) - def find_all_scanners(self) -> typing.Iterator[AelAddon]: + def find_all_scanner_addons(self) -> typing.Iterator[AelAddon]: self._uow.execute(qry.SELECT_SCANNER_ADDONS) result_set = self._uow.result_set() for addon_data in result_set: yield AelAddon(addon_data) - def find_all_scrapers(self) -> typing.Iterator[AelAddon]: + def find_all_scraper_addons(self) -> typing.Iterator[AelAddon]: self._uow.execute(qry.SELECT_SCRAPER_ADDONS) result_set = self._uow.result_set() for addon_data in result_set: yield AelAddon(addon_data) def insert_addon(self, addon: AelAddon): - self.logger.info("Saving addon '{}'".format(addon.get_addon_id())) + self.logger.info("Saving addon '{}'".format(addon.get_addon_id())) self._uow.execute(qry.INSERT_ADDON, - addon.get_id(), - addon.get_name(), - addon.get_addon_id(), - addon.get_version(), - addon.get_addon_type().name, - addon.get_extra_settings_str()) + addon.get_id(), + addon.get_name(), + addon.get_addon_id(), + addon.get_version(), + addon.get_addon_type().name, + addon.get_extra_settings_str()) def update_addon(self, addon: AelAddon): - self.logger.info("Updating addon '{}'".format(addon.get_addon_id())) - self.logger.info(f"EXTRA SETTINGS: {addon.get_extra_settings_str()}") + self.logger.info("Updating addon '{}'".format(addon.get_addon_id())) + self.logger.info(f"EXTRA SETTINGS: {addon.get_extra_settings_str()}") self._uow.execute(qry.UPDATE_ADDON, - addon.get_name(), - addon.get_addon_id(), - addon.get_version(), - addon.get_addon_type().name, - addon.get_extra_settings_str(), - addon.get_id()) + addon.get_name(), + addon.get_addon_id(), + addon.get_version(), + addon.get_addon_type().name, + addon.get_extra_settings_str(), + addon.get_id()) + + +class SourcesRepository(object): + + def __init__(self, uow: UnitOfWork): + self._uow = uow + self.logger = logging.getLogger(__name__) + + def find(self, id: str) -> Source: + if id is None or id == '': + return None + + self._uow.execute(qry.SELECT_SOURCE, id) + result_set = self._uow.single_result() + + self._uow.execute(qry.SELECT_SOURCE_ASSET_PATHS, id) + asset_paths_result_set = self._uow.result_set() + asset_paths = [] + for asset_paths_data in asset_paths_result_set: + asset_paths.append(AssetPath(asset_paths_data)) + + self._uow.execute(qry.SELECT_SOURCE_LAUNCHERS, id) + launchers_data = self._uow.result_set() + launchers = [] + for launcher_data in launchers_data: + addon = AelAddon(launcher_data.copy()) + launcher = ROMLauncherAddonFactory.create(addon, launcher_data) + launchers.append(launcher) + + addon = AelAddon(result_set.copy()) + return Source(result_set, addon, asset_paths, launchers) + + def find_all(self) -> typing.Iterator[Source]: + self._uow.execute(qry.SELECT_SOURCES) + result_sets = self._uow.result_set() + + for result_set in result_sets: + addon = AelAddon(result_set.copy()) + yield Source(result_set, addon) + + def find_sources_by_collection(self, romcollection_id) -> typing.Iterator[Source]: + self._uow.execute(qry.SELECT_SOURCES_BY_ROMCOLLECTION, romcollection_id) + result_sets = self._uow.result_set() + + for result_set in result_sets: + addon = AelAddon(result_set.copy()) + yield Source(result_set, addon) + + def find_romcollection_ids_by_source(self, source_id): + self._uow.execute(qry.SELECT_ROMCOLLECTION_IDS_BY_SOURCE, source_id) + result_sets = self._uow.result_set() + collection_ids = [] + for result_set in result_sets: + collection_ids.append(result_set['collection_id']) + return collection_ids + + def insert_source(self, source: Source): + self.logger.info(f"SourcesRepository.insert_source(): Inserting new source '{source.get_name()}'") + + assets_path = source.get_assets_root_path() + addon = source.get_addon() + + self._uow.execute(qry.INSERT_SOURCE, + source.get_id(), + source.get_name(), + source.get_platform(), + source.get_box_sizing(), + assets_path.getPath() if assets_path is not None else None, + source.get_last_scan_timestamp(), + source.get_settings_str(), + addon.get_id()) + + for asset_path in source.get_asset_paths(): + self._insert_asset_path(asset_path, source) + self._update_launchers(source.get_id(), source.get_launchers()) + + def update_source(self, source: Source): + self.logger.info(f"SourcesRepository.update_source(): Updating source '{source.get_name()}'") + assets_path = source.get_assets_root_path() + + self._uow.execute(qry.UPDATE_SOURCE, + source.get_name(), + source.get_platform(), + source.get_box_sizing(), + assets_path.getPath() if assets_path is not None else None, + source.get_last_scan_timestamp(), + source.get_settings_str(), + source.get_id()) + + for asset_path in source.get_asset_paths(): + if asset_path.get_id() == '': + self._insert_asset_path(asset_path, source) + else: + self._update_asset_path(asset_path, source) + self._update_launchers(source.get_id(), source.get_launchers()) + + def delete_source(self, source_id: str): + self.logger.info(f"SourcesRepository.delete_source(): Deleting source '{source_id}'") + self._uow.execute(qry.DELETE_SOURCE, source_id) + + def remove_all_roms_in_source(self, source_id: str): + self._uow.execute(qry.REMOVE_ROMS_FROM_SOURCE, source_id) + + def remove_launcher(self, source_id: str, launcher_id: str): + self._uow.execute(qry.DELETE_SOURCE_LAUNCHER, source_id, launcher_id) + + def _insert_asset_path(self, asset_path: AssetPath, source: Source): + asset_db_id = text.misc_generate_random_SID() + self._uow.execute(qry.INSERT_ASSET_PATH, asset_db_id, asset_path.get_path(), asset_path.get_asset_info_id()) + self._uow.execute(qry.INSERT_SOURCE_ASSET_PATH, source.get_id(), asset_db_id) + + def _update_asset_path(self, asset_path: AssetPath, source: Source): + self._uow.execute(qry.UPDATE_ASSET_PATH, asset_path.get_path(), asset_path.get_asset_info_id(), asset_path.get_id()) + if asset_path.get_custom_attribute('source_id') is None: + self._uow.execute(qry.INSERT_SOURCE_ASSET_PATH, source.get_id(), asset_path.get_id()) + + def _update_launchers(self, source_id: str, rom_launchers: typing.List[ROMLauncherAddon]): + for rom_launcher in rom_launchers: + if rom_launcher.get_custom_attribute("source_id") is None: + self._uow.execute(qry.INSERT_SOURCE_LAUNCHER, rom_launcher.get_id(), source_id, rom_launcher.is_default()) + else: + self._uow.execute(qry.UPDATE_SOURCE_LAUNCHER, rom_launcher.is_default(), source_id, rom_launcher.get_id()) + + +class LaunchersRepository(object): + + def __init__(self, uow: UnitOfWork): + self._uow = uow + self.logger = logging.getLogger(__name__) + + def find(self, id: str) -> ROMLauncherAddon: + if id is None: + return None + + self._uow.execute(qry.SELECT_LAUNCHER, id) + result_set = self._uow.single_result() + addon = AelAddon(result_set.copy()) + + return ROMLauncherAddon(result_set, addon) + + def find_all(self) -> typing.Iterator[ROMLauncherAddon]: + self._uow.execute(qry.SELECT_LAUNCHERS) + result_sets = self._uow.result_set() + + for result_set in result_sets: + addon = AelAddon(result_set.copy()) + yield ROMLauncherAddon(result_set, addon) + + def insert_launcher(self, launcher: ROMLauncherAddon): + self.logger.info(f"LaunchersRepository.insert_launcher(): Inserting new launcher '{launcher.get_name()}'") + addon = launcher.get_addon() + self._uow.execute(qry.INSERT_LAUNCHER, launcher.get_id(), launcher.get_name(), addon.get_id(), launcher.get_settings_str()) + + def update_launcher(self, launcher: ROMLauncherAddon): + self.logger.info(f"LaunchersRepository.update_launcher(): Updating launcher '{launcher.get_name()}'") + self._uow.execute(qry.UPDATE_LAUNCHER, launcher.get_name(), launcher.get_settings_str(), launcher.get_id()) + + def delete_launcher(self, launcher: ROMLauncherAddon): + self.logger.info(f"LaunchersRepository.delete_launcher(): Deleting source '{launcher.get_id()}'") + self._uow.execute(qry.DELETE_LAUNCHER, launcher.get_id()) diff --git a/resources/lib/services.py b/resources/lib/services.py index 89b9be69..f06ca4a3 100644 --- a/resources/lib/services.py +++ b/resources/lib/services.py @@ -65,7 +65,7 @@ def run(self): if db_version is None or LooseVersion(db_version) < LooseVersion(globals.addon_version): try: self._do_version_upgrade(uow, LooseVersion(db_version)) - except: + except Exception: logger.exception("Failure while doing database migration") kodi.notify_error(kodi.translate(40954)) @@ -157,7 +157,7 @@ def _last_time_scanned_is_too_long_ago(self): else: logger.info(f'Skipping automatic scan and view generation. Last scan was {now-then} days ago') return too_long_ago - + class AppMonitor(xbmc.Monitor): def __init__(self, *args, **kwargs): diff --git a/resources/lib/viewqueries.py b/resources/lib/viewqueries.py index 5fd9b10a..a1d70f48 100644 --- a/resources/lib/viewqueries.py +++ b/resources/lib/viewqueries.py @@ -34,7 +34,7 @@ from resources.lib import globals from resources.lib.commands.mediator import AppMediator from resources.lib.commands import view_rendering_commands -from resources.lib.repositories import ViewRepository, UnitOfWork, ROMsRepository, g_assetFactory +from resources.lib.repositories import ViewRepository, UnitOfWork, ROMsRepository, LaunchersRepository, g_assetFactory logger = logging.getLogger(__name__) @@ -58,9 +58,51 @@ def qry_get_root_items(): AppMediator.async_cmd('RENDER_VIEWS') listitem_fanart = globals.g_PATHS.FANART_FILE_PATH.getPath() + + listitem_name = kodi.translate(40914) + container['items'].append({ + 'name': listitem_name, + 'url': globals.router.url_for_path('sources'), + 'is_folder': True, + 'type': 'video', + 'info': { + 'title': listitem_name, + 'plot': kodi.translate(44032), + 'overlay': 4 + }, + 'art': { + 'fanart': listitem_fanart, + 'icon': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_icon.png').getPath(), + 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_poster.png').getPath() + }, + 'properties': { + 'obj_type': constants.OBJ_SOURCE + } + }) + + listitem_name = kodi.translate(40920) + container['items'].append({ + 'name': listitem_name, + 'url': globals.router.url_for_path('launchers'), + 'is_folder': True, + 'type': 'video', + 'info': { + 'title': listitem_name, + 'plot': kodi.translate(44033), + 'overlay': 4 + }, + 'art': { + 'fanart': listitem_fanart, + 'icon': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_icon.png').getPath(), + 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_poster.png').getPath() + }, + 'properties': { + 'obj_type': constants.OBJ_LAUNCHER + } + }) - if not settings.getSettingAsBool('display_hide_utilities'): - listitem_name = kodi.translate(40897) + if not settings.getSettingAsBool('display_hide_utilities'): + listitem_name = kodi.translate(40897) container['items'].append({ 'name': listitem_name, 'url': globals.router.url_for_path('utilities'), @@ -68,35 +110,39 @@ def qry_get_root_items(): 'type': 'video', 'info': { 'title': listitem_name, - 'plot': kodi.translate(42001), + 'plot': kodi.translate(44001), 'overlay': 4 }, - 'art': { - 'fanart' : listitem_fanart, - 'icon' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_icon.png').getPath(), - 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_poster.png').getPath() + 'art': { + 'fanart': listitem_fanart, + 'icon': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_icon.png').getPath(), + 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_poster.png').getPath() }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_CATEGORY, 'obj_type': constants.OBJ_NONE } + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) - if not settings.getSettingAsBool('display_hide_g_reports'): - listitem_name = kodi.translate(40898) + if not settings.getSettingAsBool('display_hide_g_reports'): + listitem_name = kodi.translate(40898) container['items'].append({ 'name': listitem_name, - 'url': globals.router.url_for_path('globalreports'), #SHOW_GLOBALREPORTS_VLAUNCHERS' + 'url': globals.router.url_for_path('globalreports'), # SHOW_GLOBALREPORTS_VLAUNCHERS' 'is_folder': True, 'type': 'video', 'info': { 'title': listitem_name, - 'plot': kodi.translate(42002), + 'plot': kodi.translate(44002), 'overlay': 4 }, - 'art': { - 'fanart' : listitem_fanart, - 'icon' : globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_icon.png').getPath(), - 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_poster.png').getPath() + 'art': { + 'fanart': listitem_fanart, + 'icon': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_icon.png').getPath(), + 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_poster.png').getPath() }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_CATEGORY, 'obj_type': constants.OBJ_NONE } + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) return container @@ -105,11 +151,12 @@ def qry_get_root_items(): # # View pre-rendered items. # -def qry_get_view_items(view_id: str, is_virtual_view=False): +def qry_get_view_items(view_id: str, obj_type: int): views_repository = ViewRepository(globals.g_PATHS) - container = views_repository.find_items(view_id, is_virtual_view) + container = views_repository.find_items(view_id, obj_type) return container + # # DB based items # @@ -130,6 +177,7 @@ def qry_get_view_item(rom_id: str): return container + def qry_get_view_metadata(rom_id: str): uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) container = None @@ -138,59 +186,59 @@ def qry_get_view_metadata(rom_id: str): rom = roms_repository.find_rom(rom_id) items = [] - items.append({ - 'id': 40801, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_GENRE_ID}/items?value={rom.get_genre()}'), - 'name': kodi.translate(40801), 'name2': rom.get_genre(), - 'info': {}, 'art': {}, 'properties': {'field': 'genre'}}) - items.append({ - 'id': 40803, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_YEARS_ID}/items?value={rom.get_releaseyear()}'), - 'name': kodi.translate(40803), 'name2': rom.get_releaseyear(), - 'info': {}, 'art': {}, 'properties': {'field': 'releaseyear'}}) - items.append({ - 'id': 40802, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_DEVELOPER_ID}/items?value={rom.get_developer()}'), - 'name': kodi.translate(40802), 'name2': rom.get_developer(), - 'info': {}, 'art': {}, 'properties': {'field': 'developer'}}) - items.append({ - 'id': 40806, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_RATING_ID}/items?value={rom.get_rating()}'), - 'name': kodi.translate(40806), 'name2': str(rom.get_rating()), - 'info': {}, 'art': {}, 'properties': {'field': 'rating'}}) - items.append({ - 'id': 40804, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_ESRB_ID}/items?value={rom.get_esrb_rating()}'), - 'name': kodi.translate(40804), 'name2': rom.get_esrb_rating(), - 'info': {}, 'art': {}, 'properties': {'field': 'esrb'}}) - items.append({ - 'id': 40805, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_PEGI_ID}/items?value={rom.get_pegi_rating()}'), - 'name': kodi.translate(40805), 'name2': rom.get_pegi_rating(), - 'info': {}, 'art': {}, 'properties': {'field': 'pegi'}}) - items.append({ - 'id': 40808, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/{constants.VCATEGORY_NPLAYERS_ID}/items?value={rom.get_number_of_players()}'), - 'name': kodi.translate(40808), 'name2': str(rom.get_number_of_players()), - 'info': {}, 'art': {}, 'properties': {'field': 'nplayers'}}) - items.append({ - 'id': 40809, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path('execute/command/reset_database'), - 'name': kodi.translate(40809), 'name2': str(rom.get_number_of_players_online()), - 'info': {}, 'art': {}, 'properties': {'field': 'nplayers_online'}}) - items.append({ - 'id': 40810, 'is_folder': False, 'type': 'game', - 'url': globals.router.url_for_path( - f'/collection/virtual/items?value={rom.get_genre()}'), - 'name': kodi.translate(40810), 'name2': ','.join(rom.get_tags()), - 'info': {}, 'art': {}, 'properties': {'field': 'tags'}}) + items.append({ + 'id': 40801, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_GENRE_ID}/items?value={rom.get_genre()}'), + 'name': kodi.translate(40801), 'name2': rom.get_genre(), + 'info': {}, 'art': {}, 'properties': {'field': 'genre'}}) + items.append({ + 'id': 40803, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_YEARS_ID}/items?value={rom.get_releaseyear()}'), + 'name': kodi.translate(40803), 'name2': rom.get_releaseyear(), + 'info': {}, 'art': {}, 'properties': {'field': 'releaseyear'}}) + items.append({ + 'id': 40802, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_DEVELOPER_ID}/items?value={rom.get_developer()}'), + 'name': kodi.translate(40802), 'name2': rom.get_developer(), + 'info': {}, 'art': {}, 'properties': {'field': 'developer'}}) + items.append({ + 'id': 40806, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_RATING_ID}/items?value={rom.get_rating()}'), + 'name': kodi.translate(40806), 'name2': str(rom.get_rating()), + 'info': {}, 'art': {}, 'properties': {'field': 'rating'}}) + items.append({ + 'id': 40804, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_ESRB_ID}/items?value={rom.get_esrb_rating()}'), + 'name': kodi.translate(40804), 'name2': rom.get_esrb_rating(), + 'info': {}, 'art': {}, 'properties': {'field': 'esrb'}}) + items.append({ + 'id': 40805, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_PEGI_ID}/items?value={rom.get_pegi_rating()}'), + 'name': kodi.translate(40805), 'name2': rom.get_pegi_rating(), + 'info': {}, 'art': {}, 'properties': {'field': 'pegi'}}) + items.append({ + 'id': 40808, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/{constants.VCATEGORY_NPLAYERS_ID}/items?value={rom.get_number_of_players()}'), + 'name': kodi.translate(40808), 'name2': str(rom.get_number_of_players()), + 'info': {}, 'art': {}, 'properties': {'field': 'nplayers'}}) + items.append({ + 'id': 40809, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path('execute/command/reset_database'), + 'name': kodi.translate(40809), 'name2': str(rom.get_number_of_players_online()), + 'info': {}, 'art': {}, 'properties': {'field': 'nplayers_online'}}) + items.append({ + 'id': 40810, 'is_folder': False, 'type': 'game', + 'url': globals.router.url_for_path( + f'/collection/virtual/items?value={rom.get_genre()}'), + 'name': kodi.translate(40810), 'name2': ','.join(rom.get_tags()), + 'info': {}, 'art': {}, 'properties': {'field': 'tags'}}) container = { 'id': rom_id, @@ -215,7 +263,7 @@ def qry_get_view_assets(rom_id: str): for asset_id in asset_ids: asset = next((a for a in assigned_assets if a.get_asset_info_id() == asset_id), None) asset_info = asset.asset_info if asset else g_assetFactory.get_asset_info(asset_id) - items.append({ + items.append({ 'id': asset_id, 'is_folder': False, 'type': 'pictures', @@ -227,10 +275,10 @@ def qry_get_view_assets(rom_id: str): 'title': kodi.translate(asset_info.name_id), 'picturepath': asset.get_path() if asset else None, }, - 'art': { + 'art': { 'thumb': asset.get_path() if asset else 'DefaultAddonImages.png' }, - 'properties': { + 'properties': { 'is_set': str(asset and asset.is_assigned()), 'assetid': asset_id } @@ -255,9 +303,9 @@ def qry_get_view_scanned_data(rom_id: str): scanned_data = rom.get_scanned_data() items = [] for key, value in scanned_data.items(): - items.append({ + items.append({ 'is_folder': False, 'type': 'game', - 'name': key, 'name2': value, + 'name': key, 'name2': value, 'url': globals.router.url_for_path( f'/rom/{rom.get_id()}/view/scanneddata?field={key}'), 'info': {}, 'art': {}, 'properties': {}}) @@ -271,12 +319,74 @@ def qry_get_view_scanned_data(rom_id: str): return container +# +# Source items +# +def qry_get_sources(): + views_repository = ViewRepository(globals.g_PATHS) + container = views_repository.find_sources_items() + + if container is None: + container = { + 'id': '', + 'name': kodi.translate(constants.OBJ_SOURCE), + 'obj_type': constants.OBJ_SOURCE, + 'items': [] + } + return container + + +# +# Launcher items +# +def qry_get_launchers(): + uow = UnitOfWork(globals.g_PATHS.DATABASE_FILE_PATH) + container = None + with uow: + repository = LaunchersRepository(uow) + launchers = repository.find_all() + + container = { + 'id': '', + 'name': kodi.translate(constants.OBJ_LAUNCHER), + 'obj_type': constants.OBJ_LAUNCHER, + 'items': [] + } + + listitem_fanart = globals.g_PATHS.FANART_FILE_PATH.getPath() + + for launcher in launchers: + listitem_name = launcher.get_name() + container['items'].append({ + 'id': launcher.get_id(), + 'name': listitem_name, + 'url': globals.router.url_for_path(f'/launcher/edit/{launcher.get_id()}'), + 'is_folder': False, + 'type': 'video', + 'info': { + 'title': listitem_name, + 'plot': f'Launcher of type {launcher.addon.get_addon_type()}', + 'overlay': 4 + }, + 'art': { + 'fanart': listitem_fanart, + 'icon': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_icon.png').getPath(), + 'poster': globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Sources_poster.png').getPath() + }, + 'properties': { + 'obj_type': constants.OBJ_LAUNCHER + } + }) + + return container + + # # Utilities items # def qry_get_utilities_items(): # --- Common artwork for all Utilities VLaunchers --- - listitem_icon = globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_icon.png').getPath() + listitem_icon = globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_icon.png').getPath() listitem_fanart = globals.g_PATHS.FANART_FILE_PATH.getPath() listitem_poster = globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Utilities_poster.png').getPath() @@ -298,11 +408,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40899), - 'plot': kodi.translate(42003), + 'plot': kodi.translate(44003), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40856), @@ -311,11 +423,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40856), - 'plot': kodi.translate(42004), + 'plot': kodi.translate(44004), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40900), @@ -324,11 +438,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40900), - 'plot': kodi.translate(42018), + 'plot': kodi.translate(44018), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40901), @@ -337,11 +453,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40901), - 'plot': kodi.translate(42019), + 'plot': kodi.translate(44019), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40902), @@ -350,11 +468,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40902), - 'plot': kodi.translate(42020), + 'plot': kodi.translate(44020), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40903), @@ -363,11 +483,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40903), - 'plot': kodi.translate(42021), + 'plot': kodi.translate(44021), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40904), @@ -376,24 +498,28 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40904), - 'plot': kodi.translate(42022), + 'plot': kodi.translate(44022), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40905), - 'url': globals.router.url_for_path('execute/command/export_to_legacy_xml'), + 'url': globals.router.url_for_path('execute/command/export_to_legacy_xml'), 'is_folder': False, 'type': 'video', 'info': { 'title': kodi.translate(40905), - 'plot': kodi.translate(42023), + 'plot': kodi.translate(44023), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40906), @@ -402,11 +528,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40906), - 'plot': kodi.translate(42024), + 'plot': kodi.translate(44024), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40907), @@ -415,11 +543,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40907), - 'plot': kodi.translate(42025), + 'plot': kodi.translate(44025), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40908), @@ -427,12 +557,14 @@ def qry_get_utilities_items(): 'is_folder': False, 'type': 'video', 'info': { - 'title': kodi.translate(40908), - 'plot': kodi.translate(42026), + 'title': kodi.translate(40908), + 'plot': kodi.translate(44026), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ 'name': kodi.translate(40909), @@ -441,11 +573,13 @@ def qry_get_utilities_items(): 'type': 'video', 'info': { 'title': kodi.translate(40909), - 'plot': kodi.translate(42027), + 'plot': kodi.translate(44027), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) return container @@ -455,8 +589,8 @@ def qry_get_utilities_items(): # Global Reports items # def qry_get_globalreport_items(): - # --- Common artwork for all Utilities VLaunchers --- - listitem_icon = globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_icon.png').getPath() + # --- Common artwork for all Utilities VLaunchers --- + listitem_icon = globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_icon.png').getPath() listitem_fanart = globals.g_PATHS.FANART_FILE_PATH.getPath() listitem_poster = globals.g_PATHS.ADDON_CODE_DIR.pjoin('media/theme/Global_Reports_poster.png').getPath() @@ -475,11 +609,13 @@ def qry_get_globalreport_items(): 'type': 'video', 'info': { 'title': kodi.translate(40910), - 'plot': kodi.translate(42028), + 'plot': kodi.translate(44028), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) # --- Global ROM Audit statistics --- @@ -490,11 +626,13 @@ def qry_get_globalreport_items(): 'type': 'video', 'info': { 'title': kodi.translate(40911), - 'plot': kodi.translate(42029), + 'plot': kodi.translate(44029), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ @@ -504,11 +642,13 @@ def qry_get_globalreport_items(): 'type': 'video', 'info': { 'title': kodi.translate(40912), - 'plot': kodi.translate(42030), + 'plot': kodi.translate(44030), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) container['items'].append({ @@ -518,11 +658,13 @@ def qry_get_globalreport_items(): 'type': 'video', 'info': { 'title': kodi.translate(40913), - 'plot': kodi.translate(42031), + 'plot': kodi.translate(44031), 'overlay': 4 }, - 'art': { 'icon' : listitem_icon, 'fanart' : listitem_fanart, 'poster' : listitem_poster }, - 'properties': { constants.AKL_CONTENT_LABEL: constants.AKL_CONTENT_VALUE_NONE, 'obj_type': constants.OBJ_NONE } + 'art': {'icon': listitem_icon, 'fanart': listitem_fanart, 'poster': listitem_poster}, + 'properties': { + 'obj_type': constants.OBJ_NONE + } }) return container @@ -530,7 +672,7 @@ def qry_get_globalreport_items(): # # Default context menu items for the whole container. # -def qry_container_context_menu_items(container_data) -> typing.List[typing.Tuple[str,str]]: +def qry_container_context_menu_items(container_data) -> typing.List[typing.Tuple[str, str]]: if container_data is None: return [] # --- Create context menu items to be applied to each item in this container --- @@ -539,27 +681,29 @@ def qry_container_context_menu_items(container_data) -> typing.List[typing.Tuple container_id = container_data['id'] if 'id' in container_data else '' container_parentid = container_data['parent_id'] if 'parent_id' in container_data else '' - is_category: bool = container_type == constants.OBJ_CATEGORY - is_romcollection: bool = container_type == constants.OBJ_ROMCOLLECTION - is_virtual_category: bool = container_type == constants.OBJ_CATEGORY_VIRTUAL - is_virtual_collection: bool = container_type == constants.OBJ_COLLECTION_VIRTUAL is_root: bool = container_data['id'] == '' commands = [] - if is_category: + if container_type == constants.OBJ_CATEGORY: commands.append((kodi.translate(40893).format(container_name), - _context_menu_url_for('execute/command/render_category_view',{'category_id':container_id}))) + _context_menu_url_for('execute/command/render_category_view', {'category_id': container_id}))) + + if container_type == constants.OBJ_SOURCE and is_root: + commands.append((kodi.translate(40916), _context_menu_url_for('/execute/command/add_source'))) - if is_romcollection: + if container_type == constants.OBJ_LAUNCHER and is_root: + commands.append((kodi.translate(40917), _context_menu_url_for('/execute/command/add_launcher'))) + + if container_type == constants.OBJ_ROMCOLLECTION: commands.append((kodi.translate(40894), _context_menu_url_for(f'/collection/{container_id}/search'))) commands.append((kodi.translate(40893).format(container_name), - _context_menu_url_for('execute/command/render_romcollection_view', {'romcollection_id':container_id}))) - if is_virtual_category and not is_root: + _context_menu_url_for('execute/command/render_romcollection_view', {'romcollection_id': container_id}))) + if container_type == constants.OBJ_CATEGORY_VIRTUAL and not is_root: commands.append((kodi.translate(40893).format(container_name), - _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':container_id}))) - if is_virtual_collection: + _context_menu_url_for('execute/command/render_vcategory_view', {'vcategory_id': container_id}))) + if container_type == constants.OBJ_COLLECTION_VIRTUAL: commands.append((kodi.translate(40893).format(container_name), - _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':container_parentid}))) + _context_menu_url_for('execute/command/render_vcategory_view', {'vcategory_id': container_parentid}))) commands.append((kodi.translate(40856), _context_menu_url_for('execute/command/render_views'))) commands.append((kodi.translate(40895), 'ActivateWindow(filemanager)')) @@ -571,7 +715,7 @@ def qry_container_context_menu_items(container_data) -> typing.List[typing.Tuple # # ListItem specific context menu items. # -def qry_listitem_context_menu_items(list_item_data, container_data)-> typing.List[typing.Tuple[str,str]]: +def qry_listitem_context_menu_items(list_item_data, container_data) -> typing.List[typing.Tuple[str, str]]: if container_data is None or list_item_data is None: return [] # --- Create context menu items only applicable on this item --- @@ -580,43 +724,55 @@ def qry_listitem_context_menu_items(list_item_data, container_data)-> typing.Lis item_name = list_item_data['name'] if 'name' in list_item_data else 'Unknown' item_id = list_item_data['id'] if 'id' in list_item_data else '' - container_id = container_data['id'] if 'id' in container_data else constants.VCATEGORY_ADDONROOT_ID + container_id = container_data['id'] if 'id' in container_data else '' container_type = container_data['obj_type'] if 'obj_type' in container_data else constants.OBJ_NONE - if container_id == '': - container_id = constants.VCATEGORY_ADDONROOT_ID container_is_category: bool = container_type == constants.OBJ_CATEGORY is_category: bool = item_type == constants.OBJ_CATEGORY + is_source: bool = item_type == constants.OBJ_SOURCE + is_launcher: bool = item_type == constants.OBJ_LAUNCHER is_romcollection: bool = item_type == constants.OBJ_ROMCOLLECTION is_virtual_category: bool = item_type == constants.OBJ_CATEGORY_VIRTUAL is_rom: bool = item_type == constants.OBJ_ROM commands = [] - if is_rom: + if is_rom: commands.append((kodi.translate(40882), _context_menu_url_for(f'/rom/view/{item_id}'))) commands.append((kodi.translate(40883), _context_menu_url_for(f'/rom/edit/{item_id}'))) - commands.append((kodi.translate(40884), _context_menu_url_for('/execute/command/link_rom',{'rom_id':item_id}))) - commands.append((kodi.translate(40885), _context_menu_url_for('/execute/command/add_rom_to_favourites',{'rom_id':item_id}))) + commands.append((kodi.translate(40884), _context_menu_url_for('/execute/command/link_rom', {'rom_id': item_id}))) + commands.append((kodi.translate(40885), _context_menu_url_for('/execute/command/add_rom_to_favourites', {'rom_id': item_id}))) - if is_category: + if is_category: commands.append((kodi.translate(40886), _context_menu_url_for(f'/categories/view/{item_id}'))) commands.append((kodi.translate(40887), _context_menu_url_for(f'/categories/edit/{item_id}'))) - commands.append((kodi.translate(40888),_context_menu_url_for(f'/categories/add/{item_id}/in/{container_id}'))) + commands.append((kodi.translate(40888), _context_menu_url_for(f'/categories/add/{item_id}/in/{container_id}'))) commands.append((kodi.translate(40889), _context_menu_url_for(f'/romcollection/add/{item_id}/in/{container_id}'))) - commands.append((kodi.translate(40890), _context_menu_url_for(f'/categories/addrom/{item_id}/in/{container_id}'))) - if is_romcollection: + if is_romcollection: commands.append((kodi.translate(40891), _context_menu_url_for(f'/romcollection/view/{item_id}'))) commands.append((kodi.translate(40892), _context_menu_url_for(f'/romcollection/edit/{item_id}'))) + if is_source: + if not item_id or len(item_id) == 0: + commands.append((kodi.translate(40916), _context_menu_url_for('/execute/command/add_library'))) + if item_id and len(item_id) > 0: + commands.append((kodi.translate(40915), _context_menu_url_for(f'/source/edit/{item_id}'))) + + if is_launcher: + if not item_id or len(item_id) == 0: + commands.append((kodi.translate(40917), _context_menu_url_for('/execute/command/add_launcher'))) + if item_id and len(item_id) > 0: + commands.append((kodi.translate(40918), _context_menu_url_for(f'/launcher/edit/{item_id}'))) + commands.append((kodi.translate(40919), _context_menu_url_for(f'/launcher/delete/{item_id}'))) + if not is_category and container_is_category: - commands.append((kodi.translate(40888),_context_menu_url_for(f'/categories/add/{container_id}'))) + commands.append((kodi.translate(40888), _context_menu_url_for(f'/categories/add/{container_id}'))) commands.append((kodi.translate(40889), _context_menu_url_for(f'/romcollection/add/{container_id}'))) - commands.append((kodi.translate(40890), _context_menu_url_for(f'/categories/addrom/{container_id}'))) if is_virtual_category: - commands.append((kodi.translate(40893).format(item_name), _context_menu_url_for('execute/command/render_vcategory_view',{'vcategory_id':item_id}))) + commands.append((kodi.translate(40893).format(item_name), _context_menu_url_for('execute/command/render_vcategory_view', { + 'vcategory_id': item_id}))) return commands @@ -625,4 +781,4 @@ def _context_menu_url_for(url: str, params: dict = None) -> str: if params is not None: url = '{}?{}'.format(url, urlencode(params)) url = globals.router.url_for_path(url) - return f'RunPlugin({url})' \ No newline at end of file + return f'RunPlugin({url})' diff --git a/resources/lib/views.py b/resources/lib/views.py index f4493acd..6aaa8aae 100644 --- a/resources/lib/views.py +++ b/resources/lib/views.py @@ -22,8 +22,8 @@ # Views.py contains all methods accessible by URL commands (using routes/paths) # triggered from Kodi. The methods will only perform operations to render and visualize # the list items in the containers. -# AKL follows a (sortof) CQRS architecture, meaning that all actions to gather the items -# or to perform commands are delegated to the queries.py file and different **_commands.py +# AKL follows a (sortof) CQRS architecture, meaning that all actions to gather the items +# or to perform commands are delegated to the queries.py file and different **_commands.py # files. Query methods are called directly and the Command methods are called through # sending notifications to the AKL service (Monitor). # @@ -35,7 +35,6 @@ import sys import abc import logging -import typing # --- Kodi stuff --- import xbmc @@ -52,6 +51,7 @@ logger = logging.getLogger(__name__) + # --------------------------------------------------------------------------------------------- # This is the plugin entry point. # --------------------------------------------------------------------------------------------- @@ -60,10 +60,10 @@ def run_plugin(addon_argv): logger.debug('------------ Called Advanced Kodi Launcher run_plugin(addon_argv) ------------') logger.debug(f'addon.id "{globals.addon_id}"') logger.debug(f'addon.version "{globals.addon_version}"') - for i in range(len(sys.argv)): + for i in range(len(sys.argv)): logger.debug(f'sys.argv[{i}] "{sys.argv[i]}"') - # --- Bootstrap object instances --- + # --- Bootstrap object instances --- globals.g_bootstrap_instances() argv = None @@ -81,6 +81,7 @@ def run_plugin(addon_argv): logger.debug('Advanced Kodi Launcher run_plugin() exit') + # ------------------------------------------------------------------------------------------------- # LisItem rendering # ------------------------------------------------------------------------------------------------- @@ -91,15 +92,18 @@ def vw_route_render_root(): container_context_items = viewqueries.qry_container_context_menu_items(container) _render_list_items(container, container_context_items) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + @router.route('/category/') @router.route('/collection/') -def vw_route_render_collection(view_id: str): - logger.debug("Executing route: vw_route_render_collection") - container = viewqueries.qry_get_view_items(view_id) +@router.route('/source/') +def vw_route_render_view(view_id: str): + logger.debug("Executing route: vw_route_render_view") + obj_type = vw_get_object_type_by_url(router.path) + container = viewqueries.qry_get_view_items(view_id, obj_type) container_context_items = viewqueries.qry_container_context_menu_items(container) - container_type = container['obj_type'] if 'obj_type' in container else constants.OBJ_NONE + container_type = container['obj_type'] if 'obj_type' in container else constants.OBJ_NONE filter_type = router.args['filter'][0] if 'filter' in router.args else None filter_term = router.args['term'][0] if 'term' in router.args else None @@ -115,21 +119,24 @@ def vw_route_render_collection(view_id: str): else: _render_list_items(container, container_context_items, filter) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + @router.route('/collection//search') def vw_route_search_collection(view_id: str): logger.debug("Executing route: vw_route_search_collection") - #vw_route_render_collection(view_id) + # vw_route_render_collection(view_id) AppMediator.sync_cmd('SEARCH', {'romcollection_id': view_id}) kodi.refresh_container() + @router.route('/category/virtual/') @router.route('/collection/virtual/') def vw_route_render_virtual_view(view_id: str): - container = viewqueries.qry_get_view_items(view_id, is_virtual_view=True) + obj_type = vw_get_object_type_by_url(router.path) + container = viewqueries.qry_get_view_items(view_id, obj_type) container_context_items = viewqueries.qry_container_context_menu_items(container) - container_type = container['obj_type'] if 'obj_type' in container else constants.OBJ_NONE + container_type = container['obj_type'] if 'obj_type' in container else constants.OBJ_NONE filter_type = router.args['filter'][0] if 'filter' in router.args else None filter_term = router.args['term'][0] if 'term' in router.args else None @@ -147,8 +154,9 @@ def vw_route_render_virtual_view(view_id: str): else: _render_list_items(container, container_context_items, filter) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) - + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + + @router.route('/collection/virtual//items') def vw_route_render_virtual_items_view(category_id: str): collection_value = router.args["value"][0] @@ -161,7 +169,32 @@ def vw_route_render_virtual_items_view(category_id: str): _render_list_items(container, container_context_items, filter) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + + +# ------------------------------------------------------------------------------------------------- +# Sources +# ------------------------------------------------------------------------------------------------- +@router.route('/sources') +def vw_route_render_sources(): + container = viewqueries.qry_get_sources() + container_context_items = viewqueries.qry_container_context_menu_items(container) + + _render_list_items(container, container_context_items) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + + +# ------------------------------------------------------------------------------------------------- +# Launchers +# ------------------------------------------------------------------------------------------------- +@router.route('/launchers') +def vw_route_render_launchers(): + container = viewqueries.qry_get_launchers() + container_context_items = viewqueries.qry_container_context_menu_items(container) + + _render_list_items(container, container_context_items) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + # ------------------------------------------------------------------------------------------------- # Utilities and Global reports @@ -172,7 +205,8 @@ def vw_route_render_utilities(): container_context_items = viewqueries.qry_container_context_menu_items(container) _render_list_items(container, container_context_items) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + @router.route('/globalreports') def vw_route_render_globalreports(): @@ -180,16 +214,18 @@ def vw_route_render_globalreports(): container_context_items = viewqueries.qry_container_context_menu_items(container) _render_list_items(container, container_context_items) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + # ------------------------------------------------------------------------------------------------- # Command execution # ------------------------------------------------------------------------------------------------- @router.route('/execute/command/') -def vw_execute_cmd(cmd: str): - cmd_args = { arg: router.args[arg][0] for arg in router.args } +def vw_execute_cmd(cmd: str): + cmd_args = {arg: router.args[arg][0] for arg in router.args} AppMediator.async_cmd(cmd.capitalize(), cmd_args) + @router.route('/categories/add') @router.route('/categories/add/') @router.route('/categories/add//in') @@ -197,15 +233,11 @@ def vw_execute_cmd(cmd: str): def vw_add_category(category_id: str = None, parent_category_id: str = None): AppMediator.async_cmd('ADD_CATEGORY', {'category_id': category_id, 'parent_category_id': parent_category_id}) + @router.route('/categories/edit/') def vw_edit_category(category_id: str): - AppMediator.async_cmd('EDIT_CATEGORY', {'category_id': category_id }) + AppMediator.async_cmd('EDIT_CATEGORY', {'category_id': category_id}) -@router.route('/categories/addrom/') -@router.route('/categories/addrom//in') -@router.route('/categories/addrom//in/') -def vw_add_rom_to_category(category_id: str = None, parent_category_id: str = None): - AppMediator.async_cmd('ADD_STANDALONE_ROM', {'category_id': category_id, 'parent_category_id': parent_category_id}) @router.route('/romcollection/add') @router.route('/romcollection/add/') @@ -214,25 +246,43 @@ def vw_add_rom_to_category(category_id: str = None, parent_category_id: str = No def vw_add_romcollection(category_id: str = None, parent_category_id: str = None): AppMediator.async_cmd('ADD_ROMCOLLECTION', {'category_id': category_id, 'parent_category_id': parent_category_id}) + @router.route('/romcollection/view/') def vw_view_romcollection(romcollection_id: str): - #todo pass + @router.route('/romcollection/edit/') def vw_edit_romcollection(romcollection_id: str): - AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id }) + AppMediator.async_cmd('EDIT_ROMCOLLECTION', {'romcollection_id': romcollection_id}) + + +@router.route('/source/edit/') +def vw_edit_source(source_id: str): + AppMediator.async_cmd('EDIT_SOURCE', {'source_id': source_id}) + + +@router.route('/launcher/edit/') +def vw_edit_launcher(launcher_id: str): + AppMediator.async_cmd('EDIT_LAUNCHER', {'launcher_id': launcher_id}) + + +@router.route('/launcher/delete/') +def vw_delete_launcher(launcher_id: str): + AppMediator.async_cmd('DELETE_LAUNCHER', {'launcher_id': launcher_id}) + @router.route('/rom/edit/') def vw_edit_rom(rom_id: str): - AppMediator.async_cmd('EDIT_ROM', {'rom_id': rom_id }) + AppMediator.async_cmd('EDIT_ROM', {'rom_id': rom_id}) + # ------------------------------------------------------------------------------------------------- # ROM execution / view -# ------------------------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------------------------- @router.route('/execute/rom/') def vw_route_execute_rom(rom_id): - AppMediator.async_cmd("EXECUTE_ROM", {'rom_id': rom_id} ) + AppMediator.async_cmd("EXECUTE_ROM", {'rom_id': rom_id}) @router.route('/rom/view/') @@ -240,7 +290,7 @@ def vw_view_rom(rom_id): xbmc.executebuiltin('Dialog.Close(busydialog)') container = viewqueries.qry_get_view_item(rom_id) ui = ViewRomGUI('script-akl-romdetails.xml', globals.addon_path, 'default', '1080i', True, - container_id=19801,container_data=container) + container_id=19801, container_data=container) ui.doModal() del ui @@ -249,24 +299,25 @@ def vw_view_rom(rom_id): def vw_view_rom_metadata(rom_id): container = viewqueries.qry_get_view_metadata(rom_id) _render_list_items(container) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) @router.route('/rom//assets') def vw_view_rom_assets(rom_id): container = viewqueries.qry_get_view_assets(rom_id) _render_list_items(container) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) @router.route('/rom//scanneddata') -def vw_view_rom_metadata(rom_id): +def vw_view_list_rom_scanneddata(rom_id): container = viewqueries.qry_get_view_scanned_data(rom_id) _render_list_items(container) - xbmcplugin.endOfDirectory(handle = router.handle, succeeded = True, cacheToDisc = False) + xbmcplugin.endOfDirectory(handle=router.handle, succeeded=True, cacheToDisc=False) + @router.route('/rom//view/scanneddata') -def vw_view_rom_metadata(rom_id): +def vw_view_rom_scanneddata(rom_id): field = router.args['field'][0] if 'field' in router.args else None if not field: kodi.notify_warn(kodi.translate(40997)) @@ -275,13 +326,14 @@ def vw_view_rom_metadata(rom_id): requested_item = next((i for i in container['items'] if i['name'] == field), None) xbmcgui.Dialog().textviewer(str(field), str(requested_item['name2'])) + # ------------------------------------------------------------------------------------------------- # UI render methods # ------------------------------------------------------------------------------------------------- # # Renders items for a view. # -def _render_list_items(container_data:dict, container_context_items = [], filter_method:ListFilter = None): +def _render_list_items(container_data: dict, container_context_items=[], filter_method: ListFilter = None): vw_misc_set_all_sorting_methods() vw_misc_set_AEL_Content(container_data['obj_type'] if 'obj_type' in container_data else constants.OBJ_NONE) vw_misc_clear_AEL_Launcher_Content() @@ -305,7 +357,8 @@ def _render_list_items(container_data:dict, container_context_items = [], filter item_context_items = viewqueries.qry_listitem_context_menu_items(list_item_data, container_data) list_item.addContextMenuItems(item_context_items + container_context_items) - xbmcplugin.addDirectoryItem(handle = router.handle, url = url_str, listitem = list_item, isFolder = folder_flag) + xbmcplugin.addDirectoryItem(handle=router.handle, url=url_str, listitem=list_item, isFolder=folder_flag) + def _render_list_item(list_item_data: dict) -> xbmcgui.ListItem: if list_item_data is None: @@ -350,7 +403,7 @@ def onClick(self, controlId: int): uri_args = None if args: args_lst = args.split("=") - uri_args = { args_lst[0]: args_lst[1]} + uri_args = {args_lst[0]: args_lst[1]} self.close() kodi.update_uri(uri, uri_args) @@ -386,11 +439,12 @@ def vw_misc_set_all_sorting_methods(): # >> This must be called only if router.handle > 0, otherwise Kodi will complain in the log. if router.handle < 0: return - xbmcplugin.addSortMethod(handle = router.handle, sortMethod = xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS) - xbmcplugin.addSortMethod(handle = router.handle, sortMethod = xbmcplugin.SORT_METHOD_VIDEO_YEAR) - xbmcplugin.addSortMethod(handle = router.handle, sortMethod = xbmcplugin.SORT_METHOD_STUDIO) - xbmcplugin.addSortMethod(handle = router.handle, sortMethod = xbmcplugin.SORT_METHOD_UNSORTED) - xbmcplugin.addSortMethod(handle = router.handle, sortMethod = xbmcplugin.SORT_METHOD_GENRE) + xbmcplugin.addSortMethod(handle=router.handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS) + xbmcplugin.addSortMethod(handle=router.handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR) + xbmcplugin.addSortMethod(handle=router.handle, sortMethod=xbmcplugin.SORT_METHOD_STUDIO) + xbmcplugin.addSortMethod(handle=router.handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(handle=router.handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE) + # # Set the AEL content type. @@ -400,39 +454,41 @@ def vw_misc_set_all_sorting_methods(): def vw_misc_set_AEL_Content(AEL_Content_Value): if AEL_Content_Value == constants.AKL_CONTENT_VALUE_LAUNCHERS: logger.debug('vw_misc_set_AEL_Content() Setting Window({0}) '.format(constants.AKL_CONTENT_WINDOW_ID) + - 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_LAUNCHERS)) + 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_LAUNCHERS)) xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_LAUNCHERS) elif AEL_Content_Value == constants.AKL_CONTENT_VALUE_CATEGORY: logger.debug('vw_misc_set_AEL_Content() Setting Window({0}) '.format(constants.AKL_CONTENT_WINDOW_ID) + - 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_CATEGORY)) + 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_CATEGORY)) xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_CATEGORY) elif AEL_Content_Value == constants.AKL_CONTENT_VALUE_ROMS: logger.debug('vw_misc_set_AEL_Content() Setting Window({0}) '.format(constants.AKL_CONTENT_WINDOW_ID) + - 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_ROMS)) + 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_ROMS)) xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_ROMS) elif AEL_Content_Value == constants.AKL_CONTENT_VALUE_NONE: logger.debug('vw_misc_set_AEL_Content() Setting Window({0}) '.format(constants.AKL_CONTENT_WINDOW_ID) + - 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_NONE)) + 'property "{0}" = "{1}"'.format(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_NONE)) xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_CONTENT_LABEL, constants.AKL_CONTENT_VALUE_NONE) else: logger.error('vw_misc_set_AEL_Content() Invalid AEL_Content_Value "{0}"'.format(AEL_Content_Value)) + def vw_misc_clear_AEL_Launcher_Content(): xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_LAUNCHER_NAME_LABEL, '') xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_LAUNCHER_ICON_LABEL, '') xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_LAUNCHER_CLEARLOGO_LABEL, '') xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_LAUNCHER_PLATFORM_LABEL, '') xbmcgui.Window(constants.AKL_CONTENT_WINDOW_ID).setProperty(constants.AKL_LAUNCHER_BOXSIZE_LABEL, '') + -def vw_create_filter(filter_on_type:str, filter_on_value:str) -> ListFilter: +def vw_create_filter(filter_on_type: str, filter_on_value: str) -> ListFilter: if filter_on_type is None: return None if filter_on_value == 'UNDEFINED': filter_on_value = '' if filter_on_type == constants.META_TITLE_ID: - return OnTitleFilter(filter_on_value) + return OnTitleFilter(filter_on_value) if filter_on_type == constants.META_DEVELOPER_ID: return OnDeveloperFilter(filter_on_value) if filter_on_type == constants.META_GENRE_ID: @@ -440,7 +496,7 @@ def vw_create_filter(filter_on_type:str, filter_on_value:str) -> ListFilter: if filter_on_type == constants.META_YEAR_ID: return OnReleaseYearFilter(filter_on_value) if filter_on_type == constants.META_RATING_ID: - return OnRatingFilter(filter_on_value) + return OnRatingFilter(filter_on_value) if filter_on_type == constants.META_ESRB_ID: return OnESRBFilter(filter_on_value) if filter_on_type == constants.META_PEGI_ID: @@ -453,48 +509,74 @@ def vw_create_filter(filter_on_type:str, filter_on_value:str) -> ListFilter: logger.debug(f'Filter called without proper filter type. "{filter_on_type}"') return None + +def vw_get_object_type_by_url(url: str): + if 'category' in url or 'categories' in url: + if 'virtual' in url: + return constants.OBJ_CATEGORY_VIRTUAL + return constants.OBJ_CATEGORY + if 'collection' in url: + if 'virtual' in url: + return constants.OBJ_COLLECTION_VIRTUAL + return constants.OBJ_ROMCOLLECTION + if 'rom' in url: + return constants.OBJ_ROM + if 'source' in url: + return constants.OBJ_SOURCE + return constants.OBJ_NONE + + class ListFilter(object): __metaclass__ = abc.ABCMeta - def __init__(self, filter_value:str): + def __init__(self, filter_value: str): self.filter_value = filter_value - @abc.abstractmethod - def is_valid(self, subject:dict) -> bool: + @abc.abstractmethod + def is_valid(self, subject: dict) -> bool: return True + class OnTitleFilter(ListFilter): def is_valid(self, subject: dict) -> bool: return 'name' in subject and subject['name'].lower().find(self.filter_value) > -1 + class OnDeveloperFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'info' in subject and 'studio' in subject['info'] and subject['info']['studio'] == self.filter_value + return 'info' in subject and 'studio' in subject['info'] and subject['info']['studio'] == self.filter_value + class OnGenreFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'info' in subject and 'genre' in subject['info'] and subject['info']['genre'] == self.filter_value + return 'info' in subject and 'genre' in subject['info'] and subject['info']['genre'] == self.filter_value + class OnReleaseYearFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'info' in subject and 'year' in subject['info'] and subject['info']['year'] == self.filter_on_value + return 'info' in subject and 'year' in subject['info'] and subject['info']['year'] == self.filter_on_value + class OnRatingFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'info' in subject and 'rating' in subject['info'] and subject['info']['rating'] == self.filter_on_value + return 'info' in subject and 'rating' in subject['info'] and subject['info']['rating'] == self.filter_on_value + class OnESRBFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'properties' in subject and 'esrb' in subject['properties'] and subject['properties']['esrb'] == self.filter_on_value + return 'properties' in subject and 'esrb' in subject['properties'] and subject['properties']['esrb'] == self.filter_on_value + class OnPEGIFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'properties' in subject and 'pegi' in subject['properties'] and subject['properties']['pegi'] == self.filter_on_value + return 'properties' in subject and 'pegi' in subject['properties'] and subject['properties']['pegi'] == self.filter_on_value + class OnNumberOfPlayersFilter(ListFilter): def is_valid(self, subject: dict) -> bool: - return 'properties' in subject and 'nplayers' in subject['properties'] and subject['properties']['nplayers'] == self.filter_on_value + return 'properties' in subject and 'nplayers' in subject['properties'] and subject['properties']['nplayers'] == self.filter_on_value + class OnPlatformFilter(ListFilter): def is_valid(self, subject: dict) -> bool: return 'properties' in subject and 'platform' in subject['properties'] and subject['properties']['platform'] == self.filter_on_value diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index d5a18cad..a0916ff8 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -68,7 +68,7 @@ def is_alive(self): return alive - def stop(self, check_alive = False): + def stop(self, check_alive=False): if check_alive and not self.is_alive(): logger.info("Webservice not running, so stopping not needed.") @@ -196,7 +196,7 @@ def handle_request(self, headers_only=False): self.end_headers() elif 'query/' in api_path: - self.handle_queries(api_path) + self.handle_queries(api_path) elif 'store/' in api_path: if self.handle_posts(api_path): self.send_response(200) @@ -223,15 +223,21 @@ def handle_queries(self, api_path): if 'query/rom/' in api_path: obj = 'ROM' response_data = self.handle_rom_queries(api_path) - elif 'query/romcollection/': + elif 'query/romcollection/' in api_path: obj = 'ROMCollection' response_data = self.handle_romcollection_queries(api_path) - - if response_data is None: + elif 'query/source/' in api_path: + obj = 'Source' + response_data = self.handle_source_queries(api_path) + elif 'query/launcher/' in api_path: + obj = 'Launcher' + response_data = self.handle_launcher_queries(api_path) + + if response_data is None: self.send_response(404) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write('{} entity not found'.format(obj)) + self.wfile.write(f'{obj} entity not found'.encode(encoding='utf-8')) return self.send_response(200) @@ -254,18 +260,34 @@ def handle_romcollection_queries(self, api_path): params = self.get_params() id = params.get('id') - if 'romcollection/launcher/settings/' in api_path: - return apiqueries.qry_get_collection_launcher_settings(id, params.get('launcher_id')) - if 'romcollection/scanner/settings/' in api_path: - return apiqueries.qry_get_collection_scanner_settings(id, params.get('scanner_id')) - if 'romcollection/launchers/' in api_path: - return apiqueries.qry_get_launchers(id) if 'romcollection/roms/' in api_path: return apiqueries.qry_get_roms(id) if 'romcollection/' in api_path: return apiqueries.qry_get_rom_collection(id) return None + + def handle_source_queries(self, api_path): + params = self.get_params() + id = params.get('id') + + if 'source/scanner/settings/' in api_path: + return apiqueries.qry_get_source_scanner_settings(id) + if 'source/roms/' in api_path: + return apiqueries.qry_get_roms(id) + if 'source/launchers' in api_path: + return apiqueries.qry_get_source_launchers(id) + + return None + + def handle_launcher_queries(self, api_path): + params = self.get_params() + id = params.get('launcher_id') + + if 'query/launcher/' in api_path: + return apiqueries.qry_get_launcher_settings(id) + + return None def handle_posts(self, api_path) -> bool: params = self.get_params() @@ -287,4 +309,4 @@ def handle_posts(self, api_path) -> bool: if 'store/rom/updated' in api_path: return api_commands.cmd_store_scraped_single_rom(data) - return False \ No newline at end of file + return diff --git a/resources/migrations/1.5.0_002.sql b/resources/migrations/1.5.0_002.sql new file mode 100644 index 00000000..5599177d --- /dev/null +++ b/resources/migrations/1.5.0_002.sql @@ -0,0 +1,375 @@ +-- -------------------------------------- +-- CREATE NEW TABLES +-- -------------------------------------- +CREATE TABLE IF NOT EXISTS sources( + id TEXT PRIMARY KEY, + name TEXT, + platform TEXT, + box_size TEXT, + assets_path TEXT, + last_scan_timestamp TIMESTAMP, + akl_addon_id TEXT, + settings TEXT, + FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE IF NOT EXISTS collection_source_ruleset( + ruleset_id TEXT PRIMARY KEY, + source_id TEXT, + collection_id TEXT, + set_operator INTEGER DEFAULT NULL +); + +CREATE TABLE IF NOT EXISTS import_rule( + rule_id TEXT PRIMARY KEY, + ruleset_id TEXT, + property TEXT, + value TEXT, + operator INTEGER DEFAULT 1 NOT NULL +); + +CREATE TABLE IF NOT EXISTS source_assetpaths( + source_id TEXT, + assetpaths_id TEXT, + FOREIGN KEY (source_id) REFERENCES sources (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (assetpaths_id) REFERENCES assetpaths (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE IF NOT EXISTS launchers( + id TEXT PRIMARY KEY, + name TEXT, + akl_addon_id TEXT, + settings TEXT, + FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE IF NOT EXISTS source_launchers( + source_id TEXT, + launcher_id TEXT, + is_default INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY (source_id) REFERENCES sources (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (launcher_id) REFERENCES launcher (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); +-- -------------------------------------- +-- MIGRATE EXISTING DATA INTO NEW TABLES +-- -------------------------------------- +INSERT INTO sources (id, name, platform, box_size, assets_path, akl_addon_id, settings) + SELECT rcs.id, rc.name || ' (' || rcs.id || ')', rc.platform, rc.box_size, m.assets_path, rcs.akl_addon_id, rcs.settings + FROM romcollection_scanners as rcs + LEFT JOIN romcollections as rc ON rc.id = rcs.romcollection_id + INNER JOIN metadata as m ON m.id = rc.metadata_id + LEFT JOIN akl_addon as aa ON rcs.akl_addon_id = aa.id; + +INSERT INTO collection_source_ruleset (ruleset_id, source_id, collection_id) + SELECT rcs.id, rcs.id, rc.id + FROM romcollection_scanners as rcs + LEFT JOIN romcollections as rc ON rc.id = rcs.romcollection_id; + +INSERT INTO source_assetpaths (source_id, assetpaths_id) + SELECT ra.romcollection_id, ra.assetpaths_id + FROM romcollection_assetpaths as ra; + +INSERT INTO launchers (id, name, akl_addon_id, settings) + SELECT rl.id, a.name || ' (' || rl.id || ')', a.id, rl.settings + FROM rom_launchers AS rl + INNER JOIN akl_addon AS a ON rl.akl_addon_id = a.id; + +INSERT INTO launchers (id, name, akl_addon_id, settings) + SELECT rcl.id, a.name || ' (' || rcl.id || ')', a.id, rcl.settings + FROM romcollection_launchers AS rcl + INNER JOIN akl_addon AS a ON rcl.akl_addon_id = a.id; + +INSERT INTO source_launchers(source_id, launcher_id, is_default) + SELECT rcl.romcollection_id, rcl.id, rcl.is_default + FROM romcollection_launchers AS rcl; + +-- -------------------------------------- +-- ASSOCIATE ROMS WITH SOURCE INSTEAD OF SCANNER +-- AND ALTER LAUNCHER TABLES +-- -------------------------------------- +PRAGMA foreign_keys=off; +BEGIN TRANSACTION; + +ALTER TABLE roms RENAME TO _roms_old; +ALTER TABLE romcollection_launchers RENAME TO _romcollection_launchers_old; +ALTER TABLE rom_launchers RENAME TO _rom_launchers_old; +ALTER TABLE metadata RENAME TO _metadata_old; + +CREATE TABLE IF NOT EXISTS metadata( + id TEXT PRIMARY KEY, + year TEXT, + genre TEXT, + developer TEXT, + rating INTEGER NULL, + plot TEXT, + extra TEXT, + finished INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS roms( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + num_of_players INTEGER DEFAULT 1 NOT NULL, + num_of_players_online INTEGER DEFAULT 0 NOT NULL, + esrb_rating TEXT, + pegi_rating TEXT, + nointro_status TEXT, + pclone_status TEXT, + cloneof TEXT, + platform TEXT, + box_size TEXT, + rom_status TEXT, + is_favourite INTEGER DEFAULT 0 NOT NULL, + launch_count INTEGER DEFAULT 0 NOT NULL, + last_launch_timestamp TIMESTAMP, + metadata_id TEXT, + scanned_by_id TEXT NULL, + FOREIGN KEY (metadata_id) REFERENCES metadata (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (scanned_by_id) REFERENCES sources (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE IF NOT EXISTS romcollection_launchers( + romcollection_id TEXT, + launcher_id TEXT, + is_default INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY (romcollection_id) REFERENCES romcollections (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (launcher_id) REFERENCES launcher (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); +CREATE TABLE IF NOT EXISTS rom_launchers( + rom_id TEXT, + + launcher_id TEXT, + is_default INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY (rom_id) REFERENCES roms (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (launcher_id) REFERENCES launcher (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +INSERT INTO metadata (id, year, genre, developer, rating, plot, extra, finished) +SELECT id, year, genre, developer, rating, plot, extra, finished + FROM _metadata_old; + +INSERT INTO roms ( + id,name,num_of_players,num_of_players_online,esrb_rating,pegi_rating,nointro_status,pclone_status,cloneof, + platform,box_size,rom_status,is_favourite, launch_count, last_launch_timestamp, metadata_id, scanned_by_id +) SELECT + id,name,num_of_players,num_of_players_online,esrb_rating,pegi_rating,nointro_status,pclone_status,cloneof, + platform,box_size,rom_status,is_favourite, launch_count, last_launch_timestamp, metadata_id, scanned_by_id + FROM _roms_old; + +INSERT INTO romcollection_launchers(romcollection_id, launcher_id, is_default) + SELECT rcl.romcollection_id, rcl.id, rcl.is_default + FROM _romcollection_launchers_old AS rcl; + +INSERT INTO rom_launchers(rom_id, launcher_id, is_default) + SELECT rc.rom_id, rc.id, rc.is_default + FROM _rom_launchers_old AS rc; + +-- -------------------------------------- +-- CLEANUP OLD TABLES +-- -------------------------------------- +DROP TABLE romcollection_scanners; +DROP TABLE romcollection_assetpaths; +DROP TABLE _roms_old; +DROP TABLE _romcollection_launchers_old; +DROP TABLE _rom_launchers_old; + +COMMIT; + +PRAGMA foreign_keys=on; + +-- -------------------------------------- +-- CREATE NEW VIEWS / DROP OLD VIEWS +-- -------------------------------------- +DROP VIEW vw_romcollection_scanners; +DROP VIEW vw_romcollection_asset_paths; +DROP VIEW vw_romcollections; +DROP VIEW vw_roms; +DROP VIEW vw_rom_assets; +DROP VIEW vw_rom_asset_paths; +DROP VIEW vw_rom_tags; +DROP VIEW vw_romcollection_launchers; +DROP VIEW vw_rom_launchers; +DROP VIEW vw_categories; + +CREATE VIEW IF NOT EXISTS vw_sources AS SELECT + s.id AS id, + s.name AS name, + s.platform AS platform, + s.box_size AS box_size, + s.assets_path AS assets_path, + s.last_scan_timestamp AS last_scan_timestamp, + s.settings AS settings, + a.id AS associated_addon_id, + a.name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings, + (SELECT COUNT(*) FROM roms AS rms WHERE rms.scanned_by_id = s.id) as num_roms +FROM sources AS s + INNER JOIN akl_addon AS a ON s.akl_addon_id = a.id; + +CREATE VIEW IF NOT EXISTS vw_source_asset_paths AS SELECT + a.id as id, + s.id as source_id, + a.path, + a.asset_type +FROM assetpaths AS a + INNER JOIN source_assetpaths AS sa ON a.id = sa.assetpaths_id + INNER JOIN sources AS s ON sa.source_id = s.id; + +CREATE VIEW IF NOT EXISTS vw_categories AS SELECT + c.id AS id, + c.parent_id AS parent_id, + c.metadata_id, + c.name AS m_name, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished AS finished, + (SELECT COUNT(*) FROM categories AS sc WHERE sc.parent_id = c.id) AS num_categories, + (SELECT COUNT(*) FROM romcollections AS sr WHERE sr.parent_id = c.id) AS num_collections +FROM categories AS c + INNER JOIN metadata AS m ON c.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT + r.id AS id, + r.parent_id AS parent_id, + r.metadata_id, + r.name AS m_name, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished AS finished, + r.platform AS platform, + r.box_size AS box_size, + (SELECT COUNT(*) FROM roms AS rms INNER JOIN roms_in_romcollection AS rrs ON rms.id = rrs.rom_id AND rrs.romcollection_id = r.id) as num_roms +FROM romcollections AS r + INNER JOIN metadata AS m ON r.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_roms AS SELECT + r.id AS id, + r.metadata_id, + r.name AS m_name, + r.num_of_players AS nplayers, + r.num_of_players_online AS nplayers_online, + r.esrb_rating AS esrb, + r.pegi_rating AS pegi, + r.nointro_status AS nointro_status, + r.pclone_status AS pclone_status, + r.cloneof AS cloneof, + r.platform AS platform, + r.box_size AS box_size, + r.scanned_by_id AS scanned_by_id, + m.year AS m_year, + m.genre AS m_genre, + m.developer AS m_developer, + m.rating AS m_rating, + m.plot AS m_plot, + m.extra AS extra, + m.finished, + r.rom_status, + r.is_favourite, + r.launch_count, + r.last_launch_timestamp, + ( + SELECT group_concat(t.tag) AS rom_tags + FROM tags AS t + INNER JOIN metatags AS mt ON t.id = mt.tag_id + WHERE mt.metadata_id = r.metadata_id + GROUP BY mt.metadata_id + ) AS rom_tags +FROM roms AS r + INNER JOIN metadata AS m ON r.metadata_id = m.id; + +CREATE VIEW IF NOT EXISTS vw_rom_assets AS SELECT + a.id as id, + r.id as rom_id, + a.filepath, + a.asset_type +FROM assets AS a + INNER JOIN rom_assets AS ra ON a.id = ra.asset_id + INNER JOIN roms AS r ON ra.rom_id = r.id; + +CREATE VIEW IF NOT EXISTS vw_rom_asset_paths AS SELECT + a.id as id, + r.id as rom_id, + a.path, + a.asset_type +FROM assetpaths AS a + INNER JOIN rom_assetpaths AS ra ON a.id = ra.assetpaths_id + INNER JOIN roms AS r ON ra.rom_id = r.id; + +CREATE VIEW IF NOT EXISTS vw_rom_tags AS SELECT + t.id as id, + r.id as rom_id, + t.tag +FROM tags AS t + INNER JOIN metatags AS mt ON t.id = mt.tag_id + INNER JOIN roms AS r ON mt.metadata_id = r.metadata_id; + +CREATE VIEW IF NOT EXISTS vw_romcollection_launchers AS SELECT + l.id AS id, + l.name AS name, + rcl.romcollection_id, + a.id AS associated_addon_id, + a.name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings, + l.settings, + rcl.is_default +FROM romcollection_launchers AS rcl + INNER JOIN launchers AS l ON rcl.launcher_id = l.id + INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; + +CREATE VIEW IF NOT EXISTS vw_source_launchers AS SELECT + l.id AS id, + l.name AS name, + sl.source_id, + a.id AS associated_addon_id, + a.name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings, + l.settings, + sl.is_default +FROM source_launchers AS sl + INNER JOIN launchers AS l ON sl.source_id = l.id + INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; + +CREATE VIEW IF NOT EXISTS vw_rom_launchers AS SELECT + l.id AS id, + l.name AS name, + rl.rom_id, + a.id AS associated_addon_id, + a.name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings, + l.settings, + rl.is_default +FROM rom_launchers AS rl + INNER JOIN launchers AS l ON rl.rom_id = l.id + INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; \ No newline at end of file diff --git a/resources/schema.sql b/resources/schema.sql index eca71222..51d8393f 100644 --- a/resources/schema.sql +++ b/resources/schema.sql @@ -1,3 +1,6 @@ +------------------------------------------------- +-- MAIN ENTITIES +------------------------------------------------- CREATE TABLE IF NOT EXISTS metadata( id TEXT PRIMARY KEY, year TEXT, @@ -6,42 +9,9 @@ CREATE TABLE IF NOT EXISTS metadata( rating INTEGER NULL, plot TEXT, extra TEXT, - assets_path TEXT, finished INTEGER DEFAULT 0 ); -CREATE TABLE IF NOT EXISTS tags( - id TEXT PRIMARY KEY, - tag TEXT -); - -CREATE TABLE IF NOT EXISTS metatags( - metadata_id TEXT, - tag_id TEXT, - FOREIGN KEY (metadata_id) REFERENCES metadata (id) - ON DELETE CASCADE ON UPDATE NO ACTION, - FOREIGN KEY (tag_id) REFERENCES tags (id) - ON DELETE CASCADE ON UPDATE NO ACTION -); - -CREATE TABLE IF NOT EXISTS assets( - id TEXT PRIMARY KEY, - filepath TEXT NOT NULL, - asset_type TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS assetpaths( - id TEXT PRIMARY KEY, - path TEXT NOT NULL, - asset_type TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS assetmappings ( - id TEXT PRIMARY KEY, - mapped_asset_type TEXT NOT NULL, - to_asset_type TEXT NOT NULL -); - CREATE TABLE IF NOT EXISTS akl_addon( id TEXT PRIMARY KEY, name TEXT, @@ -51,6 +21,19 @@ CREATE TABLE IF NOT EXISTS akl_addon( extra_settings TEXT ); +CREATE TABLE IF NOT EXISTS sources( + id TEXT PRIMARY KEY, + name TEXT, + platform TEXT, + box_size TEXT, + assets_path TEXT, + last_scan_timestamp TIMESTAMP, + akl_addon_id TEXT, + settings TEXT, + FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + CREATE TABLE IF NOT EXISTS categories( id TEXT PRIMARY KEY, name TEXT NOT NULL, @@ -75,29 +58,6 @@ CREATE TABLE IF NOT EXISTS romcollections( ON DELETE CASCADE ON UPDATE NO ACTION ); -CREATE TABLE IF NOT EXISTS romcollection_launchers( - id TEXT PRIMARY KEY, - romcollection_id TEXT, - akl_addon_id TEXT, - settings TEXT, - is_default INTEGER DEFAULT 0 NOT NULL, - FOREIGN KEY (romcollection_id) REFERENCES romcollections (id) - ON DELETE CASCADE ON UPDATE NO ACTION, - FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) - ON DELETE CASCADE ON UPDATE NO ACTION -); - -CREATE TABLE IF NOT EXISTS romcollection_scanners( - id TEXT PRIMARY KEY, - romcollection_id TEXT, - akl_addon_id TEXT, - settings TEXT, - FOREIGN KEY (romcollection_id) REFERENCES romcollections (id) - ON DELETE CASCADE ON UPDATE NO ACTION, - FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) - ON DELETE CASCADE ON UPDATE NO ACTION -); - CREATE TABLE IF NOT EXISTS roms( id TEXT PRIMARY KEY, name TEXT NOT NULL, @@ -118,10 +78,45 @@ CREATE TABLE IF NOT EXISTS roms( scanned_by_id TEXT NULL, FOREIGN KEY (metadata_id) REFERENCES metadata (id) ON DELETE CASCADE ON UPDATE NO ACTION, - FOREIGN KEY (scanned_by_id) REFERENCES romcollection_scanner (id) - ON DELETE SET NULL ON UPDATE NO ACTION + FOREIGN KEY (scanned_by_id) REFERENCES sources (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE IF NOT EXISTS tags( + id TEXT PRIMARY KEY, + tag TEXT +); + +CREATE TABLE IF NOT EXISTS assets( + id TEXT PRIMARY KEY, + filepath TEXT NOT NULL, + asset_type TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS assetpaths( + id TEXT PRIMARY KEY, + path TEXT NOT NULL, + asset_type TEXT NOT NULL ); +CREATE TABLE IF NOT EXISTS assetmappings ( + id TEXT PRIMARY KEY, + mapped_asset_type TEXT NOT NULL, + to_asset_type TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS launchers( + id TEXT PRIMARY KEY, + name TEXT, + akl_addon_id TEXT, + settings TEXT, + FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +------------------------------------------------- +-- SECONDARY ENTITIES +------------------------------------------------- CREATE TABLE IF NOT EXISTS scanned_roms_data( rom_id TEXT, data_key TEXT NOT NULL, @@ -130,6 +125,33 @@ CREATE TABLE IF NOT EXISTS scanned_roms_data( ON DELETE CASCADE ON UPDATE NO ACTION ); +CREATE TABLE IF NOT EXISTS collection_source_ruleset( + ruleset_id TEXT PRIMARY KEY, + source_id TEXT, + collection_id TEXT, + set_operator INTEGER DEFAULT NULL +); + +CREATE TABLE IF NOT EXISTS import_rule( + rule_id TEXT PRIMARY KEY, + ruleset_id TEXT, + property TEXT, + value TEXT, + operator INTEGER DEFAULT 1 NOT NULL +); + +------------------------------------------------- +-- ENTITY JOIN TABLES +------------------------------------------------- +CREATE TABLE IF NOT EXISTS metatags( + metadata_id TEXT, + tag_id TEXT, + FOREIGN KEY (metadata_id) REFERENCES metadata (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (tag_id) REFERENCES tags (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + CREATE TABLE IF NOT EXISTS roms_in_romcollection( rom_id TEXT, romcollection_id TEXT, @@ -148,17 +170,36 @@ CREATE TABLE IF NOT EXISTS roms_in_category( ON DELETE CASCADE ON UPDATE NO ACTION ); +CREATE TABLE IF NOT EXISTS romcollection_launchers( + romcollection_id TEXT, + launcher_id TEXT, + is_default INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY (romcollection_id) REFERENCES romcollections (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (launcher_id) REFERENCES launcher (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE IF NOT EXISTS source_launchers( + source_id TEXT, + launcher_id TEXT, + is_default INTEGER DEFAULT 0 NOT NULL, + FOREIGN KEY (source_id) REFERENCES sources (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (launcher_id) REFERENCES launcher (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); + CREATE TABLE IF NOT EXISTS rom_launchers( - id TEXT PRIMARY KEY, rom_id TEXT, - akl_addon_id TEXT, - settings TEXT, + launcher_id TEXT, is_default INTEGER DEFAULT 0 NOT NULL, FOREIGN KEY (rom_id) REFERENCES roms (id) ON DELETE CASCADE ON UPDATE NO ACTION, - FOREIGN KEY (akl_addon_id) REFERENCES akl_addon (id) + FOREIGN KEY (launcher_id) REFERENCES launcher (id) ON DELETE CASCADE ON UPDATE NO ACTION ); + ------------------------------------------------- -- ASSETS JOIN TABLES ------------------------------------------------- @@ -198,15 +239,6 @@ CREATE TABLE IF NOT EXISTS romcollection_assets( ON DELETE CASCADE ON UPDATE NO ACTION ); -CREATE TABLE IF NOT EXISTS romcollection_assetpaths( - romcollection_id TEXT, - assetpaths_id TEXT, - FOREIGN KEY (romcollection_id) REFERENCES romcollections (id) - ON DELETE CASCADE ON UPDATE NO ACTION, - FOREIGN KEY (assetpaths_id) REFERENCES assetpaths (id) - ON DELETE CASCADE ON UPDATE NO ACTION -); - CREATE TABLE IF NOT EXISTS rom_assets( rom_id TEXT, asset_id TEXT, @@ -224,6 +256,15 @@ CREATE TABLE IF NOT EXISTS rom_assetpaths( FOREIGN KEY (assetpaths_id) REFERENCES assetpaths (id) ON DELETE CASCADE ON UPDATE NO ACTION ); + +CREATE TABLE IF NOT EXISTS source_assetpaths( + source_id TEXT, + assetpaths_id TEXT, + FOREIGN KEY (source_id) REFERENCES sources (id) + ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (assetpaths_id) REFERENCES assetpaths (id) + ON DELETE CASCADE ON UPDATE NO ACTION +); ------------------------------------------------- -- VIEWS ------------------------------------------------- @@ -239,12 +280,29 @@ CREATE VIEW IF NOT EXISTS vw_categories AS SELECT m.plot AS m_plot, m.extra AS extra, m.finished AS finished, - m.assets_path AS assets_path, (SELECT COUNT(*) FROM categories AS sc WHERE sc.parent_id = c.id) AS num_categories, (SELECT COUNT(*) FROM romcollections AS sr WHERE sr.parent_id = c.id) AS num_collections FROM categories AS c INNER JOIN metadata AS m ON c.metadata_id = m.id; - + +CREATE VIEW IF NOT EXISTS vw_sources AS SELECT + s.id AS id, + s.name AS name, + s.platform AS platform, + s.box_size AS box_size, + s.assets_path AS assets_path, + s.last_scan_timestamp AS last_scan_timestamp, + s.settings AS settings, + a.id AS associated_addon_id, + a.name, + a.addon_id, + a.version, + a.addon_type, + a.extra_settings, + (SELECT COUNT(*) FROM roms AS rms WHERE rms.scanned_by_id = s.id) as num_roms +FROM sources AS s + INNER JOIN akl_addon AS a ON s.akl_addon_id = a.id; + CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT r.id AS id, r.parent_id AS parent_id, @@ -257,7 +315,6 @@ CREATE VIEW IF NOT EXISTS vw_romcollections AS SELECT m.plot AS m_plot, m.extra AS extra, m.finished AS finished, - m.assets_path AS assets_path, r.platform AS platform, r.box_size AS box_size, (SELECT COUNT(*) FROM roms AS rms INNER JOIN roms_in_romcollection AS rrs ON rms.id = rrs.rom_id AND rrs.romcollection_id = r.id) as num_roms @@ -268,10 +325,10 @@ CREATE VIEW IF NOT EXISTS vw_roms AS SELECT r.id AS id, r.metadata_id, r.name AS m_name, - r.num_of_players AS m_nplayers, - r.num_of_players_online AS m_nplayers_online, - r.esrb_rating AS m_esrb, - r.pegi_rating AS m_pegi, + r.num_of_players AS nplayers, + r.num_of_players_online AS nplayers_online, + r.esrb_rating AS esrb, + r.pegi_rating AS pegi, r.nointro_status AS nointro_status, r.pclone_status AS pclone_status, r.cloneof AS cloneof, @@ -289,7 +346,6 @@ CREATE VIEW IF NOT EXISTS vw_roms AS SELECT r.is_favourite, r.launch_count, r.last_launch_timestamp, - m.assets_path AS assets_path, ( SELECT group_concat(t.tag) AS rom_tags FROM tags AS t @@ -329,15 +385,14 @@ FROM assets AS a INNER JOIN rom_assets AS ra ON a.id = ra.asset_id INNER JOIN roms AS r ON ra.rom_id = r.id; -CREATE VIEW IF NOT EXISTS vw_romcollection_asset_paths AS SELECT +CREATE VIEW IF NOT EXISTS vw_source_asset_paths AS SELECT a.id as id, - r.id as romcollection_id, - r.parent_id, + s.id as source_id, a.path, a.asset_type FROM assetpaths AS a - INNER JOIN romcollection_assetpaths AS ra ON a.id = ra.assetpaths_id - INNER JOIN romcollections AS r ON ra.romcollection_id = r.id; + INNER JOIN source_assetpaths AS sa ON a.id = sa.assetpaths_id + INNER JOIN sources AS s ON sa.source_id = s.id; CREATE VIEW IF NOT EXISTS vw_rom_asset_paths AS SELECT a.id as id, @@ -358,7 +413,8 @@ FROM tags AS t CREATE VIEW IF NOT EXISTS vw_romcollection_launchers AS SELECT l.id AS id, - l.romcollection_id, + l.name AS name, + rcl.romcollection_id, a.id AS associated_addon_id, a.name, a.addon_id, @@ -366,34 +422,41 @@ CREATE VIEW IF NOT EXISTS vw_romcollection_launchers AS SELECT a.addon_type, a.extra_settings, l.settings, - l.is_default -FROM romcollection_launchers AS l + rcl.is_default +FROM romcollection_launchers AS rcl + INNER JOIN launchers AS l ON rcl.launcher_id = l.id INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; -CREATE VIEW IF NOT EXISTS vw_romcollection_scanners AS SELECT - s.id AS id, - s.romcollection_id, +CREATE VIEW IF NOT EXISTS vw_source_launchers AS SELECT + l.id AS id, + l.name AS name, + sl.source_id, a.id AS associated_addon_id, a.name, a.addon_id, a.version, a.addon_type, a.extra_settings, - s.settings -FROM romcollection_scanners AS s - INNER JOIN akl_addon AS a ON s.akl_addon_id = a.id; + l.settings, + sl.is_default +FROM source_launchers AS sl + INNER JOIN launchers AS l ON sl.source_id = l.id + INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; CREATE VIEW IF NOT EXISTS vw_rom_launchers AS SELECT l.id AS id, - l.rom_id, + l.name AS name, + rl.rom_id, a.id AS associated_addon_id, a.name, a.addon_id, a.version, a.addon_type, + a.extra_settings, l.settings, - l.is_default -FROM rom_launchers AS l + rl.is_default +FROM rom_launchers AS rl + INNER JOIN launchers AS l ON rl.rom_id = l.id INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; CREATE TABLE IF NOT EXISTS akl_version( diff --git a/tests/misc_commands_test.py b/tests/misc_commands_test.py index bd8e9c19..d61bb3ea 100644 --- a/tests/misc_commands_test.py +++ b/tests/misc_commands_test.py @@ -40,8 +40,9 @@ def setUpClass(cls): globals.g_PATHS = globals.AKL_Paths('plugin.tests') #globals.g_PATHS.DATABASE_FILE_PATH = dbPath + @unittest.skip("todo") @patch('resources.lib.commands.misc_commands.UnitOfWork', autospec=True) - @patch('resources.lib.commands.misc_commands.AelAddonRepository.find_all_launchers', autospec=True) + @patch('resources.lib.commands.misc_commands.AelAddonRepository.find_all_launcher_addons', autospec=True) @patch('resources.lib.commands.misc_commands.ROMCollectionRepository.insert_romcollection', autospec=True) @patch('resources.lib.commands.misc_commands.AppMediator', autospec=True) @patch('resources.lib.commands.misc_commands.kodi.browse') diff --git a/tests/view_rendering_commands_test.py b/tests/view_rendering_commands_test.py index a1a90733..4ace9cf9 100644 --- a/tests/view_rendering_commands_test.py +++ b/tests/view_rendering_commands_test.py @@ -46,6 +46,10 @@ def setUpClass(cls): dbPath = io.FileName(os.path.join(cls.TEST_ASSETS_DIR, 'test_db.db')) schemaPath = io.FileName(os.path.join(cls.ROOT_DIR, 'resources/schema.sql')) + + logger.info('DBPATH: {}'.format(dbPath)) + logger.info('SCHEMAPATH: {}'.format(schemaPath)) + dbPath.unlink() UnitOfWork(dbPath).create_empty_database(schemaPath) @@ -59,15 +63,21 @@ def write_json(view_data): def store_root_view(obj, view_data): Test_View_Rendering_Commands.write_json(view_data) + + def store_src_view(obj, view_data): + Test_View_Rendering_Commands.write_json(view_data) def store_view(obj, id, type, view_data): Test_View_Rendering_Commands.write_json(view_data) @patch('resources.lib.repositories.ViewRepository.store_root_view', autospec=True, side_effect = store_root_view) + @patch('resources.lib.repositories.ViewRepository.store_sources_view', autospec=True, side_effect = store_src_view) @patch('resources.lib.repositories.ViewRepository.store_view', autospec=True, side_effect = store_view) + @patch('resources.lib.repositories.ViewRepository.cleanup_obsolete_views', autospec=True) @patch('akl.utils.kodi.notify', autospec=True) @patch('akl.utils.kodi.refresh_container', autospec=True) - def test_rendering_views_based_on_database_data(self, refresh_mock, notify_mock, store_mock, store_root_mock): + def test_rendering_views_based_on_database_data(self, refresh_mock, notify_mock, cleanup_mock, + store_mock, store_src_mock, store_root_mock): # arrange Test_View_Rendering_Commands.CREATED_VIEWS = [] From a37bad73a4d2a2e5e5e2fcabc217bd417bac689d Mon Sep 17 00:00:00 2001 From: chrisism Date: Thu, 7 Mar 2024 22:05:30 +0100 Subject: [PATCH 12/71] Fix missing var --- resources/lib/globals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/globals.py b/resources/lib/globals.py index 01b6e917..897ec1c0 100644 --- a/resources/lib/globals.py +++ b/resources/lib/globals.py @@ -71,7 +71,7 @@ def __init__(self, addon_id): # --- Artwork and NFO for Categories and Launchers --- self.DEFAULT_CAT_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-categories') self.DEFAULT_COL_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-collections') - self.DEFAULT_LAUN_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-launchers') + self.DEFAULT_ROM_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-roms') self.DEFAULT_FAV_ASSET_DIR = self.ADDON_DATA_DIR.pjoin('asset-favourites') # --- Rendered views (normal and virtuals/generated) --- @@ -112,6 +112,7 @@ def build(self): return self + router: routing.Plugin = routing.Plugin() g_PATHS: AKL_Paths From f9d76921cd69771f36d8c7f814ac26cbf299d340 Mon Sep 17 00:00:00 2001 From: chrisism Date: Thu, 7 Mar 2024 22:18:11 +0100 Subject: [PATCH 13/71] Updated texts --- .../resource.language.en_gb/strings.po | 1698 ++++++++++++++++- 1 file changed, 1600 insertions(+), 98 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 5a6ff34e..6dbab558 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -215,6 +215,52 @@ msgctxt "#40614" msgid "AKL webserver port to use (restart required)" msgstr "settings.xml" +############################ +# Scraping settings +############################ +msgctxt "#20000" +msgid "Skip" +msgstr "settings.xml" + +msgctxt "#20010" +msgid "None" +msgstr "settings.xml" + +msgctxt "#20030" +msgid "Local files" +msgstr "settings.xml" + +msgctxt "#20050" +msgid "Local files + Scrapers" +msgstr "settings.xml" + +msgctxt "#20060" +msgid "Scrapers" +msgstr "settings.xml" + +############################ +# Scraping mode +############################ + +msgctxt "#20510" +msgid "Manual" +msgstr "settings.xml" + +msgctxt "#20520" +msgid "Automatic" +msgstr "settings.xml" + +############################ +# ROM Audit options +############################ +msgctxt "#20530" +msgid "Parents" +msgstr "settings.xml" + +msgctxt "#20531" +msgid "Clones" +msgstr "settings.xml" + ############################ # Setting options - Addons ############################ @@ -285,6 +331,217 @@ msgctxt "#40813" msgid "Identifier" msgstr "" +msgctxt "#40814" +msgid "Tag" +msgstr "" + +msgctxt "#40815" +msgid "Name" +msgstr "" + +msgctxt "#40816" +msgid "Default box size" +msgstr "" + +msgctxt "#40817" +msgid "Is Favourite" +msgstr "" + +msgctxt "#40818" +msgid "Is Favourite" +msgstr "" + +msgctxt "#40819" +msgid "Times launched" +msgstr "" + +############################ +# Constant Enum values +############################ + +msgctxt "#43001" +msgid "Icon" +msgstr "Assets" + +msgctxt "#43002" +msgid "Fanart" +msgstr "Assets" + +msgctxt "#43003" +msgid "Banner" +msgstr "Assets" + +msgctxt "#43004" +msgid "Poster" +msgstr "Assets" + +msgctxt "#43005" +msgid "Clearlogo" +msgstr "Assets" + +msgctxt "#43006" +msgid "Controller" +msgstr "Assets" + +msgctxt "#43007" +msgid "Trailer" +msgstr "Assets" + +msgctxt "#43008" +msgid "Title" +msgstr "Assets" + +msgctxt "#43009" +msgid "Snap" +msgstr "Assets" + +msgctxt "#43010" +msgid "Boxfront" +msgstr "Assets" + +msgctxt "#43011" +msgid "Boxback" +msgstr "Assets" + +msgctxt "#43012" +msgid "Cartridge" +msgstr "Assets" + +msgctxt "#43013" +msgid "Flyer" +msgstr "Assets" + +msgctxt "#43014" +msgid "Map" +msgstr "Assets" + +msgctxt "#43015" +msgid "Manual" +msgstr "Assets" + +msgctxt "#43016" +msgid "3D Box" +msgstr "Assets" + + +############################ +# Object names +############################ + +msgctxt "#42501" +msgid "Category" +msgstr "domain" + +msgctxt "#42502" +msgid "Virtual Category" +msgstr "domain" + +msgctxt "#42503" +msgid "ROM Collection" +msgstr "domain" + +msgctxt "#42504" +msgid "Virtual Collection" +msgstr "domain" + +msgctxt "#42505" +msgid "ROM" +msgstr "domain" + +msgctxt "#42506" +msgid "Source" +msgstr "domain" + +msgctxt "#42507" +msgid "No Type" +msgstr "domain" + +msgctxt "#42508" +msgid "All" +msgstr "domain" + +msgctxt "#42509" +msgid "Ruleset" +msgstr "domain" + +msgctxt "#42510" +msgid "Rules" +msgstr "domain" + +msgctxt "#42511" +msgid "Rule" +msgstr "domain" + +msgctxt "#42512" +msgid "Categories" +msgstr "domain" + +msgctxt "#42513" +msgid "Collections" +msgstr "domain" + +############################ +# Advanced Enum values +############################ + +msgctxt "#30911" +msgid "ERROR" +msgstr "LOG ENUM" + +msgctxt "#30912" +msgid "WARNING" +msgstr "LOG ENUM" + +msgctxt "#30913" +msgid "INFO" +msgstr "LOG ENUM" + +msgctxt "#30914" +msgid "VERBOSE" +msgstr "LOG ENUM" + +msgctxt "#30915" +msgid "DEBUG" +msgstr "LOG ENUM" + +############################ +# Domain Enum values +############################ + +msgctxt "#30916" +msgid "all of the rules" +msgstr "RuleSetOperator" + +msgctxt "#30917" +msgid "one or more of the rules" +msgstr "RuleSetOperator" + +msgctxt "#30918" +msgid "Equals" +msgstr "RuleOperator" + +msgctxt "#30919" +msgid "Not Equals" +msgstr "RuleOperator" + +msgctxt "#30920" +msgid "Contains" +msgstr "RuleOperator" + +msgctxt "#30921" +msgid "Does not contain" +msgstr "RuleOperator" + +msgctxt "#30922" +msgid "More than" +msgstr "RuleOperator" + +msgctxt "#30923" +msgid "Less than" +msgstr "RuleOperator" + +################################################################################################################ + ############################ # Actions ############################ @@ -346,7 +603,203 @@ msgid "Edit Platform: '{}'" msgstr "" msgctxt "#40865" -msgid "Edit Release Year" +msgid "Edit Release Year: '{}'" +msgstr "" + +msgctxt "#40866" +msgid "Edit Tags" +msgstr "" + +msgctxt "#40867" +msgid "Edit Genre: '{}'" +msgstr "" + +msgctxt "#40868" +msgid "Edit Developer: '{}'" +msgstr "" + +msgctxt "#40869" +msgid "Edit Rating: '{}'" +msgstr "" + +msgctxt "#40870" +msgid "Edit Plot: '{}'" +msgstr "" + +msgctxt "#40871" +msgid "Edit NPlayers: '{}'" +msgstr "" + +msgctxt "#40872" +msgid "Edit NPlayers online: '{}'" +msgstr "" + +msgctxt "#40873" +msgid "Edit PEGI Rating: '{}'" +msgstr "" + +msgctxt "#40874" +msgid "Edit ESRB Rating: '{}'" +msgstr "" + +msgctxt "#40875" +msgid "Edit Box Size: '{}'" +msgstr "" + +msgctxt "#40876" +msgid "Import NFO file (default, {})" +msgstr "" + +msgctxt "#40877" +msgid "Import NFO file (browse NFO file) ..." +msgstr "" + +msgctxt "#40878" +msgid "Save NFO file (default location)" +msgstr "" + +msgctxt "#40879" +msgid "Save NFO file (default location)" +msgstr "" + +msgctxt "#40879" +msgid "Load Plot from TXT file ..." +msgstr "" + +msgctxt "#40880" +msgid "Load Plot from TXT file ..." +msgstr "" + +msgctxt "#40881" +msgid "Scrape" +msgstr "" + +msgctxt "#40882" +msgid "View ROM" +msgstr "" + +msgctxt "#40883" +msgid "Edit ROM" +msgstr "" + +msgctxt "#40884" +msgid "Link ROM in other collections/categories" +msgstr "" + +msgctxt "#40885" +msgid "Add ROM to AKL Favourites" +msgstr "" + +msgctxt "#40886" +msgid "View Category" +msgstr "" + +msgctxt "#40887" +msgid "Edit Category" +msgstr "" + +msgctxt "#40888" +msgid "Add new Category" +msgstr "" + +msgctxt "#40889" +msgid "Add new ROM Collection" +msgstr "" + +msgctxt "#40890" +msgid "Standalone ROM" +msgstr "" + +msgctxt "#40891" +msgid "View ROM Collection" +msgstr "" + +msgctxt "#40892" +msgid "Edit ROM Collection" +msgstr "" + +msgctxt "#40893" +msgid "Rebuild {0} view" +msgstr "" + +msgctxt "#40894" +msgid "Search ROM in collection" +msgstr "" + +msgctxt "#40895" +msgid "Open Kodi file manager" +msgstr "" + +msgctxt "#40896" +msgid "AKL addon settings" +msgstr "" + +msgctxt "#40897" +msgid "Utilities" +msgstr "" + +msgctxt "#40898" +msgid "Global Reports" +msgstr "" + +msgctxt "#40899" +msgid "Reset database" +msgstr "" + +msgctxt "#40900" +msgid "Rebuild virtual views" +msgstr "" + +msgctxt "#40901" +msgid "Scan for plugin-addons" +msgstr "" + +msgctxt "#40902" +msgid "Show plugin-addons" +msgstr "" + +msgctxt "#40903" +msgid "Manage ROM tags" +msgstr "" + +msgctxt "#40904" +msgid "Import category/launcher XML configuration file" +msgstr "" + +msgctxt "#40905" +msgid "Export category/rom collection XML configuration file" +msgstr "" + +msgctxt "#40906" +msgid "Check collections" +msgstr "" + +msgctxt "#40907" +msgid "Check ROMs artwork image integrity" +msgstr "" + +msgctxt "#40908" +msgid "Delete ROMs redundant artwork" +msgstr "" + +msgctxt "#40909" +msgid "Show detected No-Intro/Redump DATs" +msgstr "" + +msgctxt "#40910" +msgid "Global ROM statistics" +msgstr "" + +msgctxt "#40911" +msgid "Global ROM Audit statistics (All)" +msgstr "" + +msgctxt "#40912" +msgid "Global ROM Audit statistics (No-Intro only)" +msgstr "" + +msgctxt "#40913" +msgid "Global ROM Audit statistics (Redump only)" msgstr "" msgctxt "#40914" @@ -396,148 +849,837 @@ msgctxt "#40952" msgid "File is directly executable/launchable" msgstr "" -msgctxt "#40953" -msgid "Select file to execute (Skip if not available)" +msgctxt "#40953" +msgid "Select file to execute (Skip if not available)" +msgstr "" + +msgctxt "#40954" +msgid "Failure while doing database migration." +msgstr "" + +msgctxt "#40955" +msgid "Should new platform be applied to existing ROMs in this collection?" +msgstr "" + +msgctxt "#40956" +msgid "General failure" +msgstr "" + +msgctxt "#40957" +msgid "Cannot launch ROM" +msgstr "" + +msgctxt "#40958" +msgid "Failed to store launchers settings" +msgstr "" + +msgctxt "#40959" +msgid "Building initial views" +msgstr "" + +msgctxt "#40960" +msgid "Failed to execute route or command" +msgstr "" + +msgctxt "#40961" +msgid "Current view is not rendered correctly. Re-render views first." +msgstr "" + +msgctxt "#40962" +msgid "Updated addon" +msgstr "" + +msgctxt "#40963" +msgid "No AKL addons found. Search and install default plugin addons for AKL?" +msgstr "" + +msgctxt "#40964" +msgid "Views rendered" +msgstr "" + +msgctxt "#40965" +msgid "Virtual views rendered" +msgstr "" + +msgctxt "#40966" +msgid "Selected views rendered" +msgstr "" + +msgctxt "#40967" +msgid "Rendering views" +msgstr "" + +msgctxt "#40968" +msgid "Rendering all views" +msgstr "" + +msgctxt "#40969" +msgid "All views rendered" +msgstr "" + +msgctxt "#40970" +msgid "Rendering virtual category '{0}'" +msgstr "" + +msgctxt "#40971" +msgid "{0} view rendered" +msgstr "" + +msgctxt "#40972" +msgid "Cannot find virtual category id#{0}" +msgstr "" + +msgctxt "#40973" +msgid "Rendering virtual collection '{0}'" +msgstr "" + +msgctxt "#40974" +msgid "Rendering romcollection views" +msgstr "" + +msgctxt "#40975" +msgid "Rendering all views containing ROM#{0}" +msgstr "" + +msgctxt "#40976" +msgid "Failed to process ROM collection {0}" +msgstr "" + +msgctxt "#40977" +msgid "Cleared ROMs from collection" +msgstr "" + +msgctxt "#40978" +msgid "Finished importing ROMS" +msgstr "" + +msgctxt "#40979" +msgid "Preparing scraper" +msgstr "" + +msgctxt "#40980" +msgid "Preparing scanner" +msgstr "" + +msgctxt "#40981" +msgid "Creating new AKL database" +msgstr "" + +msgctxt "#40982" +msgid "Should new platform be applied to existing ROMs in this source?" +msgstr "" + +msgctxt "#40983" +msgid "{0} {1} mapped to {2}" +msgstr "" + +msgctxt "#40984" +msgid "Changed rom asset dir for {0} to {1}" +msgstr "" + +msgctxt "#40985" +msgid "Imported {0} NFO files" +msgstr "" + +msgctxt "#40986" +msgid "{0} {1} is now {2}" +msgstr "" + +msgctxt "#40987" +msgid "{0} {1} not changed" +msgstr "" + +msgctxt "#40988" +msgid "{0} Rating not changed" +msgstr "" + +msgctxt "#40989" +msgid "{0} rating is now {1}" +msgstr "" + +msgctxt "#40990" +msgid "{0} {1} has been updated" +msgstr "" + +msgctxt "#40991" +msgid "new_asset_file and dest_asset_file are the same. Returning" +msgstr "" + +msgctxt "#40992" +msgid "Failure while copying file" +msgstr "" + +msgctxt "#40993" +msgid "{0} {1} has been unset" +msgstr "" + +msgctxt "#40994" +msgid "{0} {1} has been updated" +msgstr "" + +msgctxt "#40995" +msgid "Category {0} has no items. Add romcollections or categories first." +msgstr "" + +msgctxt "#40996" +msgid "Collection {0} has no items. Add ROMs" +msgstr "" + +msgctxt "#40997" +msgid "No field specified" +msgstr "" + +msgctxt "#40998" +msgid "Scanning for AKL supported addons" +msgstr "" + +msgctxt "#40999" +msgid "Scan completed. Found {0} addons" +msgstr "" + +msgctxt "#41000" +msgid "Scrape collection '{0}' ROMs with '{1}'" +msgstr "" + +msgctxt "#41001" +msgid "No launcher configured." +msgstr "" + +msgctxt "#41002" +msgid "No launchers configured for this ROM!" +msgstr "" + +msgctxt "#41003" +msgid "No launchers associated for this collection." +msgstr "" + +msgctxt "#41004" +msgid "Removed launcher '{0}'" +msgstr "" + +msgctxt "#41005" +msgid "Configured launcher '{0}'" +msgstr "" + +msgctxt "#41006" +msgid "Configured ROM scanner '{0}'" +msgstr "" + +msgctxt "#41007" +msgid "Stored scanned ROMS in source '{0}'" +msgstr "" + +msgctxt "#41008" +msgid "Stored scraped ROMS in ROMs Collection '{0}'" +msgstr "" + +msgctxt "#41009" +msgid "Stored scraped ROM '{0}'" +msgstr "" + +msgctxt "#41010" +msgid "Removed ROMS from Source '{0}'" +msgstr "" + +msgctxt "#41011" +msgid "Cleaned up redundant files" +msgstr "" + +msgctxt "#41012" +msgid "Finished importing Categories/Launchers" +msgstr "" + +msgctxt "#41013" +msgid "Category/Launcher XML exporting cancelled" +msgstr "" + +msgctxt "#41014" +msgid "Exported AKL Categories and Collections to XML configuration" +msgstr "" + +msgctxt "#41015" +msgid "Finished resetting the database" +msgstr "" + +msgctxt "#41016" +msgid "Done running migrations on the database" +msgstr "" + +msgctxt "#41017" +msgid "ROM Collection {0} created" +msgstr "" + +msgctxt "#41018" +msgid "Deleted romcollection {0}" +msgstr "" + +msgctxt "#41019" +msgid "Imported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41020" +msgid "Exported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41021" +msgid "Changed category for collection" +msgstr "" + +msgctxt "#41022" +msgid "Export of ROMCollection XML cancelled" +msgstr "" + +msgctxt "#41023" +msgid "Exported ROMCollection '{0}' XML config" +msgstr "" + +msgctxt "#41024" +msgid "Deleted ROM {0}" +msgstr "" + +msgctxt "#41025" +msgid "Changed ROM NPlayers" +msgstr "" + +msgctxt "#41026" +msgid "Changed ROM NPlayers online" +msgstr "" + +msgctxt "#41027" +msgid "Removing tag {0}" +msgstr "" + +msgctxt "#41028" +msgid "Updating ROM with removed tags" +msgstr "" + +msgctxt "#41029" +msgid "Adding tag {0}" +msgstr "" + +msgctxt "#41030" +msgid "Updating ROM with added tags" +msgstr "" + +msgctxt "#41031" +msgid "Removed all tags from ROM {0}" +msgstr "" + +msgctxt "#41032" +msgid "Imported ROM Plot" +msgstr "" + +msgctxt "#41033" +msgid "Imported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41034" +msgid "Exported ROMCollection NFO file {0}" +msgstr "" + +msgctxt "#41035" +msgid "Created new standalone ROM {0}" +msgstr "" + +msgctxt "#41036" +msgid "Category {0} created" +msgstr "" + +msgctxt "#41037" +msgid "Deleted category {0}" +msgstr "" + +msgctxt "#41038" +msgid "Imported Category NFO file {0}" +msgstr "" + +msgctxt "#41039" +msgid "Exported Category NFO file {0}" +msgstr "" + +msgctxt "#41040" +msgid "Export of Category XML cancelled" +msgstr "" + +msgctxt "#41041" +msgid "Exported Category '{0}' XML config" +msgstr "" + +msgctxt "#41042" +msgid "Failure while writing NFO file {0}" +msgstr "" + +msgctxt "#41043" +msgid "Failure processing command '{0}'" +msgstr "" + +msgctxt "#41044" +msgid "Exception reading NFO file {0}" +msgstr "" + +msgctxt "#41045" +msgid "NFO file not found {0}" +msgstr "" + +msgctxt "#41046" +msgid "Imported {0}" +msgstr "" + +msgctxt "#41047" +msgid "No local assets path configured. Configure now?\n Else we will use addon default directories." +msgstr "" + +msgctxt "#41048" +msgid "Virtual category '{0}' has no items. Regenerate the views now?" +msgstr "" + +msgctxt "#41049" +msgid "Virtual collection '{0}' has no items. Regenerate the views now?" +msgstr "" + +msgctxt "#41050" +msgid "Do you want to overwrite collection metadata properties with values from the launcher?" +msgstr "" + +msgctxt "#41051" +msgid "Scan for ROMs now?" +msgstr "" + +msgctxt "#41052" +msgid "Overwrite file {0}?" +msgstr "" + +msgctxt "#41053" +msgid "Are you sure you want to reset the database?" +msgstr "" + +msgctxt "#41054" +msgid "AKL_configuration.xml found in the selected directory. Overwrite?" +msgstr "" + +msgctxt "#41055" +msgid "Run migration {0}?" +msgstr "" + +msgctxt "#41056" +msgid "Are you sure you want to delete '{0}'?\nThis will delete the ROM from all views." +msgstr "" + +msgctxt "#41057" +msgid "Remove tag '{0}'?" +msgstr "" + +msgctxt "#41058" +msgid "Clear all tags from ROM '{0}'?" +msgstr "" + +msgctxt "#41059" +msgid "Are you sure to unassociate launcher '{}'?" +msgstr "" + +msgctxt "#41060" +msgid "Items must match" +msgstr "" + +msgctxt "#41061" +msgid "Overwrite existing assets?" +msgstr "" + +msgctxt "#41062" +msgid "Apply new path to all current asset paths?" +msgstr "" + +msgctxt "#41063" +msgid "ROM '{}' found in AKL database. Overwrite?" +msgstr "" + +msgctxt "#41064" +msgid "Delete the ROMs completely from the AKL database and not collection only?" +msgstr "" + +msgctxt "#41065" +msgid "Move '{0}' to category '{1}'?" +msgstr "" + +msgctxt "#41066" +msgid "Are you sure you want to delete '{}'?" +msgstr "" + +msgctxt "#41067" +msgid "Category '{0}' has {1} sub-categories and {2} romcollections. Deleting it will also delete related items." +msgstr "" + +msgctxt "#41068" +msgid "Category '{0}' has no categories or romcollections." +msgstr "" + +msgctxt "#41069" +msgid "ROMCollection '{0}' has {1} ROMs." +msgstr "" + +msgctxt "#41070" +msgid "File '{0}' has {1} bytes and it is very big. Are you sure this is the correct file?" +msgstr "" + +msgctxt "#41071" +msgid "Delete {0} files marked as redundant?\nWarning! This will actually delete the files!\m Backup filesnow if needed." +msgstr "" + +msgctxt "#41072" +msgid "Category '{0}' found in AKL database. Overwrite?" +msgstr "" + +msgctxt "#41073" +msgid "ROMCollection '{0}' found in AKL database. Overwrite?" +msgstr "" + +msgctxt "#41074" +msgid "Change {0} {1}" +msgstr "" + +msgctxt "#41075" +msgid "Edit {0} {1}" +msgstr "" + +msgctxt "#41076" +msgid "Edit {0} Assets/Artwork" +msgstr "" + +msgctxt "#41077" +msgid "Edit {0} default Assets/Artwork" +msgstr "" + +msgctxt "#41078" +msgid "Edit {0} {1} mapped Assets/Artwork" +msgstr "" + +msgctxt "#41079" +msgid "Select the {0} Rating" +msgstr "" + +msgctxt "#41080" +msgid "Addons" +msgstr "" + +msgctxt "#41081" +msgid "Addon: {0}" +msgstr "" + +msgctxt "#41082" +msgid "Supported metadata: {0}" +msgstr "" + +msgctxt "#41083" +msgid "Supported assets: {0}" +msgstr "" + +msgctxt "#41084" +msgid "Add category in?" +msgstr "" + +msgctxt "#41085" +msgid "New Category Name" +msgstr "" + +msgctxt "#41086" +msgid "Edit Category '{0}' metadata" +msgstr "" + +msgctxt "#41087" +msgid "Collections to process" +msgstr "" + +msgctxt "#41088" +msgid "Select migrations to execute (Current version {0})" +msgstr "" + +msgctxt "#41089" +msgid "Migration {0}" +msgstr "" + +msgctxt "#41090" +msgid "Run migration" +msgstr "" + +msgctxt "#41091" +msgid "Mark as executed without running" +msgstr "" + +msgctxt "#41092" +msgid "Edit ROM '{0}'" +msgstr "" + +msgctxt "#41093" +msgid "Edit ROM '{0}' metadata" +msgstr "" + +msgctxt "#41094" +msgid "Edit ROM NPlayers" +msgstr "" + +msgctxt "#41095" +msgid "Edit ROM NPlayers online" +msgstr "" + +msgctxt "#41096" +msgid "Tag to add" +msgstr "" + +msgctxt "#41097" +msgid "Manage tags" +msgstr "" + +msgctxt "#41098" +msgid "Add ROM in?" +msgstr "" + +msgctxt "#41099" +msgid "Select the platform" +msgstr "" + +msgctxt "#41100" +msgid "Manage Launchers for '{}'" +msgstr "" + +msgctxt "#41101" +msgid "Choose launcher to associate" +msgstr "" + +msgctxt "#41102" +msgid "Choose launcher to edit" +msgstr "" + +msgctxt "#41103" +msgid "Choose launcher to remove" +msgstr "" + +msgctxt "#41104" +msgid "Choose launcher to set as default" +msgstr "" + +msgctxt "#41105" +msgid "Choose launcher" +msgstr "" + +msgctxt "#41106" +msgid "Choose launcher addon type" +msgstr "" + +msgctxt "#41107" +msgid "Choose scanner to associate" +msgstr "" + +msgctxt "#41108" +msgid "Choose scanner to edit" +msgstr "" + +msgctxt "#41109" +msgid "Added launcher '{0}'" +msgstr "" + +msgctxt "#41110" +msgid "Scrape source '{0}' ROMs" +msgstr "" + +msgctxt "#41111" +msgid "Scrape source '{0}' ROMs with '{1}'" +msgstr "" + +msgctxt "#41112" +msgid "Scrape ROM '{0}' with '{1}'" +msgstr "" + +msgctxt "#41113" +msgid "Metadata to scrape" +msgstr "" + +msgctxt "#41114" +msgid "Assets to scrape" +msgstr "" + +msgctxt "#41115" +msgid "Metadata scan policy '{}'" +msgstr "" + +msgctxt "#41116" +msgid "Asset scan policy '{}'" +msgstr "" + +msgctxt "#41117" +msgid "Game search mode '{}'" +msgstr "" + +msgctxt "#41118" +msgid "Game selection mode '{}'" +msgstr "" + +msgctxt "#41119" +msgid "Asset selection mode '{}'" +msgstr "" + +msgctxt "#41120" +msgid "Scrape ROM assets" +msgstr "" + +msgctxt "#41121" +msgid "Scrape ROM {0} assets" +msgstr "" + +msgctxt "#41122" +msgid "Scrape ROM metadata" +msgstr "" + +msgctxt "#41123" +msgid "Scrape ROM {0}" +msgstr "" + +msgctxt "#41124" +msgid "Scrape collection '{0}' ROMs" +msgstr "" + +msgctxt "#41125" +msgid "Add ROM collection in?" +msgstr "" + +msgctxt "#41126" +msgid "Select action for ROM Collection {0}" msgstr "" -msgctxt "#40954" -msgid "Failure while doing database migration." +msgctxt "#41127" +msgid "Edit Launcher '{0}' metadata" msgstr "" -msgctxt "#40955" -msgid "Should new platform be applied to existing ROMs in this collection?" +msgctxt "#41128" +msgid "Manage ROM Collection '{}' ROMs" msgstr "" -############################ -# Scraping settings -############################ -msgctxt "#20000" -msgid "Skip" -msgstr "settings.xml" +msgctxt "#41129" +msgid "ROM Asset directories" +msgstr "" -msgctxt "#41023" -msgid "Exported ROMCollection '{0}' XML config" +msgctxt "#41130" +msgid "Import rules for ROMs in ROMCollection '{}'" msgstr "" -msgctxt "#41024" -msgid "Deleted ROM {0}" +msgctxt "#41131" +msgid "Search ROMs..." msgstr "" -msgctxt "#41025" -msgid "Changed ROM NPlayers" +msgctxt "#41132" +msgid "Select a Rating..." msgstr "" -msgctxt "#41026" -msgid "Changed ROM NPlayers online" +msgctxt "#41133" +msgid "Select a Genre..." msgstr "" -msgctxt "#41027" -msgid "Removing tag {0}" +msgctxt "#41134" +msgid "Select a Release year..." msgstr "" -msgctxt "#41028" -msgid "Updating ROM with removed tags" +msgctxt "#41135" +msgid "Select a Developer..." msgstr "" -msgctxt "#41029" -msgid "Adding tag {0}" +msgctxt "#41136" +msgid "Enter the ROM Title search string..." msgstr "" -msgctxt "#41030" -msgid "Updating ROM with added tags" +msgctxt "#41137" +msgid "Edit {0} '{1}' {2}" msgstr "" -msgctxt "#41031" -msgid "Removed all tags from ROM {0}" +msgctxt "#41138" +msgid "Assets root path for entry '{0}'" msgstr "" -msgctxt "#41032" -msgid "Imported ROM Plot" +msgctxt "#41139" +msgid "Directory to store artwork not found.\nConfigure it before you can edit artwork." msgstr "" -msgctxt "#41033" -msgid "Imported ROMCollection NFO file {0}" +msgctxt "#41140" +msgid "Unknown object type {}.\nThis is a bug, please report it." msgstr "" -msgctxt "#41034" -msgid "Exported ROMCollection NFO file {0}" +msgctxt "#41141" +msgid "Select {0} {1}" msgstr "" -msgctxt "#41035" -msgid "Created new standalone ROM {0}" +msgctxt "#41142" +msgid "Collection '{0}' has {1} ROMs. Are you sure you want to clear them from this collection?" msgstr "" -msgctxt "#41036" -msgid "Category {0} created" +msgctxt "#41143" +msgid "Select NFO description file" msgstr "" -msgctxt "#41037" -msgid "Deleted category {0}" +msgctxt "#41144" +msgid "Select directory to export XML" msgstr "" -msgctxt "#41038" -msgid "Imported Category NFO file {0}" +msgctxt "#41145" +msgid "Select XML category/launcher configuration file" msgstr "" -msgctxt "#41039" -msgid "Exported Category NFO file {0}" +msgctxt "#41146" +msgid "Category '{}' status is now {}" msgstr "" -msgctxt "#41040" -msgid "Export of Category XML cancelled" +msgctxt "#41147" +msgid "Duplicated asset directories: {0}.\nAKL will refuse to add/edit ROMs if there are duplicate asset directories." msgstr "" -msgctxt "#41041" -msgid "Exported Category '{0}' XML config" +msgctxt "#41148" +msgid "No NFO file available" msgstr "" -msgctxt "#41042" -msgid "Failure while writing NFO file {0}" +msgctxt "#41149" +msgid "Assets directories not set: {0}.\nAsset scanner will be disabled for this/those." msgstr "" -msgctxt "#41043" -msgid "Failure processing command '{0}'" +msgctxt "#41150" +msgid "ROMCollection '{}' status is now {}" msgstr "" -msgctxt "#41044" -msgid "Exception reading NFO file {0}" +msgctxt "#41151" +msgid "Collection has no ROMs. Nothing to do." msgstr "" -msgctxt "#41045" -msgid "NFO file not found {0}" +msgctxt "#41152" +msgid "Checking ROM artwork integrity..." msgstr "" -msgctxt "#41046" -msgid "Imported {0}" +msgctxt "#41153" +msgid "Processing NFO files" msgstr "" -msgctxt "#41047" -msgid "No local assets path configured. Configure now?\n Else we will use addon default directories." +msgctxt "#41154" +msgid "Saving ROM JSON database ..." msgstr "" -msgctxt "#41048" -msgid "Virtual category '{0}' has no items. Regenerate the views now?" +msgctxt "#41155" +msgid "Select ROMs JSON file" msgstr "" -msgctxt "#20050" -msgid "Local files + Scrapers" -msgstr "settings.xml" +msgctxt "#41156" +msgid "Unknown" +msgstr "" -msgctxt "#20060" -msgid "Scrapers" -msgstr "settings.xml" +msgctxt "#41157" +msgid "Select description file (TXT|DAT)" +msgstr "" -############################ -# Scraping mode -############################ +msgctxt "#41158" +msgid "Undefined" +msgstr "" -msgctxt "#20510" -msgid "Manual" -msgstr "settings.xml" +msgctxt "#41159" +msgid "Select root assets path" +msgstr "" -msgctxt "#20520" -msgid "Automatic" -msgstr "settings.xml" +msgctxt "#41160" +msgid "Select {} path" +msgstr "" msgctxt "#41161" msgid "Rendering source views" @@ -747,30 +1889,390 @@ msgctxt "#42022" msgid "Manual entry" msgstr "" -msgctxt "#20531" -msgid "Clones" -msgstr "settings.xml" +msgctxt "#42023" +msgid "[Add tag]" +msgstr "" + +msgctxt "#42024" +msgid "[Clear all tags]" +msgstr "" + +msgctxt "#42025" +msgid "[Manual insert tag]" +msgstr "" + +msgctxt "#42026" +msgid "Add new launcher" +msgstr "" + +msgctxt "#42027" +msgid "Edit launcher" +msgstr "" + +msgctxt "#42028" +msgid "Remove launcher" +msgstr "" + +msgctxt "#42029" +msgid "Set default launcher: '{}'" +msgstr "" + +msgctxt "#42030" +msgid "Metadata to scrape: '{}'" +msgstr "" + +msgctxt "#42031" +msgid "Assets to scrape: '{}'" +msgstr "" + +msgctxt "#42032" +msgid "Overwrite existing metadata: '{}'" +msgstr "" + +msgctxt "#42033" +msgid "Overwrite existing assets/files: '{}'" +msgstr "" + +msgctxt "#42034" +msgid "Ignore scraped titles: '{}'" +msgstr "" + +msgctxt "#42035" +msgid "Yes" +msgstr "" + +msgctxt "#42036" +msgid "No" +msgstr "" + +msgctxt "#42037" +msgid "Set the title of the ROM collection" +msgstr "" + +msgctxt "#42038" +msgid "Select asset/artwork directory" +msgstr "" + +msgctxt "#42039" +msgid "Manage ROMs ..." +msgstr "" + +msgctxt "#42040" +msgid "Change Category: '{}'" +msgstr "" + +msgctxt "#42041" +msgid "ROM Collection status: '{}'" +msgstr "" + +msgctxt "#42042" +msgid "Export ROM Collection XML configuration ..." +msgstr "" + +msgctxt "#42043" +msgid "Delete ROM Collection" +msgstr "" + +msgctxt "#42044" +msgid "Choose ROMs default artwork ..." +msgstr "" + +msgctxt "#42045" +msgid "Manage ROMs asset directories ..." +msgstr "" + +msgctxt "#42046" +msgid "Scan for new ROMs" +msgstr "" + +msgctxt "#42047" +msgid "Remove dead/missing ROMs" +msgstr "" + +msgctxt "#42048" +msgid "Configure ROM scanners" +msgstr "" + +msgctxt "#42049" +msgid "Add new ROM scanner" +msgstr "" + +msgctxt "#42050" +msgid "Import ROMs (files/metadata)" +msgstr "" + +msgctxt "#42051" +msgid "Export ROMs metadata to NFO files" +msgstr "" + +msgctxt "#42052" +msgid "Scrape ROMs" +msgstr "" + +msgctxt "#42053" +msgid "Delete ROMs NFO files" +msgstr "" + +msgctxt "#42054" +msgid "Clear ROMs from ROMCollection" +msgstr "" + +msgctxt "#42055" +msgid "Choose asset for {0} (currently {1})" +msgstr "" + +msgctxt "#42056" +msgid "Import ROMs metadata from NFO files" +msgstr "" + +msgctxt "#42057" +msgid "Import ROMs data from JSON files" +msgstr "" + +msgctxt "#42058" +msgid "By ROM Title" +msgstr "" + +msgctxt "#42059" +msgid "By Release Year" +msgstr "" + +msgctxt "#42060" +msgid "By Genre" +msgstr "" + +msgctxt "#42061" +msgid "By Developer" +msgstr "" + +msgctxt "#42062" +msgid "By Rating" +msgstr "" + +msgctxt "#42063" +msgid "" +msgstr "" + +msgctxt "#42064" +msgid "[Recently played ROMs]" +msgstr "" + +msgctxt "#42065" +msgid "[Most played ROMs]" +msgstr "" + +msgctxt "#42066" +msgid "Browse by..." +msgstr "" + +msgctxt "#42067" +msgid "Browse by Title" +msgstr "" + +msgctxt "#42068" +msgid "Browse by Year" +msgstr "" + +msgctxt "#42069" +msgid "Browse by Genre" +msgstr "" + +msgctxt "#42070" +msgid "Browse by Developer" +msgstr "" + +msgctxt "#42071" +msgid "Browse by Number of Players" +msgstr "" + +msgctxt "#42072" +msgid "Browse by ESRB Rating" +msgstr "" + +msgctxt "#42073" +msgid "Browse by PEGI Rating" +msgstr "" + +msgctxt "#42074" +msgid "Browse by Rating" +msgstr "" + +msgctxt "#42080" +msgid "Clear ROMs from source" +msgstr "" + +msgctxt "#42081" +msgid "Edit source scanner" +msgstr "" + +msgctxt "#42082" +msgid "Import ROMs" +msgstr "" + +msgctxt "#42083" +msgid "Change root assets path" +msgstr "" + +msgctxt "#42084" +msgid "Change {} path" +msgstr "" + +msgctxt "#42085" +msgid "Delete Source" +msgstr "" + +msgctxt "#42086" +msgid "Add rule" +msgstr "" + +msgctxt "#42087" +msgid "Edit rule" +msgstr "" + +msgctxt "#42088" +msgid "Remove rule" +msgstr "" + +msgctxt "#42089" +msgid "Execute ruleset" +msgstr "" + +msgctxt "#42090" +msgid "Create new launcher..." +msgstr "" ############################ # Context menu item descriptions ############################ -msgctxt "#30911" -msgid "ERROR" -msgstr "LOG ENUM" +msgctxt "#44001" +msgid "Execute several [COLOR orange]Utilities[/COLOR]." +msgstr "viewqueries" -msgctxt "#30912" -msgid "WARNING" -msgstr "LOG ENUM" +msgctxt "#44002" +msgid "Generate and view [COLOR orange]Global Reports[/COLOR]." +msgstr "viewqueries" -msgctxt "#30913" -msgid "INFO" -msgstr "LOG ENUM" +msgctxt "#44003" +msgid "Reset the AKL database. You will loose all data." +msgstr "viewqueries" -msgctxt "#30914" -msgid "VERBOSE" -msgstr "LOG ENUM" +msgctxt "#44004" +msgid "Rebuild all the container views in the application." +msgstr "viewqueries" -msgctxt "#30915" -msgid "DEBUG" -msgstr "LOG ENUM" +msgctxt "#44005" +msgid "Browse AKL Favourite ROMs" +msgstr "domain" + +msgctxt "#44006" +msgid "Browse the ROMs you played recently" +msgstr "domain" + +msgctxt "#44007" +msgid "Browse the ROMs you play most" +msgstr "domain" + +msgctxt "#44008" +msgid "Browse ROMs filtered on '{0}'" +msgstr "domain" + +msgctxt "#44009" +msgid "Browse the ROMs by specifics" +msgstr "domain" + +msgctxt "#44010" +msgid "Browse the ROMs by title" +msgstr "domain" + +msgctxt "#44011" +msgid "Browse the ROMs by year" +msgstr "domain" + +msgctxt "#44012" +msgid "Browse the ROMs by genre" +msgstr "domain" + +msgctxt "#44013" +msgid "Browse the ROMs by developer" +msgstr "domain" + +msgctxt "#44014" +msgid "Browse the ROMs by number of players" +msgstr "domain" + +msgctxt "#44015" +msgid "Browse the ROMs by ESRB rating" +msgstr "domain" + +msgctxt "#44016" +msgid "Browse the ROMs by PEGI rating" +msgstr "domain" + +msgctxt "#44017" +msgid "Browse the ROMs by rating" +msgstr "domain" + +msgctxt "#44018" +msgid "Rebuild all the virtual categories and collections in the container" +msgstr "viewqueries" + +msgctxt "#44019" +msgid "Scan for addons that can be used by AKL (launchers, scrapers etc.)" +msgstr "viewqueries" + +msgctxt "#44020" +msgid "Shows previously scanned addons that can be used by AKL (launchers, scrapers etc.)" +msgstr "viewqueries" + +msgctxt "#44021" +msgid "Manage existing/available tags for ROMs" +msgstr "viewqueries" + +msgctxt "#44022" +msgid "Execute several [COLOR orange]Utilities[/COLOR]." +msgstr "viewqueries" + +msgctxt "#44023" +msgid "Exports all AKL categories and collections into an XML configuration file.\nYou can later reimport this XML file." +msgstr "viewqueries" + +msgctxt "#44024" +msgid "Check all collections for missing launchers or scanners, missing artwork, wrong platform names, asset path existence, etc." +msgstr "viewqueries" + +msgctxt "#44025" +msgid "Scans existing [COLOR=orange]ROMs artwork images[/COLOR] in ROM Collections and verifies that the images have correct extension and size is greater than 0. You can delete corrupted images to be rescraped later." +msgstr "viewqueries" + +msgctxt "#44026" +msgid "Scans all ROM collections and finds [COLOR orange]redundant ROMs artwork[/COLOR]. You may delete these unneeded images." +msgstr "viewqueries" + +msgctxt "#44027" +msgid "Display the auto-detected No-Intro/Redump DATs that will be used for the ROM audit. You have to configure the DAT directories in [COLOR orange]AKL addon settings[/COLOR], [COLOR=orange]ROM Audit[/COLOR] tab." +msgstr "viewqueries" + +msgctxt "#44028" +msgid "Shows a report of all ROM collections with number of ROMs." +msgstr "viewqueries" + +msgctxt "#44029" +msgid "Shows a report of all audited ROM collections, with Have, Miss and Unknown statistics." +msgstr "viewqueries" + +msgctxt "#44030" +msgid "Shows a report of all audited ROM Launchers, with Have, Miss and Unknown statistics. Only No-Intro platforms (cartridge-based) are reported." +msgstr "viewqueries" + +msgctxt "#44031" +msgid "Shows a report of all audited ROM Launchers, with Have, Miss and Unknown statistics. Only Redump platforms (optical-based) are reported." +msgstr "viewqueries" + +msgctxt "#44032" +msgid "Manage your game [COLOR orange]libraries[/COLOR] and sources." +msgstr "viewqueries" + +msgctxt "#44033" +msgid "Manage your game [COLOR orange]launchers[/COLOR]." +msgstr "viewqueries" From 585e830a984106a2aac06d1cf513569a5d442445 Mon Sep 17 00:00:00 2001 From: chrisism Date: Thu, 7 Mar 2024 23:02:15 +0100 Subject: [PATCH 14/71] Fix for add source --- resources/lib/viewqueries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/viewqueries.py b/resources/lib/viewqueries.py index a1d70f48..37ad858f 100644 --- a/resources/lib/viewqueries.py +++ b/resources/lib/viewqueries.py @@ -755,7 +755,7 @@ def qry_listitem_context_menu_items(list_item_data, container_data) -> typing.Li if is_source: if not item_id or len(item_id) == 0: - commands.append((kodi.translate(40916), _context_menu_url_for('/execute/command/add_library'))) + commands.append((kodi.translate(40916), _context_menu_url_for('/execute/command/add_source'))) if item_id and len(item_id) > 0: commands.append((kodi.translate(40915), _context_menu_url_for(f'/source/edit/{item_id}'))) From daf708f97e692606a91abcd32ee81f9195e4b56f Mon Sep 17 00:00:00 2001 From: chrisism Date: Fri, 8 Mar 2024 10:48:33 +0100 Subject: [PATCH 15/71] Source rendering fix --- resources/lib/commands/view_rendering_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/commands/view_rendering_commands.py b/resources/lib/commands/view_rendering_commands.py index 88cb6566..78fe7dcb 100644 --- a/resources/lib/commands/view_rendering_commands.py +++ b/resources/lib/commands/view_rendering_commands.py @@ -333,7 +333,7 @@ def _render_root_view(categories_repository: CategoryRepository, romcollections_ root_categories = categories_repository.find_root_categories() root_romcollections = romcollections_repository.find_root_romcollections() root_roms = roms_repository.find_root_roms() - sources = sources_repository.find_all() + sources = [*sources_repository.find_all()] root_data = { 'id': constants.VCATEGORY_ADDONROOT_ID, From 6bf5be82a4a5bdaf8ecbdb775e53eae9e9ad7472 Mon Sep 17 00:00:00 2001 From: chrisism Date: Fri, 8 Mar 2024 11:51:37 +0100 Subject: [PATCH 16/71] Cleanup old mess --- resources/lib/commands/rom_launcher_commands.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/resources/lib/commands/rom_launcher_commands.py b/resources/lib/commands/rom_launcher_commands.py index c039a86b..dd21ca60 100644 --- a/resources/lib/commands/rom_launcher_commands.py +++ b/resources/lib/commands/rom_launcher_commands.py @@ -233,18 +233,9 @@ def cmd_add_rom_launchers(args): is_default = kodi.dialog_yesno(kodi.translate(41171).format(selected_option.get_name())) rom.add_launcher(launcher, is_default) - rom_repository.update_rom(rom) + rom_repository.update_rom(rom) logger.info(f'Added launcher#{selected_option.get_id()} to ROM {rom.get_id()}') uow.commit() - - repository = AelAddonRepository(uow) - addons = repository.find_all_launcher_addons() - - for addon in addons: - options[addon] = addon.get_name() - - s = kodi.translate(41106) - selected_option: AelAddon = kodi.OrdDictionaryDialog().select(s, options) kodi.notify(kodi.translate(41109).format(selected_option.get_name())) AppMediator.sync_cmd('EDIT_ROM_LAUNCHERS', args) From 462eec8a74f35696c6526ad1717f9f397032843f Mon Sep 17 00:00:00 2001 From: chrisism Date: Fri, 8 Mar 2024 12:07:32 +0100 Subject: [PATCH 17/71] Fix with associated launchers --- resources/migrations/1.5.0_002.sql | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/resources/migrations/1.5.0_002.sql b/resources/migrations/1.5.0_002.sql index 5599177d..d4882a8e 100644 --- a/resources/migrations/1.5.0_002.sql +++ b/resources/migrations/1.5.0_002.sql @@ -72,8 +72,9 @@ INSERT INTO collection_source_ruleset (ruleset_id, source_id, collection_id) LEFT JOIN romcollections as rc ON rc.id = rcs.romcollection_id; INSERT INTO source_assetpaths (source_id, assetpaths_id) - SELECT ra.romcollection_id, ra.assetpaths_id - FROM romcollection_assetpaths as ra; + SELECT rcs.id, ra.assetpaths_id + FROM romcollection_scanners as rcs + INNER JOIN romcollection_assetpaths as ra ON ra.romcollection_id = rcs.romcollection_id; INSERT INTO launchers (id, name, akl_addon_id, settings) SELECT rl.id, a.name || ' (' || rl.id || ')', a.id, rl.settings @@ -86,8 +87,9 @@ INSERT INTO launchers (id, name, akl_addon_id, settings) INNER JOIN akl_addon AS a ON rcl.akl_addon_id = a.id; INSERT INTO source_launchers(source_id, launcher_id, is_default) - SELECT rcl.romcollection_id, rcl.id, rcl.is_default - FROM romcollection_launchers AS rcl; + SELECT rcs.id, rcl.id, rcl.is_default + FROM romcollection_launchers AS rcl + INNER JOIN romcollection_scanners AS rcs ON rcs.romcollection_id = rcl.romcollection_id; -- -------------------------------------- -- ASSOCIATE ROMS WITH SOURCE INSTEAD OF SCANNER @@ -355,7 +357,7 @@ CREATE VIEW IF NOT EXISTS vw_source_launchers AS SELECT l.settings, sl.is_default FROM source_launchers AS sl - INNER JOIN launchers AS l ON sl.source_id = l.id + INNER JOIN launchers AS l ON sl.launcher_id = l.id INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; CREATE VIEW IF NOT EXISTS vw_rom_launchers AS SELECT @@ -371,5 +373,5 @@ CREATE VIEW IF NOT EXISTS vw_rom_launchers AS SELECT l.settings, rl.is_default FROM rom_launchers AS rl - INNER JOIN launchers AS l ON rl.rom_id = l.id + INNER JOIN launchers AS l ON rl.launcher_id = l.id INNER JOIN akl_addon AS a ON l.akl_addon_id = a.id; \ No newline at end of file From ee42469c3019f3f304783dda62a7dc6e2460df00 Mon Sep 17 00:00:00 2001 From: chrisism Date: Sat, 9 Mar 2024 09:38:34 +0100 Subject: [PATCH 18/71] Added service tests --- resources/lib/services.py | 5 ++++- tests/services_test.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/lib/services.py b/resources/lib/services.py index f06ca4a3..d3b85d92 100644 --- a/resources/lib/services.py +++ b/resources/lib/services.py @@ -109,9 +109,10 @@ def _initial_setup(self, uow:UnitOfWork): self._perform_scans() - def _do_version_upgrade(self, uow:UnitOfWork, db_version:LooseVersion): + def _do_version_upgrade(self, uow: UnitOfWork, db_version: LooseVersion): migrations_files_to_execute = uow.get_migration_files(db_version) if len(migrations_files_to_execute) == 0: + logger.debug('No migrations to execute') return migrations_executed = uow.get_migrations_history() @@ -120,7 +121,9 @@ def _do_version_upgrade(self, uow:UnitOfWork, db_version:LooseVersion): logger.info(f"Found {len(new_migration_files_to_execute)} migration files to process.") if len(new_migration_files_to_execute) == 0: + logger.debug('No new migrations to execute') return + version_to_store = LooseVersion(globals.addon_version) file_version = uow.get_version_from_migration_file(new_migration_files_to_execute[-1]) if file_version > version_to_store: diff --git a/tests/services_test.py b/tests/services_test.py index a84f47cf..c58a5445 100644 --- a/tests/services_test.py +++ b/tests/services_test.py @@ -56,6 +56,12 @@ def test_version_compare(self, file_mock:MagicMock, globals_mock): FakeFile('/migrations/with/1.2.7.sql') ] + a = LooseVersion('1.5.0~rc4') + b = LooseVersion('1.5.0~rc7') + + c = a < b + self.assertTrue(c) + target = UnitOfWork(FakeFile("/x.db")) start_version = LooseVersion('1.1.1') globals.addon_version = '1.0.0' From 32fb797d7a4f616d036108b75df8120c3fabb943 Mon Sep 17 00:00:00 2001 From: chrisism Date: Sat, 9 Mar 2024 10:32:12 +0100 Subject: [PATCH 19/71] Fix for Retroplayer configure cmd --- resources/lib/domain.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/resources/lib/domain.py b/resources/lib/domain.py index 9fa0b74f..89fd39ab 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -483,28 +483,18 @@ def launch(self, rom: ROM): kodi.play_item(rom.get_name(), rom_file_path.getPath(), 'game', game_info) logger.debug('Retroyplayer call finished') - def configure(self, romcollection: ROMCollection): + def configure(self, entity: EntityABC): post_data = { - 'romcollection_id': romcollection.get_id(), 'akl_addon_id': self.get_id(), 'addon_id': self.addon.get_addon_id(), + 'entity_id': entity.get_id(), + 'entity_type': entity.get_type(), 'settings': {} - } - is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, settings.getSettingAsInt('webserver_port'), post_data) - if not is_stored: - kodi.notify_error(kodi.translate(40958)) - - def configure_for_rom(self, rom: ROM): - post_data = { - 'rom_id': rom.get_id(), - 'akl_addon_id': self.get_id(), - 'addon_id': self.addon.get_addon_id(), - 'settings': {} - } + } is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, settings.getSettingAsInt('webserver_port'), post_data) if not is_stored: kodi.notify_error(kodi.translate(40958)) - + class Source(ROMAddon): From a34196b370abf4718105bf46ffc461fb2056f36f Mon Sep 17 00:00:00 2001 From: chrisism Date: Sat, 9 Mar 2024 10:34:47 +0100 Subject: [PATCH 20/71] proper fix --- resources/lib/domain.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/lib/domain.py b/resources/lib/domain.py index 89fd39ab..03a55a89 100644 --- a/resources/lib/domain.py +++ b/resources/lib/domain.py @@ -483,15 +483,17 @@ def launch(self, rom: ROM): kodi.play_item(rom.get_name(), rom_file_path.getPath(), 'game', game_info) logger.debug('Retroyplayer call finished') - def configure(self, entity: EntityABC): + def configure(self, args: dict): post_data = { 'akl_addon_id': self.get_id(), 'addon_id': self.addon.get_addon_id(), - 'entity_id': entity.get_id(), - 'entity_type': entity.get_type(), + 'entity_type': args['entity_type'] if 'entity_type' in args else '', + 'entity_id': args['entity_id'] if 'entity_id' in args else '', 'settings': {} } - is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, settings.getSettingAsInt('webserver_port'), post_data) + is_stored = api.client_post_launcher_settings(globals.WEBSERVER_HOST, + settings.getSettingAsInt('webserver_port'), + post_data) if not is_stored: kodi.notify_error(kodi.translate(40958)) From 752020286ca793e3e9bcc48706edfe879bfd5cd1 Mon Sep 17 00:00:00 2001 From: chrisism Date: Sun, 10 Mar 2024 10:54:28 +0100 Subject: [PATCH 21/71] Added name for launcher in api data --- resources/lib/apiqueries.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/lib/apiqueries.py b/resources/lib/apiqueries.py index 159c7d91..ffeefa84 100644 --- a/resources/lib/apiqueries.py +++ b/resources/lib/apiqueries.py @@ -72,7 +72,7 @@ def qry_get_roms(source_id: str) -> str: rom_dto = rom.create_dto() data.append(rom_dto.get_data_dic()) - return json.dumps(data) + return json.dumps(data) def qry_get_launcher_settings(launcher_id: str) -> str: @@ -82,7 +82,9 @@ def qry_get_launcher_settings(launcher_id: str) -> str: launcher = repository.find(launcher_id) if launcher is not None: - return launcher.get_settings_str() + settings = launcher.get_settings() + settings['name'] = launcher.get_name() + return json.dumps(settings) return None @@ -97,7 +99,9 @@ def qry_get_collection_launcher_settings(collection_id: str, launcher_id: str) - return None launcher = rom_collection.get_launcher(launcher_id) - return launcher.get_settings_str() + settings = launcher.get_settings() + settings['name'] = launcher.get_name() + return json.dumps(settings) def qry_get_source_scanner_settings(source_id: str) -> str: @@ -125,5 +129,6 @@ def qry_get_source_launchers(source_id: str) -> str: launchers = source.get_launchers() for launcher in launchers: launchers_data[launcher.get_id()] = launcher.get_settings() + launchers_data[launcher.get_id()]['name'] = launcher.get_name() return json.dumps(launchers_data) From 41f6a1f70edcd9c7f08d03c9d56f7750c853a5ee Mon Sep 17 00:00:00 2001 From: chrisism Date: Mon, 11 Mar 2024 12:51:03 +0100 Subject: [PATCH 22/71] Added proper rebuild views --- resources/lib/commands/api_commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/commands/api_commands.py b/resources/lib/commands/api_commands.py index a27e8f8a..16edbd1a 100644 --- a/resources/lib/commands/api_commands.py +++ b/resources/lib/commands/api_commands.py @@ -194,6 +194,8 @@ def cmd_remove_roms(args) -> bool: kodi.notify(kodi.translate(41010).format(source.get_name())) AppMediator.async_cmd('RENDER_SOURCE_VIEW', {'source_id': source_id}) + for collection in romcollections: + AppMediator.async_cmd('RENDER_ROMCOLLECTION_VIEW', {'romcollection_id': collection.get_id()}) AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') AppMediator.async_cmd('EDIT_SOURCE', {'source_id': source_id}) return True @@ -333,7 +335,7 @@ def cmd_store_scraped_single_rom(args) -> bool: scraped_meta = applied_settings.scrape_metadata_policy != constants.SCRAPE_ACTION_NONE scraped_assets = applied_settings.scrape_assets_policy != constants.SCRAPE_ACTION_NONE - if metadata_is_updated: + if metadata_is_updated: AppMediator.async_cmd('RENDER_VCATEGORY_VIEWS') if scraped_meta and not scraped_assets: From b1266821dff4c062a4ec433c418f774e90c9dcf1 Mon Sep 17 00:00:00 2001 From: chrisism Date: Mon, 11 Mar 2024 16:32:46 +0100 Subject: [PATCH 23/71] Updated some icons --- SKINNING.md | 2 ++ media/theme/Launchers_icon.png | Bin 0 -> 36815 bytes media/theme/Launchers_poster.png | Bin 0 -> 107324 bytes media/theme/Libraries_icon.png | Bin 33674 -> 0 bytes media/theme/Libraries_poster.png | Bin 74038 -> 0 bytes media/theme/Showcase_Browse_by_icons.jpg | Bin 83626 -> 0 bytes media/theme/Showcase_Browse_by_posters.jpg | Bin 58597 -> 0 bytes media/theme/Showcase_VLauncher_icons.jpg | Bin 69795 -> 0 bytes media/theme/Showcase_VLauncher_posters.jpg | Bin 53459 -> 0 bytes media/theme/Sources_icon.png | Bin 0 -> 38413 bytes media/theme/Sources_poster.png | Bin 0 -> 86362 bytes .../lib/commands/view_rendering_commands.py | 32 ++++++++++-------- resources/lib/viewqueries.py | 8 ++--- resources/lib/views.py | 4 +-- 14 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 media/theme/Launchers_icon.png create mode 100644 media/theme/Launchers_poster.png delete mode 100644 media/theme/Libraries_icon.png delete mode 100644 media/theme/Libraries_poster.png delete mode 100644 media/theme/Showcase_Browse_by_icons.jpg delete mode 100644 media/theme/Showcase_Browse_by_posters.jpg delete mode 100644 media/theme/Showcase_VLauncher_icons.jpg delete mode 100644 media/theme/Showcase_VLauncher_posters.jpg create mode 100644 media/theme/Sources_icon.png create mode 100644 media/theme/Sources_poster.png diff --git a/SKINNING.md b/SKINNING.md index 1f9e2829..63efddde 100644 --- a/SKINNING.md +++ b/SKINNING.md @@ -299,3 +299,5 @@ Artwork directory ADDON_DATA_DIR/asset-launchers/ * User will have to reorganise artwork directories to take full advantage of AEL capabilities after importing AL `launchers.xml`. + +Font used: Agency FB \ No newline at end of file diff --git a/media/theme/Launchers_icon.png b/media/theme/Launchers_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7716970703a3f36922322d95e9b0f3275e2f1ac8 GIT binary patch literal 36815 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%0Deb_XX2t|&hCfz8ryeRF(%E1bTU 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 z

B`Ot@VQe=Ol%ML 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