From dbb6076fdce0a1561fbc30922f6e84e8b11efc31 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 15 Aug 2023 23:53:18 +0200 Subject: [PATCH] Various fixes for locations - Simplified some preferences code - Added type hints to locations - Made locations not shared between source instances (source of a bug) - Updated source iter to resolve locations correctly --- src/importer/sources/bottles_source.py | 32 +++++++----- src/importer/sources/flatpak_source.py | 30 ++++++----- src/importer/sources/heroic_source.py | 48 +++++++++-------- src/importer/sources/itch_source.py | 32 +++++++----- src/importer/sources/legendary_source.py | 32 +++++++----- src/importer/sources/location.py | 6 +-- src/importer/sources/lutris_source.py | 56 ++++++++++---------- src/importer/sources/retroarch_source.py | 66 +++++++++++++----------- src/importer/sources/source.py | 10 ++-- src/importer/sources/steam_source.py | 36 +++++++------ src/preferences.py | 29 +++-------- 11 files changed, 199 insertions(+), 178 deletions(-) diff --git a/src/importer/sources/bottles_source.py b/src/importer/sources/bottles_source.py index 041247b..d85529c 100644 --- a/src/importer/sources/bottles_source.py +++ b/src/importer/sources/bottles_source.py @@ -90,18 +90,22 @@ class BottlesSource(URLExecutableSource): url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' available_on = {"linux"} - locations = BottlesLocations( - Location( - schema_key="bottles-location", - candidates=( - shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", - shared.data_dir / "bottles/", - shared.home / ".local" / "share" / "bottles", - ), - paths={ - "library.yml": LocationSubPath("library.yml"), - "data.yml": LocationSubPath("data.yml"), - }, - invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + locations: BottlesLocations + + def __init__(self) -> None: + super().__init__() + self.locations = BottlesLocations( + Location( + schema_key="bottles-location", + candidates=( + shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", + shared.data_dir / "bottles/", + shared.home / ".local" / "share" / "bottles", + ), + paths={ + "library.yml": LocationSubPath("library.yml"), + "data.yml": LocationSubPath("data.yml"), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ) ) - ) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index ee4ebbe..6bb982e 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -125,17 +125,21 @@ class FlatpakSource(Source): executable_format = "flatpak run {flatpak_id}" available_on = {"linux"} - locations = FlatpakLocations( - Location( - schema_key="flatpak-location", - candidates=( - "/var/lib/flatpak/", - shared.data_dir / "flatpak", - ), - paths={ - "applications": LocationSubPath("exports/share/applications", True), - "icons": LocationSubPath("exports/share/icons", True), - }, - invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + locations: FlatpakLocations + + def __init__(self) -> None: + super().__init__() + self.locations = FlatpakLocations( + Location( + schema_key="flatpak-location", + candidates=( + "/var/lib/flatpak/", + shared.data_dir / "flatpak", + ), + paths={ + "applications": LocationSubPath("exports/share/applications", True), + "icons": LocationSubPath("exports/share/icons", True), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ) ) - ) diff --git a/src/importer/sources/heroic_source.py b/src/importer/sources/heroic_source.py index c06a266..42dc446 100644 --- a/src/importer/sources/heroic_source.py +++ b/src/importer/sources/heroic_source.py @@ -21,12 +21,12 @@ import json import logging from abc import abstractmethod +from functools import cached_property from hashlib import sha256 from json import JSONDecodeError from pathlib import Path from time import time from typing import Iterable, NamedTuple, Optional, TypedDict -from functools import cached_property from src import shared from src.game import Game @@ -108,7 +108,9 @@ class SubSourceIterable(Iterable): "game_id": self.source.game_id_format.format( service=self.service, game_id=app_name ), - "executable": self.source.executable_format.format(runner=runner, app_name=app_name), + "executable": self.source.executable_format.format( + runner=runner, app_name=app_name + ), "hidden": self.source_iterable.is_hidden(app_name), } game = Game(values) @@ -363,27 +365,31 @@ class HeroicSource(URLExecutableSource): url_format = "heroic://launch/{runner}/{app_name}" available_on = {"linux", "win32"} - locations = HeroicLocations( - Location( - schema_key="heroic-location", - candidates=( - shared.config_dir / "heroic", - shared.home / ".config" / "heroic", - shared.flatpak_dir - / "com.heroicgameslauncher.hgl" - / "config" - / "heroic", - shared.appdata_dir / "heroic", - ), - paths={ - "config.json": LocationSubPath("config.json"), - "store_config.json": LocationSubPath("store/config.json"), - }, - invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, - ) - ) + locations: HeroicLocations @property def game_id_format(self) -> str: """The string format used to construct game IDs""" return self.source_id + "_{service}_{game_id}" + + def __init__(self) -> None: + super().__init__() + self.locations = HeroicLocations( + Location( + schema_key="heroic-location", + candidates=( + shared.config_dir / "heroic", + shared.home / ".config" / "heroic", + shared.flatpak_dir + / "com.heroicgameslauncher.hgl" + / "config" + / "heroic", + shared.appdata_dir / "heroic", + ), + paths={ + "config.json": LocationSubPath("config.json"), + "store_config.json": LocationSubPath("store/config.json"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) + ) diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index 36e02e0..e39d090 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -86,18 +86,22 @@ class ItchSource(URLExecutableSource): url_format = "itch://caves/{cave_id}/launch" available_on = {"linux", "win32"} - locations = ItchLocations( - Location( - schema_key="itch-location", - candidates=( - shared.flatpak_dir / "io.itch.itch" / "config" / "itch", - shared.config_dir / "itch", - shared.home / ".config" / "itch", - shared.appdata_dir / "itch", - ), - paths={ - "butler.db": LocationSubPath("db/butler.db"), - }, - invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + locations: ItchLocations + + def __init__(self) -> None: + super().__init__() + self.locations = ItchLocations( + Location( + schema_key="itch-location", + candidates=( + shared.flatpak_dir / "io.itch.itch" / "config" / "itch", + shared.config_dir / "itch", + shared.home / ".config" / "itch", + shared.appdata_dir / "itch", + ), + paths={ + "butler.db": LocationSubPath("db/butler.db"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) ) - ) diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 03bbdd2..9fdb611 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -26,7 +26,7 @@ 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 Source, SourceIterable, SourceIterationResult class LegendarySourceIterable(SourceIterable): @@ -100,17 +100,21 @@ class LegendarySource(Source): available_on = {"linux"} iterable_class = LegendarySourceIterable - locations = LegendaryLocations( - Location( - schema_key="legendary-location", - candidates=( - shared.config_dir / "legendary", - shared.home / ".config" / "legendary", - ), - paths={ - "installed.json": LocationSubPath("installed.json"), - "metadata": LocationSubPath("metadata", True), - }, - invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + locations: LegendaryLocations + + def __init__(self) -> None: + super().__init__() + self.locations = LegendaryLocations( + Location( + schema_key="legendary-location", + candidates=( + shared.config_dir / "legendary", + shared.home / ".config" / "legendary", + ), + paths={ + "installed.json": LocationSubPath("installed.json"), + "metadata": LocationSubPath("metadata", True), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) ) - ) diff --git a/src/importer/sources/location.py b/src/importer/sources/location.py index 55684b4..dde8e3f 100644 --- a/src/importer/sources/location.py +++ b/src/importer/sources/location.py @@ -1,7 +1,7 @@ import logging -from pathlib import Path -from typing import Mapping, Iterable, NamedTuple from os import PathLike +from pathlib import Path +from typing import Iterable, Mapping, NamedTuple from src import shared @@ -70,7 +70,7 @@ class Location: def resolve(self) -> None: """Choose a root path from the candidates for the location. - If none fits, raise a UnresolvableLocationError""" + If none fits, raise an UnresolvableLocationError""" if self.root is not None: return diff --git a/src/importer/sources/lutris_source.py b/src/importer/sources/lutris_source.py index 023d3be..dd5defd 100644 --- a/src/importer/sources/lutris_source.py +++ b/src/importer/sources/lutris_source.py @@ -100,33 +100,37 @@ class LutrisSource(URLExecutableSource): # FIXME possible bug: config picks ~/.var... and cache picks ~/.local... - locations = LutrisLocations( - Location( - schema_key="lutris-location", - candidates=( - shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", - shared.data_dir / "lutris", - shared.home / ".local" / "share" / "lutris", - ), - paths={ - "pga.db": LocationSubPath("pga.db"), - }, - invalid_subtitle=Location.DATA_INVALID_SUBTITLE, - ), - Location( - schema_key="lutris-cache-location", - candidates=( - shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", - shared.cache_dir / "lutris", - shared.home / ".cache" / "lutris", - ), - paths={ - "coverart": LocationSubPath("coverart", True), - }, - invalid_subtitle=Location.CACHE_INVALID_SUBTITLE, - ), - ) + locations: LutrisLocations @property def game_id_format(self): return self.source_id + "_{runner}_{game_id}" + + def __init__(self) -> None: + super().__init__() + self.locations = LutrisLocations( + Location( + schema_key="lutris-location", + candidates=( + shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", + shared.data_dir / "lutris", + shared.home / ".local" / "share" / "lutris", + ), + paths={ + "pga.db": LocationSubPath("pga.db"), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ), + Location( + schema_key="lutris-cache-location", + candidates=( + shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", + shared.cache_dir / "lutris", + shared.home / ".cache" / "lutris", + ), + paths={ + "coverart": LocationSubPath("coverart", True), + }, + invalid_subtitle=Location.CACHE_INVALID_SUBTITLE, + ), + ) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 96b16e1..d69970b 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -145,28 +145,7 @@ class RetroarchSource(Source): available_on = {"linux"} iterable_class = RetroarchSourceIterable - locations = RetroarchLocations( - Location( - schema_key="retroarch-location", - candidates=[ - shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", - shared.config_dir / "retroarch", - shared.home / ".config" / "retroarch", - # TODO: Windows support, waiting for executable path setting improvement - # Path("C:\\RetroArch-Win64"), - # Path("C:\\RetroArch-Win32"), - # TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563) - # shared.local_appdata_dir - # / "Packages" - # / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" - # / "LocalState", - ], - paths={ - "retroarch.cfg": LocationSubPath("retroarch.cfg"), - }, - invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, - ) - ) + locations: RetroarchLocations @property def executable_format(self): @@ -176,15 +155,6 @@ class RetroarchSource(Source): args = '-L "{core_path}" "{rom_path}"' return f"{base} {args}" - def __init__(self) -> None: - super().__init__() - try: - self.locations.config.candidates.append(self.get_steam_location()) - except (OSError, KeyError, UnresolvableLocationError): - logging.debug("Steam isn't installed") - except ValueError as error: - logging.debug("RetroArch Steam location candiate not found", exc_info=error) - def get_steam_location(self) -> str: """ Get the RetroArch installed via Steam location @@ -214,3 +184,37 @@ class RetroarchSource(Source): return Path(f"{library_path}/steamapps/common/RetroArch") # Not found raise ValueError("RetroArch not found in Steam library") + + def __init__(self) -> None: + super().__init__() + self.locations = RetroarchLocations( + Location( + schema_key="retroarch-location", + candidates=[ + shared.flatpak_dir + / "org.libretro.RetroArch" + / "config" + / "retroarch", + shared.config_dir / "retroarch", + shared.home / ".config" / "retroarch", + # TODO: Windows support, waiting for executable path setting improvement + # Path("C:\\RetroArch-Win64"), + # Path("C:\\RetroArch-Win32"), + # TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563) + # shared.local_appdata_dir + # / "Packages" + # / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" + # / "LocalState", + ], + paths={ + "retroarch.cfg": LocationSubPath("retroarch.cfg"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) + ) + try: + self.locations.config.candidates.append(self.get_steam_location()) + except (OSError, KeyError, UnresolvableLocationError): + logging.debug("Steam isn't installed") + except ValueError as error: + logging.debug("RetroArch Steam location candiate not found", exc_info=error) diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 26b2a84..499881a 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -20,7 +20,7 @@ import sys from abc import abstractmethod from collections.abc import Iterable -from typing import Any, Generator, Collection +from typing import Any, Collection, Generator from src.game import Game from src.importer.sources.location import Location @@ -56,6 +56,9 @@ class Source(Iterable): variant: str = None available_on: set[str] = set() iterable_class: type[SourceIterable] + + # NOTE: Locations must be set at __init__ time, not in the class definition. + # They must not be shared between source instances. locations: Collection[Location] @property @@ -85,10 +88,7 @@ class Source(Iterable): Get an iterator for the source :raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable """ - for location_name in ("data", "cache", "config"): - location = getattr(self, f"{location_name}_location", None) - if location is None: - continue + for location in self.locations: location.resolve() return iter(self.iterable_class(self)) diff --git a/src/importer/sources/steam_source.py b/src/importer/sources/steam_source.py index 904fb0b..90460bd 100644 --- a/src/importer/sources/steam_source.py +++ b/src/importer/sources/steam_source.py @@ -120,19 +120,25 @@ class SteamSource(URLExecutableSource): iterable_class = SteamSourceIterable url_format = "steam://rungameid/{game_id}" - locations = SteamLocations( - Location( - schema_key="steam-location", - candidates=( - shared.home / ".steam" / "steam", - shared.data_dir / "Steam", - shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", - shared.programfiles32_dir / "Steam", - ), - paths={ - "libraryfolders.vdf": LocationSubPath("steamapps/libraryfolders.vdf"), - "librarycache": LocationSubPath("appcache/librarycache", True), - }, - invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + locations: SteamLocations + + def __init__(self) -> None: + super().__init__() + self.locations = SteamLocations( + Location( + schema_key="steam-location", + candidates=( + shared.home / ".steam" / "steam", + shared.data_dir / "Steam", + shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", + shared.programfiles32_dir / "Steam", + ), + paths={ + "libraryfolders.vdf": LocationSubPath( + "steamapps/libraryfolders.vdf" + ), + "librarycache": LocationSubPath("appcache/librarycache", True), + }, + invalid_subtitle=Location.DATA_INVALID_SUBTITLE, + ) ) - ) diff --git a/src/preferences.py b/src/preferences.py index a3172ac..6b3d766 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -255,20 +255,16 @@ class PreferencesWindow(Adw.PreferencesWindow): shared.win.get_application().quit() - def update_source_action_row_paths(self, source): + def update_source_action_row_paths(self, source: Source): """Set the dir subtitle for a source's action rows""" - for location in ("data", "config", "cache"): + for location_name, location in source.locations._asdict().items(): # Get the action row to subtitle action_row = getattr( - self, f"{source.source_id}_{location}_action_row", None + self, f"{source.source_id}_{location_name}_action_row", None ) if not action_row: continue - - infix = "-cache" if location == "cache" else "" - key = f"{source.source_id}{infix}-location" - path = Path(shared.schema.get_string(key)).expanduser() - + path = Path(shared.schema.get_string(location.schema_key)).expanduser() # Remove the path prefix if picked via Flatpak portal subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path)) action_row.set_subtitle(subtitle) @@ -338,28 +334,17 @@ class PreferencesWindow(Adw.PreferencesWindow): return # Good picked location - location = getattr(source.locations, location_name) + location = source.locations._asdict()[location_name] if location.check_candidate(path): - # Set the schema - match location_name: - case "config" | "data": - infix = "" - case _: - infix = f"-{location_name}" - key = f"{source.source_id}{infix}-location" - value = str(path) - shared.schema.set_string(key, value) - # Update the row + shared.schema.set_string(location.schema_key, str(path)) self.update_source_action_row_paths(source) - if self.warning_menu_buttons.get(source.source_id): action_row = getattr( self, f"{source.source_id}_{location_name}_action_row", None ) action_row.remove(self.warning_menu_buttons[source.source_id]) self.warning_menu_buttons.pop(source.source_id) - - logging.debug("User-set value for schema key %s: %s", key, value) + logging.debug("User-set value for %s is %s", location.schema_key, path) # Bad picked location, inform user else: