From fbb2ccec574f77ca8c6b00bf14ab8ec89f30f5de Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 18 Jul 2023 14:20:19 +0200 Subject: [PATCH 01/21] silence pil --- src/logging/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging/setup.py b/src/logging/setup.py index 2c0e484..e9737cd 100644 --- a/src/logging/setup.py +++ b/src/logging/setup.py @@ -73,7 +73,7 @@ def setup_logging(): "PIL": { "handlers": ["lib_console_handler", "file_handler"], "propagate": False, - "level": "NOTSET", + "level": "WARNING", }, "urllib3": { "handlers": ["lib_console_handler", "file_handler"], From 00ff29786722c469110e1dd4a840f15aae7e4b19 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 18 Jul 2023 14:23:43 +0200 Subject: [PATCH 02/21] Steam source debug info on skip --- src/importer/sources/steam_source.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 561d843..975f369 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -18,6 +18,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import logging import re from pathlib import Path from time import time @@ -25,13 +26,13 @@ from typing import Iterable from src import shared from src.game import Game +from src.importer.sources.location import Location from src.importer.sources.source import ( SourceIterationResult, SourceIterator, URLExecutableSource, ) from src.utils.steam import SteamFileHelper, SteamInvalidManifestError -from src.importer.sources.location import Location class SteamSourceIterator(SourceIterator): @@ -74,17 +75,20 @@ class SteamSourceIterator(SourceIterator): steam = SteamFileHelper() try: local_data = steam.get_manifest_data(manifest) - except (OSError, SteamInvalidManifestError): + except (OSError, SteamInvalidManifestError) as error: + logging.debug("Couldn't load appmanifest %s", manifest, exc_info=error) continue # Skip non installed games installed_mask = 4 if not int(local_data["stateflags"]) & installed_mask: + logging.debug("Skipped %s: not installed", manifest) continue # Skip duplicate appids appid = local_data["appid"] if appid in appid_cache: + logging.debug("Skipped %s: appid already seen during import", manifest) continue appid_cache.add(appid) From 15da65fccf02812511e4c53a4a1c836c67c92c68 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 05:01:17 +0200 Subject: [PATCH 03/21] WIP heroic source refactor - Fixed installed games lookup - Added support for amazon games TODO - Test (obviously) - Consider getting hidden value --- data/hu.kramo.Cartridges.gschema.xml.in | 3 + src/importer/sources/heroic_source.py | 285 +++++++++++++++++++----- 2 files changed, 229 insertions(+), 59 deletions(-) diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index edd5aec..31c6a68 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -46,6 +46,9 @@ true + + true + true diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 499803b..cedfcd7 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -22,19 +22,40 @@ import json import logging from hashlib import sha256 from json import JSONDecodeError +from pathlib import Path from time import time -from typing import Optional, TypedDict +from typing import Optional, TypedDict, Generator, Iterable +from abc import abstractmethod from src import shared from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import ( - URLExecutableSource, SourceIterationResult, SourceIterator, + URLExecutableSource, ) +def path_json_load(path: Path): + """ + Load JSON from the file at the given path + + :raises OSError: if the file can't be opened + :raises JSONDecodeError: if the file isn't valid JSON + """ + with path.open("r", encoding="utf-8") as open_file: + return json.load(open_file) + + +class InvalidLibraryFileError(Exception): + pass + + +class InvalidInstalledFileError(Exception): + pass + + class HeroicLibraryEntry(TypedDict): app_name: str installed: Optional[bool] @@ -44,49 +65,38 @@ class HeroicLibraryEntry(TypedDict): art_square: str -class HeroicSubSource(TypedDict): - service: str - path: tuple[str] +class SubSource(Iterable): + """Class representing a Heroic sub-source""" - -class HeroicSourceIterator(SourceIterator): source: "HeroicSource" + name: str + service: str + image_uri_params: str = "" + relative_library_path: Path + library_json_entries_key: str = "library" - sub_sources: dict[str, HeroicSubSource] = { - "sideload": { - "service": "sideload", - "path": ("sideload_apps", "library.json"), - }, - "legendary": { - "service": "epic", - "path": ("store_cache", "legendary_library.json"), - }, - "gog": { - "service": "gog", - "path": ("store_cache", "gog_library.json"), - }, - } + def __init__(self, source) -> None: + self.source = source - def game_from_library_entry( + @property + def library_path(self) -> Path: + return self.source.config_location.root / self.relative_library_path + + def process_library_entry( self, entry: HeroicLibraryEntry, added_time: int ) -> SourceIterationResult: - """Helper method used to build a Game from a Heroic library entry""" + """Build a Game from a Heroic library entry""" - # Skip games that are not installed - if not entry["is_installed"]: - return None + app_name = entry["app_name"] # Build game - app_name = entry["app_name"] - runner = entry["runner"] - service = self.sub_sources[runner]["service"] values = { - "source": f"{self.source.id}_{service}", + "source": f"{self.source.id}_{self.service}", "added": added_time, "name": entry["title"], "developer": entry.get("developer", None), "game_id": self.source.game_id_format.format( - service=service, game_id=app_name + service=self.service, game_id=app_name ), "executable": self.source.executable_format.format(app_name=app_name), } @@ -94,45 +104,202 @@ class HeroicSourceIterator(SourceIterator): # Get the image path from the heroic cache # Filenames are derived from the URL that heroic used to get the file - uri: str = entry["art_square"] - if service == "epic": - uri += "?h=400&resize=1&w=300" + uri: str = entry["art_square"] + self.image_uri_params digest = sha256(uri.encode()).hexdigest() image_path = self.source.config_location.root / "images-cache" / digest additional_data = {"local_image_path": image_path} return (game, additional_data) - def generator_builder(self) -> SourceIterationResult: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: + """ + Iterate through the installed games with a generator + :raises InvalidLibraryFileError: on initial call if the library file is bad + """ + added_time = int(time()) + try: + iterator = iter( + path_json_load(self.library_path)[self.library_json_entries_key] + ) + except (OSError, JSONDecodeError, TypeError, KeyError) as error: + raise InvalidLibraryFileError( + f"Invalid {self.library_path.name}" + ) from error + for entry in iterator: + try: + yield self.process_library_entry(entry, added_time) + except KeyError as error: + logging.warning( + "Skipped invalid %s game %s", + self.name, + entry.get("app_name", "UNKNOWN"), + exc_info=error, + ) + continue + + +class StoreSubSource(SubSource): + """ + Class representing a "store" sub source iterator. + Games can be installed or not, this class does the check accordingly. + """ + + relative_installed_path: Optional[Path] + installed_app_names: set[str] + + @property + def installed_path(self) -> Path: + return self.source.config_location.root / self.relative_installed_path + + @abstractmethod + def get_installed_app_names(self) -> set[str]: + """ + Get the sub source's installed app names as a set. + + :raises InvalidInstalledFileError: if the installed file data cannot be read + Whenever possible, `__cause__` is set with the original exception + """ + + def is_installed(self, app_name: str) -> bool: + return app_name in self.installed_app_names + + def process_library_entry(self, entry, added_time): + # Skip games that are not installed + app_name = entry["app_name"] + if not self.is_installed(app_name): + logging.warning( + "Skipped %s game %s (%s): not installed", + self.service, + entry["title"], + app_name, + ) + return None + # Process entry as normal + return super().process_library_entry(entry, added_time) + + def __iter__(self): + """ + Iterate through the installed games with a generator + :raises InvalidLibraryFileError: on initial call if the library file is bad + :raises InvalidInstalledFileError: on initial call if the installed file is bad + """ + self.installed_app_names = self.get_installed_app_names() + # TODO check that this syntax works + yield from super() + + +class SideloadIterable(SubSource): + name = "sideload" + service = "sideload" + relative_library_path = Path("sideload_apps") / "library.json" + + +class LegendaryIterable(StoreSubSource): + name = "legendary" + service = "epic" + image_uri_params = "?h=400&resize=1&w=300" + relative_library_path = Path("store_cache") / "legendary_library.json" + + # TODO simplify Heroic 2.9 has been out for a while + # (uncomment value and remove the library_path property override) + # + # relative_installed_path = ( + # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" + # ) + relative_installed_path = None + + @property + def library_path(self) -> Path: + """Get the right path depending on the Heroic version""" + heroic_config_path = self.source.config_location.root + if (path := heroic_config_path / "legendaryConfig").is_dir(): + # Heroic > 2.9 + pass + elif heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak < 2.8 + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + else: + # Heroic < 2.8 + path = Path.home() / ".config" + return path / "legendary" / "installed.json" + + def get_installed_app_names(self): + try: + return set(path_json_load(self.installed_path).keys()) + except (OSError, JSONDecodeError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class GogIterable(StoreSubSource): + name = "gog" + service = "gog" + library_json_entries_key = "games" + relative_library_path = Path("store_cache") / "gog_library.json" + relative_installed_path = Path("gog_store") / "installed.json" + + def get_installed_app_names(self): + try: + return ( + app_name + for entry in path_json_load(self.installed_path)["installed"] + if (app_name := entry.get("appName")) is not None + ) + except (OSError, JSONDecodeError, KeyError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class NileIterable(StoreSubSource): + name = "nile" + service = "amazon" + relative_library_path = Path("store_cache") / "nile_library.json" + relative_installed_path = Path("nile_config") / "nile" / "installed.json" + + def get_installed_app_names(self): + try: + installed_json = path_json_load(self.installed_path) + return ( + app_name + for entry in installed_json + if (app_name := entry.get("id")) is not None + ) + except (OSError, JSONDecodeError, AttributeError) as error: + raise InvalidInstalledFileError( + f"Invalid {self.installed_path.name}" + ) from error + + +class HeroicSourceIterator(SourceIterator): + source: "HeroicSource" + + def __iter__(self): """Generator method producing games from all the Heroic sub-sources""" - for sub_source_name, sub_source in self.sub_sources.items(): - # Skip disabled sub-sources - if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]): + for sub_source_class in ( + SideloadIterable, + LegendaryIterable, + GogIterable, + NileIterable, + ): + sub_source = sub_source_class(self.source) + + if not shared.schema.get_boolean("heroic-import-" + sub_source.service): + logging.debug("Skipping Heroic %s: disabled", sub_source.service) continue - # Load games from JSON - file = self.source.config_location.root.joinpath(*sub_source["path"]) + try: - contents = json.load(file.open()) - key = "library" if sub_source_name == "legendary" else "games" - library = contents[key] - except (JSONDecodeError, OSError, KeyError): - # Invalid library.json file, skip it - logging.warning("Couldn't open Heroic file: %s", str(file)) - continue + sub_source_iterator = iter(sub_source) + except (InvalidLibraryFileError, InvalidInstalledFileError) as error: + logging.error( + "Skipping bad Heroic sub-source %s", + sub_source.service, + exc_info=error, + ) - added_time = int(time()) - - for entry in library: - try: - result = self.game_from_library_entry(entry, added_time) - except KeyError as error: - # Skip invalid games - logging.warning( - "Invalid Heroic game skipped in %s", str(file), exc_info=error - ) - continue - yield result + yield from sub_source_iterator class HeroicSource(URLExecutableSource): From 0601fd5ebb41d2e52a1cbc7794365bd7880a4ac9 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 05:32:47 +0200 Subject: [PATCH 04/21] Converted genexps to setcomps --- src/importer/sources/heroic_source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index cedfcd7..b3ad604 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -241,11 +241,11 @@ class GogIterable(StoreSubSource): def get_installed_app_names(self): try: - return ( + return { app_name for entry in path_json_load(self.installed_path)["installed"] if (app_name := entry.get("appName")) is not None - ) + } except (OSError, JSONDecodeError, KeyError, AttributeError) as error: raise InvalidInstalledFileError( f"Invalid {self.installed_path.name}" @@ -261,11 +261,11 @@ class NileIterable(StoreSubSource): def get_installed_app_names(self): try: installed_json = path_json_load(self.installed_path) - return ( + return { app_name for entry in installed_json if (app_name := entry.get("id")) is not None - ) + } except (OSError, JSONDecodeError, AttributeError) as error: raise InvalidInstalledFileError( f"Invalid {self.installed_path.name}" From a0bfca01d6922ceac96ea3d1e4b1cb829bedb066 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 05:33:55 +0200 Subject: [PATCH 05/21] WIP added support for heroic hidden TODO - Test all of that --- src/importer/sources/heroic_source.py | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index b3ad604..e3ba9bd 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -56,6 +56,10 @@ class InvalidInstalledFileError(Exception): pass +class InvalidStoreFileError(Exception): + pass + + class HeroicLibraryEntry(TypedDict): app_name: str installed: Optional[bool] @@ -69,14 +73,16 @@ class SubSource(Iterable): """Class representing a Heroic sub-source""" source: "HeroicSource" + source_iterator: "HeroicSourceIterator" name: str service: str image_uri_params: str = "" relative_library_path: Path library_json_entries_key: str = "library" - def __init__(self, source) -> None: + def __init__(self, source, source_iterator) -> None: self.source = source + self.source_iterator = source_iterator @property def library_path(self) -> Path: @@ -99,6 +105,7 @@ class SubSource(Iterable): service=self.service, game_id=app_name ), "executable": self.source.executable_format.format(app_name=app_name), + "hidden": self.source_iterator.is_hidden(app_name), } game = Game(values) @@ -275,16 +282,32 @@ class NileIterable(StoreSubSource): class HeroicSourceIterator(SourceIterator): source: "HeroicSource" + hidden_app_names: set[str] = set() + + def is_hidden(self, app_name: str) -> bool: + return app_name in self.hidden_app_names + def __iter__(self): """Generator method producing games from all the Heroic sub-sources""" + # Get the hidden app names + try: + store = path_json_load(self.source.config_location["store_config.json"]) + self.hidden_app_names = { + game["appName"] for game in store["games"]["hidden"] + } + except (OSError, JSONDecodeError, KeyError, TypeError) as error: + logging.error("Invalid Heroic store file", exc_info=error) + raise InvalidStoreFileError() from error + + # Get games from the sub sources for sub_source_class in ( SideloadIterable, LegendaryIterable, GogIterable, NileIterable, ): - sub_source = sub_source_class(self.source) + sub_source = sub_source_class(self.source, self) if not shared.schema.get_boolean("heroic-import-" + sub_source.service): logging.debug("Skipping Heroic %s: disabled", sub_source.service) @@ -320,6 +343,7 @@ class HeroicSource(URLExecutableSource): ), paths={ "config.json": (False, "config.json"), + "store_config.json": (False, ("store", "config.json")), }, ) From 8839db272b49daf0d7bb80cad5907b0604df2e95 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 13:00:29 +0200 Subject: [PATCH 06/21] better legendary sub-source library path detection --- src/importer/sources/heroic_source.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index e3ba9bd..b194650 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -222,12 +222,17 @@ class LegendaryIterable(StoreSubSource): if (path := heroic_config_path / "legendaryConfig").is_dir(): # Heroic > 2.9 pass - elif heroic_config_path.is_relative_to(shared.flatpak_dir): - # Heroic flatpak < 2.8 - path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" else: - # Heroic < 2.8 - path = Path.home() / ".config" + # Heroic <= 2.8 + if heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + elif shared.config_dir.is_relative_to(shared.flatpak_dir): + # Heroic native (from Cartridges flatpak) + path = Path.home() / ".config" + else: + # Heroic native (from other Cartridges installations) + path = shared.config_dir return path / "legendary" / "installed.json" def get_installed_app_names(self): From a399113ff99e71cd1bff9908eeec2e95f07e0fea Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 19 Jul 2023 13:12:07 +0200 Subject: [PATCH 07/21] fixed typo --- src/importer/sources/heroic_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index b194650..5150696 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -220,7 +220,7 @@ class LegendaryIterable(StoreSubSource): """Get the right path depending on the Heroic version""" heroic_config_path = self.source.config_location.root if (path := heroic_config_path / "legendaryConfig").is_dir(): - # Heroic > 2.9 + # Heroic >= 2.9 pass else: # Heroic <= 2.8 From 2acdedf033367d0ea33d3dcf04393d0e9a8707e5 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:29:27 +0200 Subject: [PATCH 08/21] Added heroic import amazon to ui + fixes --- data/gtk/preferences.blp | 9 +++++++++ data/hu.kramo.Cartridges.gschema.xml.in | 4 ++-- src/importer/sources/heroic_source.py | 2 +- src/preferences.py | 2 ++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index fbc2dee..7aa63b9 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -174,6 +174,15 @@ template $PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ActionRow { + title: _("Import Amazon Games"); + activatable-widget: heroic_import_amazon_switch; + + Switch heroic_import_amazon_switch { + valign: center; + } + } + Adw.ActionRow { title: _("Import Sideloaded Games"); activatable-widget: heroic_import_sideload_switch; diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index 31c6a68..b0a614d 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -43,10 +43,10 @@ true - + true - + true diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 5150696..a3dce89 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -348,7 +348,7 @@ class HeroicSource(URLExecutableSource): ), paths={ "config.json": (False, "config.json"), - "store_config.json": (False, ("store", "config.json")), + "store_config.json": (False, Path("store") / "config.json"), }, ) diff --git a/src/preferences.py b/src/preferences.py index 9e9be4d..fdafd69 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -68,6 +68,7 @@ class PreferencesWindow(Adw.PreferencesWindow): heroic_config_file_chooser_button = Gtk.Template.Child() heroic_import_epic_switch = Gtk.Template.Child() heroic_import_gog_switch = Gtk.Template.Child() + heroic_import_amazon_switch = Gtk.Template.Child() heroic_import_sideload_switch = Gtk.Template.Child() bottles_expander_row = Gtk.Template.Child() @@ -181,6 +182,7 @@ class PreferencesWindow(Adw.PreferencesWindow): "lutris-import-flatpak", "heroic-import-epic", "heroic-import-gog", + "heroic-import-amazon", "heroic-import-sideload", "flatpak-import-launchers", "sgdb", From 30152cd10afda3389789830b047e566db7d8ef55 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:29:39 +0200 Subject: [PATCH 09/21] simplified SourceIterator --- src/importer/sources/bottles_source.py | 8 ++------ src/importer/sources/flatpak_source.py | 4 ++-- src/importer/sources/heroic_source.py | 4 ++-- src/importer/sources/itch_source.py | 8 ++------ src/importer/sources/legendary_source.py | 3 +-- src/importer/sources/lutris_source.py | 8 ++------ src/importer/sources/source.py | 15 +++------------ src/importer/sources/steam_source.py | 8 ++------ 8 files changed, 16 insertions(+), 42 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index d993598..f945740 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -26,17 +26,13 @@ import yaml from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource class BottlesSourceIterator(SourceIterator): source: "BottlesSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" data = self.source.data_location["library.yml"].read_text("utf-8") diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 6ec6262..4195d21 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -25,13 +25,13 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.source import Source, SourceIterator class FlatpakSourceIterator(SourceIterator): source: "FlatpakSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" added_time = int(time()) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index a3dce89..0a7038a 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -24,7 +24,7 @@ from hashlib import sha256 from json import JSONDecodeError from pathlib import Path from time import time -from typing import Optional, TypedDict, Generator, Iterable +from typing import Optional, TypedDict, Iterable from abc import abstractmethod from src import shared @@ -118,7 +118,7 @@ class SubSource(Iterable): return (game, additional_data) - def __iter__(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self): """ Iterate through the installed games with a generator :raises InvalidLibraryFileError: on initial call if the library file is bad diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 141c9f9..6659f7c 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -25,18 +25,14 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource from src.utils.sqlite import copy_db class ItchSourceIterator(SourceIterator): source: "ItchSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" # Query the database diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 22392be..2e43948 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -21,7 +21,6 @@ import json import logging from json import JSONDecodeError from time import time -from typing import Generator from src import shared from src.game import Game @@ -65,7 +64,7 @@ class LegendarySourceIterator(SourceIterator): game = Game(values) return (game, data) - def generator_builder(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self): # Open library file = self.source.config_location["installed.json"] try: diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index ec4b066..9077591 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -24,18 +24,14 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource from src.utils.sqlite import copy_db class LutrisSourceIterator(SourceIterator): source: "LutrisSource" - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" # Query the database diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index cb9eb0c..2abfbde 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -29,25 +29,16 @@ from src.importer.sources.location import Location SourceIterationResult = None | Game | tuple[Game, tuple[Any]] -class SourceIterator(Iterator): +class SourceIterator: """Data producer for a source of games""" source: "Source" = None - generator: Generator = None def __init__(self, source: "Source") -> None: - super().__init__() self.source = source - self.generator = self.generator_builder() - - def __iter__(self) -> "SourceIterator": - return self - - def __next__(self) -> SourceIterationResult: - return next(self.generator) @abstractmethod - def generator_builder(self) -> Generator[SourceIterationResult, None, None]: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Method that returns a generator that produces games * Should be implemented as a generator method @@ -108,7 +99,7 @@ class Source(Iterable): if location is None: continue location.resolve() - return self.iterator_class(self) + return iter(self.iterator_class(self)) # pylint: disable=abstract-method diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 975f369..a47a90c 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -27,11 +27,7 @@ from typing import Iterable from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import ( - SourceIterationResult, - SourceIterator, - URLExecutableSource, -) +from src.importer.sources.source import SourceIterator, URLExecutableSource from src.utils.steam import SteamFileHelper, SteamInvalidManifestError @@ -63,7 +59,7 @@ class SteamSourceIterator(SourceIterator): ) return manifests - def generator_builder(self) -> SourceIterationResult: + def __iter__(self): """Generator method producing games""" appid_cache = set() manifests = self.get_manifests() From 7f576d1bd3667252e37ea7eee19494d61e33dec5 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:32:43 +0200 Subject: [PATCH 10/21] SourceIterator is actually just SourceIterable --- src/importer/sources/bottles_source.py | 6 +++--- src/importer/sources/flatpak_source.py | 6 +++--- src/importer/sources/heroic_source.py | 8 ++++---- src/importer/sources/itch_source.py | 6 +++--- src/importer/sources/legendary_source.py | 6 +++--- src/importer/sources/lutris_source.py | 6 +++--- src/importer/sources/source.py | 6 +++--- src/importer/sources/steam_source.py | 6 +++--- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index f945740..22e071b 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -26,10 +26,10 @@ import yaml from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource -class BottlesSourceIterator(SourceIterator): +class BottlesSourceIterable(SourceIterable): source: "BottlesSource" def __iter__(self): @@ -80,7 +80,7 @@ class BottlesSource(URLExecutableSource): """Generic Bottles source""" name = _("Bottles") - iterator_class = BottlesSourceIterator + iterator_class = BottlesSourceIterable url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 4195d21..2c52dab 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -25,10 +25,10 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterator +from src.importer.sources.source import Source, SourceIterable -class FlatpakSourceIterator(SourceIterator): +class FlatpakSourceIterable(SourceIterable): source: "FlatpakSource" def __iter__(self): @@ -115,7 +115,7 @@ class FlatpakSource(Source): """Generic Flatpak source""" name = _("Flatpak") - iterator_class = FlatpakSourceIterator + iterator_class = FlatpakSourceIterable executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 0a7038a..1b9ff6e 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -32,7 +32,7 @@ from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import ( SourceIterationResult, - SourceIterator, + SourceIterable, URLExecutableSource, ) @@ -73,7 +73,7 @@ class SubSource(Iterable): """Class representing a Heroic sub-source""" source: "HeroicSource" - source_iterator: "HeroicSourceIterator" + source_iterator: "HeroicSourceIterable" name: str service: str image_uri_params: str = "" @@ -284,7 +284,7 @@ class NileIterable(StoreSubSource): ) from error -class HeroicSourceIterator(SourceIterator): +class HeroicSourceIterable(SourceIterable): source: "HeroicSource" hidden_app_names: set[str] = set() @@ -334,7 +334,7 @@ class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" name = _("Heroic") - iterator_class = HeroicSourceIterator + iterator_class = HeroicSourceIterable url_format = "heroic://launch/{app_name}" available_on = {"linux", "win32"} diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 6659f7c..b6c0ad3 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -25,11 +25,11 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db -class ItchSourceIterator(SourceIterator): +class ItchSourceIterable(SourceIterable): source: "ItchSource" def __iter__(self): @@ -76,7 +76,7 @@ class ItchSourceIterator(SourceIterator): class ItchSource(URLExecutableSource): name = _("itch") - iterator_class = ItchSourceIterator + iterator_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 2e43948..72eae35 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -25,10 +25,10 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import Source, SourceIterationResult, SourceIterator +from src.importer.sources.source import Source, SourceIterationResult, SourceIterable -class LegendarySourceIterator(SourceIterator): +class LegendarySourceIterable(SourceIterable): source: "LegendarySource" def game_from_library_entry( @@ -93,7 +93,7 @@ class LegendarySource(Source): executable_format = "legendary launch {app_name}" available_on = {"linux"} - iterator_class = LegendarySourceIterator + iterator_class = LegendarySourceIterable config_location: Location = Location( schema_key="legendary-location", candidates=( diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 9077591..8696aef 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -24,11 +24,11 @@ from time import time from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.sqlite import copy_db -class LutrisSourceIterator(SourceIterator): +class LutrisSourceIterable(SourceIterable): source: "LutrisSource" def __iter__(self): @@ -87,7 +87,7 @@ class LutrisSource(URLExecutableSource): """Generic Lutris source""" name = _("Lutris") - iterator_class = LutrisSourceIterator + iterator_class = LutrisSourceIterable url_format = "lutris:rungameid/{game_id}" available_on = {"linux"} diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 2abfbde..383dc1c 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -29,7 +29,7 @@ from src.importer.sources.location import Location SourceIterationResult = None | Game | tuple[Game, tuple[Any]] -class SourceIterator: +class SourceIterable(Iterable): """Data producer for a source of games""" source: "Source" = None @@ -57,7 +57,7 @@ class Source(Iterable): data_location: Optional[Location] = None cache_location: Optional[Location] = None config_location: Optional[Location] = None - iterator_class: type[SourceIterator] + iterator_class: type[SourceIterable] @property def full_name(self) -> str: @@ -89,7 +89,7 @@ class Source(Iterable): def executable_format(self) -> str: """The executable format used to construct game executables""" - def __iter__(self) -> SourceIterator: + def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ Get an iterator for the source :raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index a47a90c..040edcb 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -27,11 +27,11 @@ from typing import Iterable from src import shared from src.game import Game from src.importer.sources.location import Location -from src.importer.sources.source import SourceIterator, URLExecutableSource +from src.importer.sources.source import SourceIterable, URLExecutableSource from src.utils.steam import SteamFileHelper, SteamInvalidManifestError -class SteamSourceIterator(SourceIterator): +class SteamSourceIterable(SourceIterable): source: "SteamSource" def get_manifest_dirs(self) -> Iterable[Path]: @@ -112,7 +112,7 @@ class SteamSourceIterator(SourceIterator): class SteamSource(URLExecutableSource): name = _("Steam") available_on = {"linux", "win32"} - iterator_class = SteamSourceIterator + iterator_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" data_location = Location( From 0df123975cc08bb8456daec5bf77ac1942a89c5e Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:35:07 +0200 Subject: [PATCH 11/21] Fix some syntax --- src/importer/sources/heroic_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 1b9ff6e..bb2c053 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -191,8 +191,7 @@ class StoreSubSource(SubSource): :raises InvalidInstalledFileError: on initial call if the installed file is bad """ self.installed_app_names = self.get_installed_app_names() - # TODO check that this syntax works - yield from super() + yield from super().__iter__() class SideloadIterable(SubSource): From 52b6c47c8d805ffdd022261b95865dbc5ebcd2c1 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 10:58:23 +0200 Subject: [PATCH 12/21] More renaming to iterable + fixes to heroic --- src/importer/sources/bottles_source.py | 2 +- src/importer/sources/flatpak_source.py | 2 +- src/importer/sources/heroic_source.py | 40 +++++++++++------------- src/importer/sources/itch_source.py | 2 +- src/importer/sources/legendary_source.py | 2 +- src/importer/sources/lutris_source.py | 2 +- src/importer/sources/source.py | 4 +-- src/importer/sources/steam_source.py | 2 +- 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 22e071b..8829023 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -80,7 +80,7 @@ class BottlesSource(URLExecutableSource): """Generic Bottles source""" name = _("Bottles") - iterator_class = BottlesSourceIterable + iterable_class = BottlesSourceIterable url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index 2c52dab..2c9a4aa 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -115,7 +115,7 @@ class FlatpakSource(Source): """Generic Flatpak source""" name = _("Flatpak") - iterator_class = FlatpakSourceIterable + iterable_class = FlatpakSourceIterable executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index bb2c053..3e53787 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -69,20 +69,20 @@ class HeroicLibraryEntry(TypedDict): art_square: str -class SubSource(Iterable): +class SubSourceIterable(Iterable): """Class representing a Heroic sub-source""" source: "HeroicSource" - source_iterator: "HeroicSourceIterable" + source_iterable: "HeroicSourceIterable" name: str service: str image_uri_params: str = "" relative_library_path: Path library_json_entries_key: str = "library" - def __init__(self, source, source_iterator) -> None: + def __init__(self, source, source_iterable) -> None: self.source = source - self.source_iterator = source_iterator + self.source_iterable = source_iterable @property def library_path(self) -> Path: @@ -105,7 +105,7 @@ class SubSource(Iterable): service=self.service, game_id=app_name ), "executable": self.source.executable_format.format(app_name=app_name), - "hidden": self.source_iterator.is_hidden(app_name), + "hidden": self.source_iterable.is_hidden(app_name), } game = Game(values) @@ -120,7 +120,7 @@ class SubSource(Iterable): def __iter__(self): """ - Iterate through the installed games with a generator + Iterate through the games with a generator :raises InvalidLibraryFileError: on initial call if the library file is bad """ added_time = int(time()) @@ -145,13 +145,13 @@ class SubSource(Iterable): continue -class StoreSubSource(SubSource): +class StoreSubSourceIterable(SubSourceIterable): """ - Class representing a "store" sub source iterator. + Class representing a "store" sub source. Games can be installed or not, this class does the check accordingly. """ - relative_installed_path: Optional[Path] + relative_installed_path: Path installed_app_names: set[str] @property @@ -194,28 +194,27 @@ class StoreSubSource(SubSource): yield from super().__iter__() -class SideloadIterable(SubSource): +class SideloadIterable(SubSourceIterable): name = "sideload" service = "sideload" relative_library_path = Path("sideload_apps") / "library.json" -class LegendaryIterable(StoreSubSource): +class LegendaryIterable(StoreSubSourceIterable): name = "legendary" service = "epic" image_uri_params = "?h=400&resize=1&w=300" relative_library_path = Path("store_cache") / "legendary_library.json" # TODO simplify Heroic 2.9 has been out for a while - # (uncomment value and remove the library_path property override) + # (uncomment value and remove the installed_path property override) # # relative_installed_path = ( # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" # ) - relative_installed_path = None @property - def library_path(self) -> Path: + def installed_path(self) -> Path: """Get the right path depending on the Heroic version""" heroic_config_path = self.source.config_location.root if (path := heroic_config_path / "legendaryConfig").is_dir(): @@ -243,7 +242,7 @@ class LegendaryIterable(StoreSubSource): ) from error -class GogIterable(StoreSubSource): +class GogIterable(StoreSubSourceIterable): name = "gog" service = "gog" library_json_entries_key = "games" @@ -263,7 +262,7 @@ class GogIterable(StoreSubSource): ) from error -class NileIterable(StoreSubSource): +class NileIterable(StoreSubSourceIterable): name = "nile" service = "amazon" relative_library_path = Path("store_cache") / "nile_library.json" @@ -316,24 +315,23 @@ class HeroicSourceIterable(SourceIterable): if not shared.schema.get_boolean("heroic-import-" + sub_source.service): logging.debug("Skipping Heroic %s: disabled", sub_source.service) continue - try: - sub_source_iterator = iter(sub_source) + sub_source_iterable = iter(sub_source) + yield from sub_source_iterable except (InvalidLibraryFileError, InvalidInstalledFileError) as error: logging.error( "Skipping bad Heroic sub-source %s", sub_source.service, exc_info=error, ) - - yield from sub_source_iterator + continue class HeroicSource(URLExecutableSource): """Generic Heroic Games Launcher source""" name = _("Heroic") - iterator_class = HeroicSourceIterable + iterable_class = HeroicSourceIterable url_format = "heroic://launch/{app_name}" available_on = {"linux", "win32"} diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index b6c0ad3..a6d8990 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -76,7 +76,7 @@ class ItchSourceIterable(SourceIterable): class ItchSource(URLExecutableSource): name = _("itch") - iterator_class = ItchSourceIterable + iterable_class = ItchSourceIterable url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 72eae35..c7e06de 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -93,7 +93,7 @@ class LegendarySource(Source): executable_format = "legendary launch {app_name}" available_on = {"linux"} - iterator_class = LegendarySourceIterable + iterable_class = LegendarySourceIterable config_location: Location = Location( schema_key="legendary-location", candidates=( diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 8696aef..7c100a8 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -87,7 +87,7 @@ class LutrisSource(URLExecutableSource): """Generic Lutris source""" name = _("Lutris") - iterator_class = LutrisSourceIterable + iterable_class = LutrisSourceIterable url_format = "lutris:rungameid/{game_id}" available_on = {"linux"} diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 383dc1c..d7ba467 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -57,7 +57,7 @@ class Source(Iterable): data_location: Optional[Location] = None cache_location: Optional[Location] = None config_location: Optional[Location] = None - iterator_class: type[SourceIterable] + iterable_class: type[SourceIterable] @property def full_name(self) -> str: @@ -99,7 +99,7 @@ class Source(Iterable): if location is None: continue location.resolve() - return iter(self.iterator_class(self)) + return iter(self.iterable_class(self)) # pylint: disable=abstract-method diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 040edcb..7e65e5b 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -112,7 +112,7 @@ class SteamSourceIterable(SourceIterable): class SteamSource(URLExecutableSource): name = _("Steam") available_on = {"linux", "win32"} - iterator_class = SteamSourceIterable + iterable_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" data_location = Location( From 190b446de5e63b6729c23f54cd69a8f8d20141ac Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 11:26:05 +0200 Subject: [PATCH 13/21] More debug messages + fix sideloaded heroic games --- src/importer/sources/heroic_source.py | 31 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 3e53787..d82b111 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -86,7 +86,9 @@ class SubSourceIterable(Iterable): @property def library_path(self) -> Path: - return self.source.config_location.root / self.relative_library_path + path = self.source.config_location.root / self.relative_library_path + logging.debug("Using Heroic %s library.json path %s", self.name, path) + return path def process_library_entry( self, entry: HeroicLibraryEntry, added_time: int @@ -156,7 +158,9 @@ class StoreSubSourceIterable(SubSourceIterable): @property def installed_path(self) -> Path: - return self.source.config_location.root / self.relative_installed_path + path = self.source.config_location.root / self.relative_installed_path + logging.debug("Using Heroic %s installed.json path %s", self.name, path) + return path @abstractmethod def get_installed_app_names(self) -> set[str]: @@ -198,6 +202,7 @@ class SideloadIterable(SubSourceIterable): name = "sideload" service = "sideload" relative_library_path = Path("sideload_apps") / "library.json" + library_json_entries_key = "games" class LegendaryIterable(StoreSubSourceIterable): @@ -206,32 +211,42 @@ class LegendaryIterable(StoreSubSourceIterable): image_uri_params = "?h=400&resize=1&w=300" relative_library_path = Path("store_cache") / "legendary_library.json" - # TODO simplify Heroic 2.9 has been out for a while - # (uncomment value and remove the installed_path property override) - # # relative_installed_path = ( # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" # ) @property def installed_path(self) -> Path: - """Get the right path depending on the Heroic version""" + """ + Get the right path depending on the Heroic version + + TODO after heroic 2.9 has been out for a while + We should use the commented out relative_installed_path + and remove this property override. + """ + heroic_config_path = self.source.config_location.root if (path := heroic_config_path / "legendaryConfig").is_dir(): # Heroic >= 2.9 - pass + logging.debug("Using Heroic >= 2.9 legendary file") else: # Heroic <= 2.8 if heroic_config_path.is_relative_to(shared.flatpak_dir): # Heroic flatpak path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + logging.debug("Using Heroic flatpak <= 2.8 legendary file") elif shared.config_dir.is_relative_to(shared.flatpak_dir): # Heroic native (from Cartridges flatpak) + logging.debug("Using Heroic native <= 2.8 legendary file") path = Path.home() / ".config" else: # Heroic native (from other Cartridges installations) + logging.debug("Using Heroic native <= 2.8 legendary file") path = shared.config_dir - return path / "legendary" / "installed.json" + + path = path / "legendary" / "installed.json" + logging.debug("Using Heroic %s installed.json path %s", self.name, path) + return path def get_installed_app_names(self): try: From bb4870e99d977d9c576ba1f3300f1b7c089128dc Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 11:33:55 +0200 Subject: [PATCH 14/21] Add debug message to local cover manager --- src/store/managers/local_cover_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/store/managers/local_cover_manager.py b/src/store/managers/local_cover_manager.py index b95c22b..0b95f30 100644 --- a/src/store/managers/local_cover_manager.py +++ b/src/store/managers/local_cover_manager.py @@ -18,6 +18,8 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import logging + from gi.repository import GdkPixbuf from src import shared @@ -35,6 +37,7 @@ class LocalCoverManager(Manager): def manager_logic(self, game: Game, additional_data: dict) -> None: if image_path := additional_data.get("local_image_path"): if not image_path.is_file(): + logging.error("Local image path is not a file: %s", image_path) return save_cover(game.game_id, resize_cover(image_path)) elif icon_path := additional_data.get("local_icon_path"): From 82dddd1c5cc29584eb35ae33d57c29eb342ac0f1 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Thu, 20 Jul 2023 19:52:59 +0200 Subject: [PATCH 15/21] Skip missing hidden key --- src/importer/sources/heroic_source.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index d82b111..378c3b3 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -20,19 +20,19 @@ import json import logging +from abc import abstractmethod from hashlib import sha256 from json import JSONDecodeError from pathlib import Path from time import time -from typing import Optional, TypedDict, Iterable -from abc import abstractmethod +from typing import Iterable, Optional, TypedDict from src import shared from src.game import Game from src.importer.sources.location import Location from src.importer.sources.source import ( - SourceIterationResult, SourceIterable, + SourceIterationResult, URLExecutableSource, ) @@ -311,9 +311,10 @@ class HeroicSourceIterable(SourceIterable): # Get the hidden app names try: store = path_json_load(self.source.config_location["store_config.json"]) - self.hidden_app_names = { - game["appName"] for game in store["games"]["hidden"] - } + if "hidden" in store["games"].keys(): + self.hidden_app_names = { + game["appName"] for game in store["games"]["hidden"] + } except (OSError, JSONDecodeError, KeyError, TypeError) as error: logging.error("Invalid Heroic store file", exc_info=error) raise InvalidStoreFileError() from error From 270fa2092cb3d532083c11c394d9fa4f7ea60bd7 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 20:56:28 +0200 Subject: [PATCH 16/21] Fixed heroic location candidates priority --- src/importer/sources/heroic_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 378c3b3..32a8d5e 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -354,9 +354,9 @@ class HeroicSource(URLExecutableSource): config_location = Location( schema_key="heroic-location", candidates=( - shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", shared.config_dir / "heroic", shared.home / ".config" / "heroic", + shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", shared.appdata_dir / "heroic", ), paths={ From 45877209342eab5d1b1436c8313e95227d1bd9b6 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 21:06:04 +0200 Subject: [PATCH 17/21] using cached_property for sub-source paths --- src/importer/sources/heroic_source.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 32a8d5e..8673aaa 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -26,6 +26,7 @@ from json import JSONDecodeError from pathlib import Path from time import time from typing import Iterable, Optional, TypedDict +from functools import cached_property from src import shared from src.game import Game @@ -84,7 +85,7 @@ class SubSourceIterable(Iterable): self.source = source self.source_iterable = source_iterable - @property + @cached_property def library_path(self) -> Path: path = self.source.config_location.root / self.relative_library_path logging.debug("Using Heroic %s library.json path %s", self.name, path) @@ -156,7 +157,7 @@ class StoreSubSourceIterable(SubSourceIterable): relative_installed_path: Path installed_app_names: set[str] - @property + @cached_property def installed_path(self) -> Path: path = self.source.config_location.root / self.relative_installed_path logging.debug("Using Heroic %s installed.json path %s", self.name, path) @@ -215,7 +216,7 @@ class LegendaryIterable(StoreSubSourceIterable): # Path("legendary") / "legendaryConfig" / "legendary" / "installed.json" # ) - @property + @cached_property def installed_path(self) -> Path: """ Get the right path depending on the Heroic version From da777d3605968ac24b68e8873d72fc8df37b75a6 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 20 Jul 2023 21:06:30 +0200 Subject: [PATCH 18/21] Permission for heroic flatpak's legendary files --- flatpak/hu.kramo.Cartridges.Devel.json | 1 + 1 file changed, 1 insertion(+) diff --git a/flatpak/hu.kramo.Cartridges.Devel.json b/flatpak/hu.kramo.Cartridges.Devel.json index 1c2ecea..d0dc5b9 100644 --- a/flatpak/hu.kramo.Cartridges.Devel.json +++ b/flatpak/hu.kramo.Cartridges.Devel.json @@ -15,6 +15,7 @@ "--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro", "--filesystem=~/.var/app/net.lutris.Lutris/:ro", "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", + "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/legendary/:ro", "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro", "--filesystem=~/.var/app/io.itch.itch/config/itch/:ro", "--filesystem=/var/lib/flatpak:ro" From b1992a9466ded8f77b657156195b75785ed9d73f Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Fri, 21 Jul 2023 14:39:07 +0200 Subject: [PATCH 19/21] Fix heroic legendary path detection --- src/importer/sources/heroic_source.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 8673aaa..acd16f5 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -227,23 +227,18 @@ class LegendaryIterable(StoreSubSourceIterable): """ heroic_config_path = self.source.config_location.root + # Heroic >= 2.9 if (path := heroic_config_path / "legendaryConfig").is_dir(): - # Heroic >= 2.9 logging.debug("Using Heroic >= 2.9 legendary file") + # Heroic <= 2.8 + elif heroic_config_path.is_relative_to(shared.flatpak_dir): + # Heroic flatpak + path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" + logging.debug("Using Heroic flatpak <= 2.8 legendary file") else: - # Heroic <= 2.8 - if heroic_config_path.is_relative_to(shared.flatpak_dir): - # Heroic flatpak - path = shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" - logging.debug("Using Heroic flatpak <= 2.8 legendary file") - elif shared.config_dir.is_relative_to(shared.flatpak_dir): - # Heroic native (from Cartridges flatpak) - logging.debug("Using Heroic native <= 2.8 legendary file") - path = Path.home() / ".config" - else: - # Heroic native (from other Cartridges installations) - logging.debug("Using Heroic native <= 2.8 legendary file") - path = shared.config_dir + # Heroic native + logging.debug("Using Heroic native <= 2.8 legendary file") + path = Path.home() / ".config" path = path / "legendary" / "installed.json" logging.debug("Using Heroic %s installed.json path %s", self.name, path) From fbf076660d6eaabb2b2b2c0bb69956c738037486 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sat, 22 Jul 2023 00:04:02 +0200 Subject: [PATCH 20/21] Better heroic store file parsing --- src/importer/sources/heroic_source.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index acd16f5..73461e8 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -307,11 +307,14 @@ class HeroicSourceIterable(SourceIterable): # Get the hidden app names try: store = path_json_load(self.source.config_location["store_config.json"]) - if "hidden" in store["games"].keys(): - self.hidden_app_names = { - game["appName"] for game in store["games"]["hidden"] - } - except (OSError, JSONDecodeError, KeyError, TypeError) as error: + self.hidden_app_names = { + app_name + for game in store["games"]["hidden"] + if (app_name := game.get("appName")) is not None + } + except KeyError: + logging.warning('No ["games"]["hidden"] key in Heroic store file') + except (OSError, JSONDecodeError, TypeError) as error: logging.error("Invalid Heroic store file", exc_info=error) raise InvalidStoreFileError() from error From 7bcb113a3377353b80a127d6dde5b015f3a2e4db Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sat, 22 Jul 2023 00:06:16 +0200 Subject: [PATCH 21/21] extracted get_hidden_app_names to a method --- src/importer/sources/heroic_source.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index 73461e8..d169129 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -301,10 +301,12 @@ class HeroicSourceIterable(SourceIterable): def is_hidden(self, app_name: str) -> bool: return app_name in self.hidden_app_names - def __iter__(self): - """Generator method producing games from all the Heroic sub-sources""" + def get_hidden_app_names(self) -> set[str]: + """Get the hidden app names from store/config.json + + :raises InvalidStoreFileError: if the store is invalid for some reason + """ - # Get the hidden app names try: store = path_json_load(self.source.config_location["store_config.json"]) self.hidden_app_names = { @@ -318,6 +320,11 @@ class HeroicSourceIterable(SourceIterable): logging.error("Invalid Heroic store file", exc_info=error) raise InvalidStoreFileError() from error + def __iter__(self): + """Generator method producing games from all the Heroic sub-sources""" + + self.get_hidden_app_names() + # Get games from the sub sources for sub_source_class in ( SideloadIterable,