Using a named tuple to store source locations

This commit is contained in:
GeoffreyCoulaud
2023-07-26 03:53:17 +02:00
parent 0b577d2480
commit f3dcdbf0d2
9 changed files with 172 additions and 118 deletions

View File

@@ -20,6 +20,7 @@
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import NamedTuple
import yaml import yaml
@@ -35,7 +36,7 @@ class BottlesSourceIterable(SourceIterable):
def __iter__(self): def __iter__(self):
"""Generator method producing games""" """Generator method producing games"""
data = self.source.data_location["library.yml"].read_text("utf-8") data = self.source.locations.data["library.yml"].read_text("utf-8")
library: dict = yaml.safe_load(data) library: dict = yaml.safe_load(data)
added_time = int(time()) added_time = int(time())
@@ -58,11 +59,11 @@ class BottlesSourceIterable(SourceIterable):
# as Cartridges can't access directories picked via Bottles' file picker portal # as Cartridges can't access directories picked via Bottles' file picker portal
bottles_location = Path( bottles_location = Path(
yaml.safe_load( yaml.safe_load(
self.source.data_location["data.yml"].read_text("utf-8") self.source.locations.data["data.yml"].read_text("utf-8")
)["custom_bottles_path"] )["custom_bottles_path"]
) )
except (FileNotFoundError, KeyError): except (FileNotFoundError, KeyError):
bottles_location = self.source.data_location.root / "bottles" bottles_location = self.source.locations.data.root / "bottles"
bottle_path = entry["bottle"]["path"] bottle_path = entry["bottle"]["path"]
@@ -76,6 +77,10 @@ class BottlesSourceIterable(SourceIterable):
yield (game, additional_data) yield (game, additional_data)
class BottlesLocations(NamedTuple):
data: Location
class BottlesSource(URLExecutableSource): class BottlesSource(URLExecutableSource):
"""Generic Bottles source""" """Generic Bottles source"""
@@ -84,15 +89,17 @@ 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"}
data_location = Location( locations = BottlesLocations(
schema_key="bottles-location", Location(
candidates=( schema_key="bottles-location",
shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles", candidates=(
shared.data_dir / "bottles/", shared.flatpak_dir / "com.usebottles.bottles" / "data" / "bottles",
shared.home / ".local" / "share" / "bottles", shared.data_dir / "bottles/",
), shared.home / ".local" / "share" / "bottles",
paths={ ),
"library.yml": (False, "library.yml"), paths={
"data.yml": (False, "data.yml"), "library.yml": (False, "library.yml"),
}, "data.yml": (False, "data.yml"),
},
)
) )

View File

@@ -19,6 +19,7 @@
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import NamedTuple
from gi.repository import GLib, Gtk from gi.repository import GLib, Gtk
@@ -37,7 +38,7 @@ class FlatpakSourceIterable(SourceIterable):
added_time = int(time()) added_time = int(time())
icon_theme = Gtk.IconTheme.new() icon_theme = Gtk.IconTheme.new()
icon_theme.add_search_path(str(self.source.data_location["icons"])) icon_theme.add_search_path(str(self.source.locations.data["icons"]))
blacklist = ( blacklist = (
{"hu.kramo.Cartridges", "hu.kramo.Cartridges.Devel"} {"hu.kramo.Cartridges", "hu.kramo.Cartridges.Devel"}
@@ -53,7 +54,7 @@ class FlatpakSourceIterable(SourceIterable):
} }
) )
for entry in (self.source.data_location["applications"]).iterdir(): for entry in (self.source.locations.data["applications"]).iterdir():
if entry.suffix != ".desktop": if entry.suffix != ".desktop":
continue continue
@@ -111,6 +112,10 @@ class FlatpakSourceIterable(SourceIterable):
yield (game, additional_data) yield (game, additional_data)
class FlatpakLocations(NamedTuple):
data: Location
class FlatpakSource(Source): class FlatpakSource(Source):
"""Generic Flatpak source""" """Generic Flatpak source"""
@@ -119,14 +124,16 @@ class FlatpakSource(Source):
executable_format = "flatpak run {flatpak_id}" executable_format = "flatpak run {flatpak_id}"
available_on = {"linux"} available_on = {"linux"}
data_location = Location( locations = FlatpakLocations(
schema_key="flatpak-location", Location(
candidates=( schema_key="flatpak-location",
"/var/lib/flatpak/", candidates=(
shared.data_dir / "flatpak", "/var/lib/flatpak/",
), shared.data_dir / "flatpak",
paths={ ),
"applications": (True, "exports/share/applications"), paths={
"icons": (True, "exports/share/icons"), "applications": (True, "exports/share/applications"),
}, "icons": (True, "exports/share/icons"),
},
)
) )

