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 time import time
from typing import NamedTuple
import yaml
@@ -35,7 +36,7 @@ class BottlesSourceIterable(SourceIterable):
def __iter__(self):
"""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)
added_time = int(time())
@@ -58,11 +59,11 @@ class BottlesSourceIterable(SourceIterable):
# as Cartridges can't access directories picked via Bottles' file picker portal
bottles_location = Path(
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"]
)
except (FileNotFoundError, KeyError):
bottles_location = self.source.data_location.root / "bottles"
bottles_location = self.source.locations.data.root / "bottles"
bottle_path = entry["bottle"]["path"]
@@ -76,6 +77,10 @@ class BottlesSourceIterable(SourceIterable):
yield (game, additional_data)
class BottlesLocations(NamedTuple):
data: Location
class BottlesSource(URLExecutableSource):
"""Generic Bottles source"""
@@ -84,15 +89,17 @@ class BottlesSource(URLExecutableSource):
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
available_on = {"linux"}
data_location = 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": (False, "library.yml"),
"data.yml": (False, "data.yml"),
},
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": (False, "library.yml"),
"data.yml": (False, "data.yml"),
},
)
)

View File

@@ -19,6 +19,7 @@
from pathlib import Path
from time import time
from typing import NamedTuple
from gi.repository import GLib, Gtk
@@ -37,7 +38,7 @@ class FlatpakSourceIterable(SourceIterable):
added_time = int(time())
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 = (
{"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":
continue
@@ -111,6 +112,10 @@ class FlatpakSourceIterable(SourceIterable):
yield (game, additional_data)
class FlatpakLocations(NamedTuple):
data: Location
class FlatpakSource(Source):
"""Generic Flatpak source"""
@@ -119,14 +124,16 @@ class FlatpakSource(Source):
executable_format = "flatpak run {flatpak_id}"
available_on = {"linux"}
data_location = Location(
schema_key="flatpak-location",
candidates=(
"/var/lib/flatpak/",
shared.data_dir / "flatpak",
),
paths={
"applications": (True, "exports/share/applications"),
"icons": (True, "exports/share/icons"),
},
locations = FlatpakLocations(
Location(
schema_key="flatpak-location",
candidates=(
"/var/lib/flatpak/",
shared.data_dir / "flatpak",
),
paths={
"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 pathlib import Path
from time import time
from typing import Iterable, Optional, TypedDict
from typing import Iterable, NamedTuple, Optional, TypedDict
from functools import cached_property
from src import shared
@@ -87,7 +87,7 @@ class SubSourceIterable(Iterable):
@cached_property
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)
return path
@@ -116,7 +116,7 @@ class SubSourceIterable(Iterable):
# Filenames are derived from the URL that heroic used to get the file
uri: str = entry["art_square"] + self.image_uri_params
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}
return (game, additional_data)
@@ -159,7 +159,7 @@ class StoreSubSourceIterable(SubSourceIterable):
@cached_property
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)
return path
@@ -226,7 +226,7 @@ class LegendaryIterable(StoreSubSourceIterable):
and remove this property override.
"""
heroic_config_path = self.source.config_location.root
heroic_config_path = self.source.locations.config.root
# Heroic >= 2.9
if (path := heroic_config_path / "legendaryConfig").is_dir():
logging.debug("Using Heroic >= 2.9 legendary file")
@@ -308,7 +308,7 @@ class HeroicSourceIterable(SourceIterable):
"""
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 = {
app_name
for game in store["games"]["hidden"]
@@ -349,6 +349,10 @@ class HeroicSourceIterable(SourceIterable):
continue
class HeroicLocations(NamedTuple):
config: Location
class HeroicSource(URLExecutableSource):
"""Generic Heroic Games Launcher source"""
@@ -357,18 +361,23 @@ class HeroicSource(URLExecutableSource):
url_format = "heroic://launch/{app_name}"
available_on = {"linux", "win32"}
config_location = 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": (False, "config.json"),
"store_config.json": (False, Path("store") / "config.json"),
},
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": (False, "config.json"),
"store_config.json": (False, Path("store") / "config.json"),
},
)
)
@property

View File

@@ -21,6 +21,7 @@
from shutil import rmtree
from sqlite3 import connect
from time import time
from typing import NamedTuple
from src import shared
from src.game import Game
@@ -51,7 +52,7 @@ class ItchSourceIterable(SourceIterable):
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)
cursor = connection.execute(db_request)
@@ -74,19 +75,25 @@ class ItchSourceIterable(SourceIterable):
rmtree(str(db_path.parent))
class ItchLocations(NamedTuple):
config: Location
class ItchSource(URLExecutableSource):
name = _("itch")
iterable_class = ItchSourceIterable
url_format = "itch://caves/{cave_id}/launch"
available_on = {"linux", "win32"}
config_location = 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": (False, "db/butler.db")},
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": (False, "db/butler.db")},
)
)

View File

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

View File

@@ -20,6 +20,7 @@
from shutil import rmtree
from sqlite3 import connect
from time import time
from typing import NamedTuple
from src import shared
from src.game import Game
@@ -51,7 +52,7 @@ class LutrisSourceIterable(SourceIterable):
"import_steam": shared.schema.get_boolean("lutris-import-steam"),
"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)
cursor = connection.execute(request, params)
@@ -73,7 +74,7 @@ class LutrisSourceIterable(SourceIterable):
game = Game(values)
# 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}
# Produce game
@@ -83,6 +84,11 @@ class LutrisSourceIterable(SourceIterable):
rmtree(str(db_path.parent))
class LutrisLocations(NamedTuple):
config: Location
cache: Location
class LutrisSource(URLExecutableSource):
"""Generic Lutris source"""
@@ -91,30 +97,31 @@ class LutrisSource(URLExecutableSource):
url_format = "lutris:rungameid/{game_id}"
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(
schema_key="lutris-location",
candidates=(
shared.flatpak_dir / "net.lutris.Lutris" / "data" / "lutris",
shared.data_dir / "lutris",
shared.home / ".local" / "share" / "lutris",
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": (False, "pga.db"),
},
),
paths={
"pga.db": (False, "pga.db"),
},
)
cache_location = Location(
schema_key="lutris-cache-location",
candidates=(
shared.flatpak_dir / "net.lutris.Lutris" / "cache" / "lutris",
shared.cache_dir / "lutris",
shared.home / ".cache" / "lutris",
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": (True, "coverart"),
},
),
paths={
"coverart": (True, "coverart"),
},
)
@property

View File

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

View File

@@ -22,7 +22,7 @@ import logging
import re
from pathlib import Path
from time import time
from typing import Iterable
from typing import Iterable, NamedTuple
from src import shared
from src.game import Game
@@ -36,7 +36,7 @@ class SteamSourceIterable(SourceIterable):
def get_manifest_dirs(self) -> Iterable[Path]:
"""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:
contents = file.read()
return [
@@ -100,7 +100,7 @@ class SteamSourceIterable(SourceIterable):
# Add official cover image
image_path = (
self.source.data_location["librarycache"]
self.source.locations.data["librarycache"]
/ f"{appid}_library_600x900.jpg"
)
additional_data = {"local_image_path": image_path, "steam_appid": appid}
@@ -109,22 +109,28 @@ class SteamSourceIterable(SourceIterable):
yield (game, additional_data)
class SteamLocations(NamedTuple):
data: Location
class SteamSource(URLExecutableSource):
name = _("Steam")
available_on = {"linux", "win32"}
iterable_class = SteamSourceIterable
url_format = "steam://rungameid/{game_id}"
data_location = 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": (False, "steamapps/libraryfolders.vdf"),
"librarycache": (True, "appcache/librarycache"),
},
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": (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))
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"""
def clear_warning_selection(_widget, label):
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)
if not action_row:
continue
try:
getattr(source, f"{location_name}_location", None).resolve()
location.resolve()
except UnresolvableLocationError:
popover = Gtk.Popover(
@@ -325,10 +325,14 @@ class PreferencesWindow(Adw.PreferencesWindow):
return
# Good picked location
location = getattr(source, f"{location_name}_location")
location = getattr(source.locations, location_name)
if location.check_candidate(path):
# 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"
value = str(path)
shared.schema.set_string(key, value)
@@ -381,10 +385,12 @@ class PreferencesWindow(Adw.PreferencesWindow):
)
# Connect dir picker buttons
for location in ("data", "config", "cache"):
button = getattr(self, f"{source.id}_{location}_file_chooser_button", None)
for location_name in source.locations._asdict():
button = getattr(
self, f"{source.id}_{location_name}_file_chooser_button", 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
self.resolve_locations(source)