🚧 Ground work for heroic source

This commit is contained in:
GeoffreyCoulaud
2023-05-31 21:47:55 +02:00
parent 344aa7057d
commit ed66109404
2 changed files with 185 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
import json
from abc import abstractmethod
from hashlib import sha256
from json import JSONDecodeError
from pathlib import Path
from time import time
from typing import Generator, Optional, TypedDict
from src import shared
from src.game import Game
from src.importer.sources.source import Source, SourceIterator
from src.utils.decorators import (
replaced_by_env_path,
replaced_by_path,
replaced_by_schema_key,
)
from src.utils.save_cover import resize_cover, save_cover
class HeroicLibraryEntry(TypedDict):
app_name: str
installed: Optional[bool]
runner: str
title: str
developer: str
art_square: str
class HeroicSubSource(TypedDict):
service: str
path: tuple[str]
class HeroicSourceIterator(SourceIterator):
source: "HeroicSource"
generator: Generator = None
sub_sources: dict[str, HeroicSubSource] = {
"sideload": {
"service": "sideload",
"path": ("sideload_apps", "library.json"),
},
"legendary": {
"service": "epic",
"path": ("store_cache", "legendary_library.json"),
},
"gog": {
"service": "gog",
"path": ("store_cache", "gog_library.json"),
},
}
def game_from_library_entry(self, entry: HeroicLibraryEntry) -> Optional[Game]:
"""Helper method used to build a Game from a Heroic library entry"""
# Skip games that are not installed
if not entry["installed"]:
return None
# Build game
app_name = entry["app_name"]
runner = entry["runner"]
service = self.sub_sources[runner]["service"]
values = {
"version": shared.SPEC_VERSION,
"hidden": False,
"source": self.source.id,
"added": int(time()),
"name": entry["title"],
"developer": entry["developer"],
"game_id": self.source.game_id_format.format(
service=service, game_id=app_name
),
"executable": self.source.executable_format.format(app_name=app_name),
}
# Save image from the heroic cache
# Filenames are derived from the URL that heroic used to get the file
uri: str = entry["art_square"]
if service == "epic":
uri += "?h=400&resize=1&w=300"
digest = sha256(uri.encode()).hexdigest()
image_path = self.source.location / "images-cache" / digest
if image_path.is_file():
save_cover(values["game_id"], resize_cover(image_path))
return Game(values, allow_side_effects=False)
def sub_sources_generator(self):
"""Generator method producing games from all the Heroic sub-sources"""
for key, sub_source in self.sub_sources.items():
# Skip disabled sub-sources
if not shared.schema.get_boolean("heroic-import-" + key):
continue
# Load games from JSON
try:
file = self.source.location.joinpath(*sub_source["path"])
library = json.load(file.open())["library"]
except (JSONDecodeError, OSError, KeyError):
# Invalid library.json file, skip it
continue
for entry in library:
try:
game = self.game_from_library_entry(entry)
except KeyError:
# Skip invalid games
continue
yield game
def __init__(self, source: "HeroicSource") -> None:
self.source = source
self.generator = self.sub_sources_generator()
def __next__(self) -> Optional[Game]:
try:
game = next(self.generator)
except StopIteration:
raise
return game
class HeroicSource(Source):
"""Generic heroic games launcher source"""
name = "Heroic"
executable_format = "xdg-open heroic://launch/{app_name}"
@property
@abstractmethod
def location(self) -> Path:
pass
@property
def game_id_format(self) -> str:
"""The string format used to construct game IDs"""
return self.name.lower() + "_{service}_{game_id}"
@property
def is_installed(self):
# pylint: disable=pointless-statement
try:
self.location
except FileNotFoundError:
return False
return True
def __iter__(self):
return HeroicSourceIterator(source=self)
class HeroicNativeSource(HeroicSource):
variant = "native"
@replaced_by_schema_key("heroic-location")
@replaced_by_env_path("XDG_CONFIG_HOME", "heroic/")
@replaced_by_path("~/.config/heroic/")
def location(self) -> Path:
raise FileNotFoundError()
class HeroicFlatpakSource(HeroicSource):
variant = "flatpak"
@replaced_by_schema_key("heroic-flatpak-location")
@replaced_by_path("~/.var/app/com.heroicgameslauncher.hgl/config/heroic/")
def location(self) -> Path:
raise FileNotFoundError()
class HeroicWindowsSource(HeroicSource):
variant = "windows"
executable_format = "start heroic://launch/{app_name}"
@replaced_by_schema_key("heroic-windows-location")
@replaced_by_env_path("appdata", "heroic/")
def location(self) -> Path:
raise FileNotFoundError()

View File

@@ -34,6 +34,11 @@ from src import shared
from src.details_window import DetailsWindow
from src.game import Game
from src.importer.importer import Importer
from src.importer.sources.heroic_source import (
HeroicFlatpakSource,
HeroicNativeSource,
HeroicWindowsSource,
)
from src.importer.sources.lutris_source import LutrisFlatpakSource, LutrisNativeSource
from src.importer.sources.steam_source import (
SteamFlatpakSource,
@@ -193,6 +198,10 @@ class CartridgesApplication(Adw.Application):
importer.add_source(SteamNativeSource())
importer.add_source(SteamFlatpakSource())
importer.add_source(SteamWindowsSource())
if shared.schema.get_boolean("heroic"):
importer.add_source(HeroicNativeSource())
importer.add_source(HeroicFlatpakSource())
importer.add_source(HeroicWindowsSource())
importer.run()
def on_remove_game_action(self, *_args):