View File

@@ -25,7 +25,7 @@ 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, Optional, TypedDict from typing import Iterable, NamedTuple, Optional, TypedDict
from functools import cached_property from functools import cached_property
from src import shared from src import shared
@@ -87,7 +87,7 @@ class SubSourceIterable(Iterable):
@cached_property @cached_property
def library_path(self) -> Path: def library_path(self) -> Path:
path = self.source.config_location.root / self.relative_library_path path = self.source.locations.config.root / self.relative_library_path
logging.debug("Using Heroic %s library.json path %s", self.name, path) logging.debug("Using Heroic %s library.json path %s", self.name, path)
return path return path
@@ -116,7 +116,7 @@ class SubSourceIterable(Iterable):
# Filenames are derived from the URL that heroic used to get the file # Filenames are derived from the URL that heroic used to get the file
uri: str = entry["art_square"] + self.image_uri_params uri: str = entry["art_square"] + self.image_uri_params
digest = sha256(uri.encode()).hexdigest() digest = sha256(uri.encode()).hexdigest()
image_path = self.source.config_location.root / "images-cache" / digest image_path = self.source.locations.config.root / "images-cache" / digest
additional_data = {"local_image_path": image_path} additional_data = {"local_image_path": image_path}
return (game, additional_data) return (game, additional_data)
@@ -159,7 +159,7 @@ class StoreSubSourceIterable(SubSourceIterable):
@cached_property @cached_property
def installed_path(self) -> Path: def installed_path(self) -> Path:
path = self.source.config_location.root / self.relative_installed_path path = self.source.locations.config.root / self.relative_installed_path
logging.debug("Using Heroic %s installed.json path %s", self.name, path) logging.debug("Using Heroic %s installed.json path %s", self.name, path)
return path return path
@@ -226,7 +226,7 @@ class LegendaryIterable(StoreSubSourceIterable):
and remove this property override. and remove this property override.
""" """
heroic_config_path = self.source.config_location.root heroic_config_path = self.source.locations.config.root
# Heroic >= 2.9 # Heroic >= 2.9
if (path := heroic_config_path / "legendaryConfig").is_dir(): if (path := heroic_config_path / "legendaryConfig").is_dir():
logging.debug("Using Heroic >= 2.9 legendary file") logging.debug("Using Heroic >= 2.9 legendary file")
@@ -308,7 +308,7 @@ class HeroicSourceIterable(SourceIterable):
""" """
try: try:
store = path_json_load(self.source.config_location["store_config.json"]) store = path_json_load(self.source.locations.config["store_config.json"])
self.hidden_app_names = { self.hidden_app_names = {
app_name app_name
for game in store["games"]["hidden"] for game in store["games"]["hidden"]
@@ -349,6 +349,10 @@ class HeroicSourceIterable(SourceIterable):
continue continue
class HeroicLocations(NamedTuple):
config: Location
class HeroicSource(URLExecutableSource): class HeroicSource(URLExecutableSource):
"""Generic Heroic Games Launcher source""" """Generic Heroic Games Launcher source"""
@@ -357,18 +361,23 @@ class HeroicSource(URLExecutableSource):
url_format = "heroic://launch/{app_name}" url_format = "heroic://launch/{app_name}"
available_on = {"linux", "win32"} available_on = {"linux", "win32"}
config_location = Location( locations = HeroicLocations(
schema_key="heroic-location", Location(
candidates=( schema_key="heroic-location",
shared.config_dir / "heroic", candidates=(
shared.home / ".config" / "heroic", shared.config_dir / "heroic",
shared.flatpak_dir / "com.heroicgameslauncher.hgl" / "config" / "heroic", shared.home / ".config" / "heroic",
shared.appdata_dir / "heroic", shared.flatpak_dir
), / "com.heroicgameslauncher.hgl"
paths={ / "config"
"config.json": (False, "config.json"), / "heroic",
"store_config.json": (False, Path("store") / "config.json"), shared.appdata_dir / "heroic",
}, ),
paths={
"config.json": (False, "config.json"),
"store_config.json": (False, Path("store") / "config.json"),
},
)
) )
@property @property

View File

@@ -21,6 +21,7 @@
from shutil import rmtree from shutil import rmtree
from sqlite3 import connect from sqlite3 import connect
from time import time from time import time
from typing import NamedTuple
from src import shared from src import shared
from src.game import Game from src.game import Game
@@ -51,7 +52,7 @@ class ItchSourceIterable(SourceIterable):
caves.game_id = games.id caves.game_id = games.id
; ;
""" """
db_path = copy_db(self.source.config_location["butler.db"]) db_path = copy_db(self.source.locations.config["butler.db"])
connection = connect(db_path) connection = connect(db_path)
cursor = connection.execute(db_request) cursor = connection.execute(db_request)
@@ -74,19 +75,25 @@ class ItchSourceIterable(SourceIterable):
rmtree(str(db_path.parent)) rmtree(str(db_path.parent))
class ItchLocations(NamedTuple):
config: Location
class ItchSource(URLExecutableSource): class ItchSource(URLExecutableSource):
name = _("itch") name = _("itch")
iterable_class = ItchSourceIterable iterable_class = ItchSourceIterable
url_format = "itch://caves/{cave_id}/launch" url_format = "itch://caves/{cave_id}/launch"
available_on = {"linux", "win32"} available_on = {"linux", "win32"}
config_location = Location( locations = ItchLocations(
schema_key="itch-location", Location(
candidates=( schema_key="itch-location",
shared.flatpak_dir / "io.itch.itch" / "config" / "itch", candidates=(
shared.config_dir / "itch", shared.flatpak_dir / "io.itch.itch" / "config" / "itch",
shared.home / ".config" / "itch", shared.config_dir / "itch",
shared.appdata_dir / "itch", shared.home / ".config" / "itch",
), shared.appdata_dir / "itch",
paths={"butler.db": (False, "db/butler.db")}, ),
paths={"butler.db": (False, "db/butler.db")},
)
) )

View File

@@ -21,6 +21,7 @@ import json
import logging import logging
from json import JSONDecodeError from json import JSONDecodeError
from time import time from time import time
from typing import NamedTuple
from src import shared from src import shared
from src.game import Game from src.game import Game
@@ -50,7 +51,7 @@ class LegendarySourceIterable(SourceIterable):
data = {} data = {}
# Get additional metadata from file (optional) # Get additional metadata from file (optional)
metadata_file = self.source.config_location["metadata"] / f"{app_name}.json" metadata_file = self.source.locations.config["metadata"] / f"{app_name}.json"
try: try:
metadata = json.load(metadata_file.open()) metadata = json.load(metadata_file.open())
values["developer"] = metadata["metadata"]["developer"] values["developer"] = metadata["metadata"]["developer"]
@@ -66,7 +67,7 @@ class LegendarySourceIterable(SourceIterable):
def __iter__(self): def __iter__(self):
# Open library # Open library
file = self.source.config_location["installed.json"] file = self.source.locations.config["installed.json"]
try: try:
library: dict = json.load(file.open()) library: dict = json.load(file.open())
except (JSONDecodeError, OSError): except (JSONDecodeError, OSError):
@@ -88,20 +89,26 @@ class LegendarySourceIterable(SourceIterable):
yield result yield result
class LegendaryLocations(NamedTuple):
config: Location
class LegendarySource(Source): class LegendarySource(Source):
name = _("Legendary") name = _("Legendary")
executable_format = "legendary launch {app_name}" executable_format = "legendary launch {app_name}"
available_on = {"linux"} available_on = {"linux"}
iterable_class = LegendarySourceIterable iterable_class = LegendarySourceIterable
config_location: Location = Location(
schema_key="legendary-location", locations = LegendaryLocations(
candidates=( Location(
shared.config_dir / "legendary", schema_key="legendary-location",
shared.home / ".config" / "legendary", candidates=(
), shared.config_dir / "legendary",
paths={ shared.home / ".config" / "legendary",
"installed.json": (False, "installed.json"), ),
"metadata": (True, "metadata"), paths={
}, "installed.json": (False, "installed.json"),
"metadata": (True, "metadata"),
},
)
) )

View File

@@ -20,6 +20,7 @@
from shutil import rmtree from shutil import rmtree
from sqlite3 import connect from sqlite3 import connect
from time import time from time import time
from typing import NamedTuple
from src import shared from src import shared
from src.game import Game from src.game import Game
@@ -51,7 +52,7 @@ class LutrisSourceIterable(SourceIterable):
"import_steam": shared.schema.get_boolean("lutris-import-steam"), "import_steam": shared.schema.get_boolean("lutris-import-steam"),
"import_flatpak": shared.schema.get_boolean("lutris-import-flatpak"), "import_flatpak": shared.schema.get_boolean("lutris-import-flatpak"),
} }
db_path = copy_db(self.source.data_location["pga.db"]) db_path = copy_db(self.source.locations.config["pga.db"])
connection = connect(db_path) connection = connect(db_path)
cursor = connection.execute(request, params) cursor = connection.execute(request, params)
@@ -73,7 +74,7 @@ class LutrisSourceIterable(SourceIterable):
game = Game(values) game = Game(values)
# Get official image path # Get official image path
image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg" image_path = self.source.locations.cache["coverart"] / f"{row[2]}.jpg"
additional_data = {"local_image_path": image_path} additional_data = {"local_image_path": image_path}
# Produce game # Produce game
@@ -83,6 +84,11 @@ class LutrisSourceIterable(SourceIterable):
rmtree(str(db_path.parent)) rmtree(str(db_path.parent))
class LutrisLocations(NamedTuple):
config: Location
cache: Location
class LutrisSource(URLExecutableSource): class LutrisSource(URLExecutableSource):
"""Generic Lutris source""" """Generic Lutris source"""
@@ -91,30 +97,31 @@ class LutrisSource(URLExecutableSource):
url_format = "lutris:rungameid/{game_id}" url_format = "lutris:rungameid/{game_id}"
available_on = {"linux"} available_on = {"linux"}
# FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local... # FIXME possible bug: config picks ~/.var... and cache picks ~/.local...
data_location = Location( locations = LutrisLocations(
schema_key="lutris-location", Location(
candidates=( schema_key="lutris-location",
shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris", candidates=(
shared.data_dir / "lutris", shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris",
shared.home / ".local" / "share" / "lutris", shared.data_dir / "lutris",
shared.home / ".local" / "share" / "lutris",
),
paths={
"pga.db": (False, "pga.db"),
},
), ),
paths={ Location(
"pga.db": (False, "pga.db"), schema_key="lutris-cache-location",
}, candidates=(
) shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris",
shared.cache_dir / "lutris",
cache_location = Location( shared.home / ".cache" / "lutris",
schema_key="lutris-cache-location", ),
candidates=( paths={
shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris", "coverart": (True, "coverart"),
shared.cache_dir / "lutris", },
shared.home / ".cache" / "lutris",
), ),
paths={
"coverart": (True, "coverart"),
},
) )
@property @property

