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
This commit is contained in:
GeoffreyCoulaud
2023-08-15 23:53:18 +02:00
parent 16d6a026e5
commit dbb6076fdc
11 changed files with 199 additions and 178 deletions

View File

@@ -90,18 +90,22 @@ class BottlesSource(URLExecutableSource):
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"' url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
available_on = {"linux"} available_on = {"linux"}
locations = BottlesLocations( locations: BottlesLocations
Location(
schema_key="bottles-location", def __init__(self) -> None:
candidates=( super().__init__()
shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", self.locations = BottlesLocations(
shared.data_dir / "bottles/", Location(
shared.home / ".local" / "share" / "bottles", schema_key="bottles-location",
), candidates=(
paths={ shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles",
"library.yml": LocationSubPath("library.yml"), shared.data_dir / "bottles/",
"data.yml": LocationSubPath("data.yml"), shared.home / ".local" / "share" / "bottles",
}, ),
invalid_subtitle=Location.DATA_INVALID_SUBTITLE, paths={
"library.yml": LocationSubPath("library.yml"),
"data.yml": LocationSubPath("data.yml"),
},
invalid_subtitle=Location.DATA_INVALID_SUBTITLE,
)
) )
)

View File

@@ -125,17 +125,21 @@ class FlatpakSource(Source):
executable_format = "flatpak run {flatpak_id}" executable_format = "flatpak run {flatpak_id}"
available_on = {"linux"} available_on = {"linux"}
locations = FlatpakLocations( locations: FlatpakLocations
Location(
schema_key="flatpak-location", def __init__(self) -> None:
candidates=( super().__init__()
"/var/lib/flatpak/", self.locations = FlatpakLocations(
shared.data_dir / "flatpak", Location(
), schema_key="flatpak-location",
paths={ candidates=(
"applications": LocationSubPath("exports/share/applications", True), "/var/lib/flatpak/",
"icons": LocationSubPath("exports/share/icons", True), shared.data_dir / "flatpak",
}, ),
invalid_subtitle=Location.DATA_INVALID_SUBTITLE, paths={
"applications": LocationSubPath("exports/share/applications", True),
"icons": LocationSubPath("exports/share/icons", True),
},
invalid_subtitle=Location.DATA_INVALID_SUBTITLE,
)
) )
)

View File

@@ -21,12 +21,12 @@
import json import json
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from functools import cached_property
from hashlib import sha256 from hashlib import sha256
from json import JSONDecodeError from json import JSONDecodeError
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import Iterable, NamedTuple, Optional, TypedDict from typing import Iterable, NamedTuple, Optional, TypedDict
from functools import cached_property
from src import shared from src import shared
from src.game import Game from src.game import Game
@@ -108,7 +108,9 @@ class SubSourceIterable(Iterable):
"game_id": self.source.game_id_format.format( "game_id": self.source.game_id_format.format(
service=self.service, game_id=app_name 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), "hidden": self.source_iterable.is_hidden(app_name),
} }
game = Game(values) game = Game(values)
@@ -363,27 +365,31 @@ class HeroicSource(URLExecutableSource):
url_format = "heroic://launch/{runner}/{app_name}" url_format = "heroic://launch/{runner}/{app_name}"
available_on = {"linux", "win32"} available_on = {"linux", "win32"}
locations = HeroicLocations( 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,
)
)
@property @property
def game_id_format(self) -> str: def game_id_format(self) -> str:
"""The string format used to construct game IDs""" """The string format used to construct game IDs"""
return self.source_id + "_{service}_{game_id}" 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,
)
)

View File

