heroic: Implement Heroic source

This commit is contained in:
kramo
2025-12-22 16:29:29 +01:00
parent d1371baf2b
commit ea0e5b5c47
5 changed files with 204 additions and 2 deletions

View File

@@ -10,7 +10,7 @@ from gi.repository import Adw
from cartridges import collections, games
from cartridges.games import Game
from cartridges.sources import Source, steam
from cartridges.sources import Source, heroic, steam
from .config import APP_ID, PREFIX
from .ui.window import Window
@@ -29,7 +29,7 @@ class Application(Adw.Application):
self.set_accels_for_action("app.quit", ("<Control>q",))
saved = tuple(games.load())
new = self.import_games(steam, skip_ids={g.game_id for g in saved})
new = self.import_games(steam, heroic, skip_ids={g.game_id for g in saved})
games.model.splice(0, 0, (*saved, *new))
collections.load()

View File

@@ -12,8 +12,20 @@ from gi.repository import GLib
from cartridges.games import Game
DATA = Path(GLib.get_user_data_dir())
CONFIG = Path(GLib.get_user_config_dir())
CACHE = Path(GLib.get_user_cache_dir())
FLATPAK = Path.home() / ".var" / "app"
HOST_DATA = Path(os.getenv("HOST_XDG_DATA_HOME", Path.home() / ".local" / "share"))
HOST_CONFIG = Path(os.getenv("HOST_XDG_CONFIG_HOME", Path.home() / ".config"))
HOST_CACHE = Path(os.getenv("HOST_XDG_CACHE_HOME", Path.home() / ".cache"))
PROGRAM_FILES_X86 = Path(os.getenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
APPDATA = Path(os.getenv("APPDATA", r"C:\Users\Default\AppData\Roaming"))
LOCAL_APPDATA = Path(
os.getenv("CSIDL_LOCAL_APPDATA", r"C:\Users\Default\AppData\Local")
)
APPLICATION_SUPPORT = Path.home() / "Library" / "Application Support"
OPEN = (

View File

@@ -0,0 +1,188 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2023 Geoffrey Coulaud
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
import json
import time
from abc import ABC, abstractmethod
from collections.abc import Generator, Iterable
from contextlib import suppress
from gettext import gettext as _
from hashlib import sha256
from json import JSONDecodeError
from pathlib import Path
from typing import Any, override
from gi.repository import Gdk, GLib
from cartridges.games import Game
from . import APPDATA, APPLICATION_SUPPORT, CONFIG, FLATPAK, HOST_CONFIG, OPEN
ID, NAME = "heroic", _("Heroic")
_CONFIG_PATHS = (
CONFIG / "heroic",
HOST_CONFIG / "heroic",
FLATPAK / "com.heroicgameslauncher.hgl" / "config" / "heroic",
APPDATA / "heroic",
APPLICATION_SUPPORT / "heroic",
)
class _Source(ABC):
ID: str
COVER_URI_PARAMS = ""
LIBRARY_KEY = "library"
@classmethod
@abstractmethod
def library_path(cls) -> Path: ...
@classmethod
def installed_app_names(cls) -> set[str] | None:
return None
class _SideloadSource(_Source):
ID = "sideload"
LIBRARY_KEY = "games"
@classmethod
def library_path(cls) -> Path:
return Path("sideload_apps", "library.json")
class _StoreSource(_Source):
_INSTALLED_PATH: Path
@classmethod
def library_path(cls) -> Path:
return Path("store_cache", f"{cls.ID}_library.json")
@override
@classmethod
def installed_app_names(cls) -> set[str]:
try:
with (_config_dir() / cls._INSTALLED_PATH).open() as fp:
data = json.load(fp)
except (FileNotFoundError, JSONDecodeError):
return set()
return set(cls._installed(data))
@staticmethod
def _installed(data: Any) -> Generator[str]: # noqa: ANN401
with suppress(AttributeError):
yield from data.keys()
class _LegendarySource(_StoreSource):
ID = "legendary"
COVER_URI_PARAMS = "?h=400&resize=1&w=300"
_INSTALLED_PATH = Path("legendaryConfig", "legendary", "installed.json")
class _GOGSource(_StoreSource):
ID = "gog"
LIBRARY_KEY = "games"
_INSTALLED_PATH = Path("gog_store", "installed.json")
@override
@staticmethod
def _installed(data: Any) -> Generator[str]:
with suppress(TypeError, KeyError):
for entry in data["installed"]:
with suppress(TypeError, KeyError):
yield entry["appName"]
class _NileSource(_StoreSource):
ID = "nile"
LIBRARY_PATH = Path("store_cache", "nile_library.json")
_INSTALLED_PATH = Path("nile_config", "nile", "installed.json")
@override
@staticmethod
def _installed(data: Any) -> Generator[str]:
with suppress(TypeError):
for entry in data:
with suppress(TypeError, KeyError):
yield entry["id"]
def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]:
"""Installed Heroic games."""
added = int(time.time())
for source in _LegendarySource, _GOGSource, _NileSource, _SideloadSource:
yield from _games_from(source, added, skip_ids)
def _config_dir() -> Path:
for path in _CONFIG_PATHS:
if path.is_dir():
return path
raise FileNotFoundError
def _hidden_app_names() -> Generator[str]:
try:
with (_config_dir() / "store" / "config.json").open() as fp:
config = json.load(fp)
except (FileNotFoundError, JSONDecodeError):
return
with suppress(TypeError, KeyError):
for game in config["games"]["hidden"]:
with suppress(TypeError, KeyError):
yield game["appName"]
def _games_from(
source: type[_Source], added: int, skip_ids: Iterable[str]
) -> Generator[Game]:
try:
with (_config_dir() / source.library_path()).open() as fp:
library = json.load(fp)
except (FileNotFoundError, JSONDecodeError):
return
if not isinstance(library := library.get(source.LIBRARY_KEY), Iterable):
return
source_id = f"{ID}_{source.ID}"
images_cache = _config_dir() / "images-cache"
installed = source.installed_app_names()
hidden = set(_hidden_app_names())
for entry in library:
with suppress(TypeError, KeyError):
app_name = entry["app_name"]
if (installed is not None) and (app_name not in installed):
continue
game_id = f"{source_id}_{app_name}"
if game_id in skip_ids:
continue
cover_uri = f"{entry.get('art_square', '')}{source.COVER_URI_PARAMS}"
cover_path = images_cache / sha256(cover_uri.encode()).hexdigest()
try:
cover = Gdk.Texture.new_from_filename(str(cover_path))
except GLib.Error:
cover = None
yield Game(
added=added,
executable=f"{OPEN} heroic://launch/{entry['runner']}/{app_name}",
game_id=game_id,
source=source_id,
hidden=app_name in hidden,
name=entry["title"],
developer=entry.get("developer"),
cover=cover,
)

View File

@@ -12,6 +12,7 @@
"--socket=wayland",
"--talk-name=org.freedesktop.Flatpak",
"--filesystem=host:ro",
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
"--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro"
],
"cleanup": [

View File

@@ -3,6 +3,7 @@ cartridges/collections.py
cartridges/gamepads.py
cartridges/games.py
cartridges/sources/__init__.py
cartridges/sources/heroic.py
cartridges/sources/steam.py
cartridges/ui/collections.py
cartridges/ui/cover.blp