View File

@@ -19,8 +19,8 @@
import sys import sys
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Iterable, Iterator from collections.abc import Iterable
from typing import Any, Generator, Optional from typing import Any, Generator, Optional, Collection
from src.game import Game from src.game import Game
from src.importer.sources.location import Location from src.importer.sources.location import Location
@@ -54,10 +54,8 @@ class Source(Iterable):
name: str name: str
variant: str = None variant: str = None
available_on: set[str] = set() available_on: set[str] = set()
data_location: Optional[Location] = None
cache_location: Optional[Location] = None
config_location: Optional[Location] = None
iterable_class: type[SourceIterable] iterable_class: type[SourceIterable]
locations: Collection[Location]
@property @property
def full_name(self) -> str: def full_name(self) -> str:

View File

@@ -22,7 +22,7 @@ import logging
import re import re
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import Iterable from typing import Iterable, NamedTuple
from src import shared from src import shared
from src.game import Game from src.game import Game
@@ -36,7 +36,7 @@ class SteamSourceIterable(SourceIterable):
def get_manifest_dirs(self) -> Iterable[Path]: def get_manifest_dirs(self) -> Iterable[Path]:
"""Get dirs that contain steam app manifests""" """Get dirs that contain steam app manifests"""
libraryfolders_path = self.source.data_location["libraryfolders.vdf"] libraryfolders_path = self.source.locations.data["libraryfolders.vdf"]
with open(libraryfolders_path, "r", encoding="utf-8") as file: with open(libraryfolders_path, "r", encoding="utf-8") as file:
contents = file.read() contents = file.read()
return [ return [
@@ -100,7 +100,7 @@ class SteamSourceIterable(SourceIterable):
# Add official cover image # Add official cover image
image_path = ( image_path = (
self.source.data_location["librarycache"] self.source.locations.data["librarycache"]
/ f"{appid}_library_600x900.jpg" / f"{appid}_library_600x900.jpg"
) )
additional_data = {"local_image_path": image_path, "steam_appid": appid} additional_data = {"local_image_path": image_path, "steam_appid": appid}
@@ -109,22 +109,28 @@ class SteamSourceIterable(SourceIterable):
yield (game, additional_data) yield (game, additional_data)
class SteamLocations(NamedTuple):
data: Location
class SteamSource(URLExecutableSource): class SteamSource(URLExecutableSource):
name = _("Steam") name = _("Steam")
available_on = {"linux", "win32"} available_on = {"linux", "win32"}
iterable_class = SteamSourceIterable iterable_class = SteamSourceIterable
url_format = "steam://rungameid/{game_id}" url_format = "steam://rungameid/{game_id}"
data_location = Location( locations = SteamLocations(
schema_key="steam-location", Location(
candidates=( schema_key="steam-location",
shared.home / ".steam" / "steam", candidates=(
shared.data_dir / "Steam", shared.home / ".steam" / "steam",
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam", shared.data_dir / "Steam",
shared.programfiles32_dir / "Steam", shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
), shared.programfiles32_dir / "Steam",
paths={ ),
"libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"), paths={
"librarycache": (True, "appcache/librarycache"), "libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"),
}, "librarycache": (True, "appcache/librarycache"),
},
)
) )

