diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index ee4ebbe..b4af7c4 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -26,7 +26,7 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location, LocationSubPath -from src.importer.sources.source import Source, SourceIterable +from src.importer.sources.source import ExecutableFormatSource, SourceIterable class FlatpakSourceIterable(SourceIterable): @@ -116,7 +116,7 @@ class FlatpakLocations(NamedTuple): data: Location -class FlatpakSource(Source): +class FlatpakSource(ExecutableFormatSource): """Generic Flatpak source""" source_id = "flatpak" diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 03bbdd2..6c46ada 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -26,7 +26,11 @@ from typing import NamedTuple from src import shared from src.game import Game from src.importer.sources.location import Location, LocationSubPath -from src.importer.sources.source import Source, SourceIterationResult, SourceIterable +from src.importer.sources.source import ( + ExecutableFormatSource, + SourceIterationResult, + SourceIterable, +) class LegendarySourceIterable(SourceIterable): @@ -93,7 +97,7 @@ class LegendaryLocations(NamedTuple): config: Location -class LegendarySource(Source): +class LegendarySource(ExecutableFormatSource): source_id = "legendary" name = _("Legendary") executable_format = "legendary launch {app_name}" diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 96b16e1..b5dc4fb 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -26,6 +26,8 @@ from pathlib import Path from time import time from typing import NamedTuple +from urllib.parse import quote + from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game @@ -103,7 +105,7 @@ class RetroarchSourceIterable(SourceIterable): "added": added_time, "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), - "executable": self.source.executable_format.format( + "executable": self.source.make_executable( rom_path=item["path"], core_path=core_path, ), @@ -168,14 +170,6 @@ class RetroarchSource(Source): ) ) - @property - def executable_format(self): - self.locations.config.resolve() - is_flatpak = self.locations.config.root.is_relative_to(shared.flatpak_dir) - base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" - args = '-L "{core_path}" "{rom_path}"' - return f"{base} {args}" - def __init__(self) -> None: super().__init__() try: @@ -214,3 +208,36 @@ class RetroarchSource(Source): return Path(f"{library_path}/steamapps/common/RetroArch") # Not found raise ValueError("RetroArch not found in Steam library") + + def make_executable(self, rom_path: Path, core_path: Path) -> str: + """ + Generate an executable command from the rom path and core path, + depending on the source's location. + + The format depends on RetroArch's installation method, + detected from the source config location + + :param Path rom_path: the game's rom path + :param Path core_path: the game's core path + :return str: an executable command + """ + + self.locations.config.resolve() + args = f'-L "{core_path}" "{rom_path}"' + + # Steam RetroArch + # (Must check before Flatpak, because Steam itself can be installed as one) + if self.locations.config.root.parent.parent.name == "steamapps": + uri = "steam://run/1118310//" + quote(args) + "/" + return f"xdg-open {uri}" + + # Flatpak RetroArch + if self.locations.config.root.is_relative_to(shared.flatpak_dir): + return f"flatpak run org.libretro.RetroArch {args}" + + # TODO executable override for non-sandboxed sources + + # Linux native RetroArch + return f"retroarch {args}" + + # TODO implement for windows (needs override) diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 26b2a84..e434d14 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -75,10 +75,12 @@ class Source(Iterable): def is_available(self): return sys.platform in self.available_on - @property @abstractmethod - def executable_format(self) -> str: - """The executable format used to construct game executables""" + def make_executable(self, *args, **kwargs) -> str: + """ + Create a game executable command. + Should be implemented by child classes. + """ def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ @@ -93,8 +95,21 @@ class Source(Iterable): return iter(self.iterable_class(self)) +class ExecutableFormatSource(Source): + """Source class that uses a simple executable format to start games""" + + @property + @abstractmethod + def executable_format(self) -> str: + """The executable format used to construct game executables""" + + def make_executable(self, *args, **kwargs) -> str: + """Use the executable format to""" + return self.executable_format.format(args, kwargs) + + # pylint: disable=abstract-method -class URLExecutableSource(Source): +class URLExecutableSource(ExecutableFormatSource): """Source class that use custom URLs to start games""" url_format: str