@@ -86,18 +86,22 @@ class ItchSource(URLExecutableSource):
url_format = "itch://caves/{cave_id}/launch" url_format = "itch://caves/{cave_id}/launch"
available_on = {"linux", "win32"} available_on = {"linux", "win32"}
locations = ItchLocations( locations: ItchLocations
Location(
schema_key="itch-location", def __init__(self) -> None:
candidates=( super().__init__()
shared.flatpak_dir / "io.itch.itch" / "config" / "itch", self.locations = ItchLocations(
shared.config_dir / "itch", Location(
shared.home / ".config" / "itch", schema_key="itch-location",
shared.appdata_dir / "itch", candidates=(
), shared.flatpak_dir / "io.itch.itch" / "config" / "itch",
paths={ shared.config_dir / "itch",
"butler.db": LocationSubPath("db/butler.db"), shared.home / ".config" / "itch",
}, shared.appdata_dir / "itch",
invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, ),
paths={
"butler.db": LocationSubPath("db/butler.db"),
},
invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE,
)
) )
)

View File

@@ -26,7 +26,7 @@ from typing import NamedTuple
from src import shared from src import shared
from src.game import Game from src.game import Game
from src.importer.sources.location import Location, LocationSubPath 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): class LegendarySourceIterable(SourceIterable):
@@ -100,17 +100,21 @@ class LegendarySource(Source):
available_on = {"linux"} available_on = {"linux"}
iterable_class = LegendarySourceIterable iterable_class = LegendarySourceIterable
locations = LegendaryLocations( locations: LegendaryLocations
Location(
schema_key="legendary-location", def __init__(self) -> None:
candidates=( super().__init__()
shared.config_dir / "legendary", self.locations = LegendaryLocations(
shared.home / ".config" / "legendary", Location(
), schema_key="legendary-location",
paths={ candidates=(
"installed.json": LocationSubPath("installed.json"), shared.config_dir / "legendary",
"metadata": LocationSubPath("metadata", True), shared.home / ".config" / "legendary",
}, ),
invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, paths={
"installed.json": LocationSubPath("installed.json"),
"metadata": LocationSubPath("metadata", True),
},
invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE,
)
) )
)

View File

@@ -1,7 +1,7 @@
import logging import logging
from pathlib import Path
from typing import Mapping, Iterable, NamedTuple
from os import PathLike from os import PathLike
from pathlib import Path
from typing import Iterable, Mapping, NamedTuple
from src import shared from src import shared
@@ -70,7 +70,7 @@ class Location:
def resolve(self) -> None: def resolve(self) -> None:
"""Choose a root path from the candidates for the location. """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: if self.root is not None:
return return

View File

@@ -100,33 +100,37 @@ class LutrisSource(URLExecutableSource):
# FIXME possible bug: config picks ~/.var... and cache picks ~/.local... # FIXME possible bug: config picks ~/.var... and cache picks ~/.local...
locations = LutrisLocations( 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,
),
)
@property @property
def game_id_format(self): def game_id_format(self):
return self.source_id + "_{runner}_{game_id}" 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,
),
)

View File

@@ -145,28 +145,7 @@ class RetroarchSource(Source):
available_on = {"linux"} available_on = {"linux"}
iterable_class = RetroarchSourceIterable iterable_class = RetroarchSourceIterable
locations = RetroarchLocations( 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,
)
)
@property @property
def executable_format(self): def executable_format(self):
@@ -176,15 +155,6 @@ class RetroarchSource(Source):
args = '-L "{core_path}" "{rom_path}"' args = '-L "{core_path}" "{rom_path}"'
return f"{base} {args}" 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: def get_steam_location(self) -> str:
""" """
Get the RetroArch installed via Steam location Get the RetroArch installed via Steam location
@@ -214,3 +184,37 @@ class RetroarchSource(Source):
return Path(f"{library_path}/steamapps/common/RetroArch") return Path(f"{library_path}/steamapps/common/RetroArch")
# Not found # Not found
raise ValueError("RetroArch not found in Steam library") 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)

View File