View File

@@ -262,19 +262,19 @@ class PreferencesWindow(Adw.PreferencesWindow):
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)
def resolve_locations(self, source): def resolve_locations(self, source: Source):
"""Resolve locations and add a warning if location cannot be found""" """Resolve locations and add a warning if location cannot be found"""
def clear_warning_selection(_widget, label): def clear_warning_selection(_widget, label):
label.select_region(-1, -1) label.select_region(-1, -1)
for location_name in ("data", "config", "cache"): for location_name, location in source.locations._asdict().items():
action_row = getattr(self, f"{source.id}_{location_name}_action_row", None) action_row = getattr(self, f"{source.id}_{location_name}_action_row", None)
if not action_row: if not action_row:
continue continue
try: try:
getattr(source, f"{location_name}_location", None).resolve() location.resolve()
except UnresolvableLocationError: except UnresolvableLocationError:
popover = Gtk.Popover( popover = Gtk.Popover(
@@ -325,10 +325,14 @@ class PreferencesWindow(Adw.PreferencesWindow):
return return
# Good picked location # Good picked location
location = getattr(source, f"{location_name}_location") location = getattr(source.locations, location_name)
if location.check_candidate(path): if location.check_candidate(path):
# Set the schema # Set the schema
infix = "-cache" if location_name == "cache" else "" match location_name:
case "config" | "data":
infix = ""
case _:
infix = f"-{location_name}"
key = f"{source.id}{infix}-location" key = f"{source.id}{infix}-location"
value = str(path) value = str(path)
shared.schema.set_string(key, value) shared.schema.set_string(key, value)
@@ -381,10 +385,12 @@ class PreferencesWindow(Adw.PreferencesWindow):
) )
# Connect dir picker buttons # Connect dir picker buttons
for location in ("data", "config", "cache"): for location_name in source.locations._asdict():
button = getattr(self, f"{source.id}_{location}_file_chooser_button", None) button = getattr(
self, f"{source.id}_{location_name}_file_chooser_button", None
)
if button is not None: if button is not None:
button.connect("clicked", self.choose_folder, set_dir, location) button.connect("clicked", self.choose_folder, set_dir, location_name)
# Set the source row subtitles # Set the source row subtitles
self.resolve_locations(source) self.resolve_locations(source)