@@ -20,7 +20,7 @@
import sys import sys
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Iterable from collections.abc import Iterable
from typing import Any, Generator, Collection from typing import Any, Collection, Generator
from src.game import Game from src.game import Game
from src.importer.sources.location import Location from src.importer.sources.location import Location
@@ -56,6 +56,9 @@ class Source(Iterable):
variant: str = None variant: str = None
available_on: set[str] = set() available_on: set[str] = set()
iterable_class: type[SourceIterable] 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] locations: Collection[Location]
@property @property
@@ -85,10 +88,7 @@ class Source(Iterable):
Get an iterator for the source Get an iterator for the source
:raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable :raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable
""" """
for location_name in ("data", "cache", "config"): for location in self.locations:
location = getattr(self, f"{location_name}_location", None)
if location is None:
continue
location.resolve() location.resolve()
return iter(self.iterable_class(self)) return iter(self.iterable_class(self))

View File

@@ -120,19 +120,25 @@ class SteamSource(URLExecutableSource):
iterable_class = SteamSourceIterable iterable_class = SteamSourceIterable
url_format = "steam://rungameid/{game_id}" url_format = "steam://rungameid/{game_id}"
locations = SteamLocations( locations: SteamLocations
Location(
schema_key="steam-location", def __init__(self) -> None:
candidates=( super().__init__()
shared.home / ".steam" / "steam", self.locations = SteamLocations(
shared.data_dir / "Steam", Location(
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", schema_key="steam-location",
shared.programfiles32_dir / "Steam", candidates=(
), shared.home / ".steam" / "steam",
paths={ shared.data_dir / "Steam",
"libraryfolders.vdf": LocationSubPath("steamapps/libraryfolders.vdf"), shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
"librarycache": LocationSubPath("appcache/librarycache", True), shared.programfiles32_dir / "Steam",
}, ),
invalid_subtitle=Location.DATA_INVALID_SUBTITLE, paths={
"libraryfolders.vdf": LocationSubPath(
"steamapps/libraryfolders.vdf"
),
"librarycache": LocationSubPath("appcache/librarycache", True),
},
invalid_subtitle=Location.DATA_INVALID_SUBTITLE,
)
) )
)

View File

@@ -255,20 +255,16 @@ class PreferencesWindow(Adw.PreferencesWindow):
shared.win.get_application().quit() 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""" """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 # Get the action row to subtitle
action_row = getattr( 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: if not action_row:
continue continue
path = Path(shared.schema.get_string(location.schema_key)).expanduser()
infix = "-cache" if location == "cache" else ""
key = f"{source.source_id}{infix}-location"
path = Path(shared.schema.get_string(key)).expanduser()
# Remove the path prefix if picked via Flatpak portal # Remove the path prefix if picked via Flatpak portal
subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path)) subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path))
action_row.set_subtitle(subtitle) action_row.set_subtitle(subtitle)
@@ -338,28 +334,17 @@ class PreferencesWindow(Adw.PreferencesWindow):
return return
# Good picked location # Good picked location
location = getattr(source.locations, location_name) location = source.locations._asdict()[location_name]
if location.check_candidate(path): if location.check_candidate(path):
# Set the schema shared.schema.set_string(location.schema_key, str(path))
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
self.update_source_action_row_paths(source) self.update_source_action_row_paths(source)
if self.warning_menu_buttons.get(source.source_id): if self.warning_menu_buttons.get(source.source_id):
action_row = getattr( action_row = getattr(
self, f"{source.source_id}_{location_name}_action_row", None self, f"{source.source_id}_{location_name}_action_row", None
) )
action_row.remove(self.warning_menu_buttons[source.source_id]) action_row.remove(self.warning_menu_buttons[source.source_id])
self.warning_menu_buttons.pop(source.source_id) self.warning_menu_buttons.pop(source.source_id)
logging.debug("User-set value for %s is %s", location.schema_key, path)
logging.debug("User-set value for schema key %s: %s", key, value)
# Bad picked location, inform user # Bad picked location, inform user
else: else: