sources: Move to module

This commit is contained in:
kramo
2025-12-03 17:52:04 +01:00
committed by Jamie Gravendeel
parent d47bb2df7d
commit 49323b25b3
6 changed files with 150 additions and 146 deletions

View File

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

View File

@@ -1,14 +1,10 @@
python.install_sources( python.install_sources(
files( files('__init__.py', '__main__.py', 'application.py', 'games.py'),
'__init__.py',
'__main__.py',
'application.py',
'games.py',
'sources.py',
),
subdir: 'cartridges', subdir: 'cartridges',
) )
install_subdir('sources', install_dir: python.get_install_dir() / 'cartridges')
configure_file( configure_file(
input: 'config.py.in', input: 'config.py.in',
output: '@BASENAME@', output: '@BASENAME@',

View File

@@ -1,136 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2023 Geoffrey Coulaud
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
import itertools
import os
import re
import sys
import time
from collections.abc import Generator, Iterable
from pathlib import Path
from typing import Protocol
from gi.repository import Gdk, GLib
from cartridges.games import Game
DATA = Path(GLib.get_user_data_dir())
FLATPAK = Path.home() / ".var" / "app"
PROGRAM_FILES_X86 = Path(os.getenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
APPLICATION_SUPPORT = Path.home() / "Library" / "Application Support"
OPEN = (
"open"
if sys.platform.startswith("darwin")
else "start"
if sys.platform.startswith("win32")
else "xdg-open"
)
class Source(Protocol):
"""A source of games to import."""
ID: str
def get_games(self, *, skip_ids: Iterable[str]) -> Generator[Game]:
"""Installed games, except those in `skip_ids`."""
...
class SteamSource:
"""A source for the Valve's Steam."""
ID: str = "steam"
_INSTALLED_MASK = 4
_CAPSULE_NAMES = "library_600x900.jpg", "library_capsule.jpg"
_DATA_PATHS = (
Path.home() / ".steam" / "steam",
DATA / "Steam",
FLATPAK / "com.valvesoftware.Steam" / "data" / "Steam",
PROGRAM_FILES_X86 / "Steam",
APPLICATION_SUPPORT / "Steam",
)
@property
def _data(self) -> Path:
for path in self._DATA_PATHS:
if path.is_dir():
return path
raise FileNotFoundError
@property
def _library_folders(self) -> Generator[Path]:
return (
steamapps
for folder in re.findall(
r'"path"\s+"(.*)"\n',
(self._data / "steamapps" / "libraryfolders.vdf").read_text("utf-8"),
re.IGNORECASE,
)
if (steamapps := Path(folder) / "steamapps").is_dir()
)
@property
def _manifests(self) -> Generator[Path]:
return (
manifest
for folder in self._library_folders
for manifest in folder.glob("appmanifest_*.acf")
if manifest.is_file()
)
def get_games(self, *, skip_ids: Iterable[str]) -> Generator[Game]:
"""Installed Steam games."""
added = int(time.time())
librarycache = self._data / "appcache" / "librarycache"
appids = {i.rsplit("_", 1)[-1] for i in skip_ids if i.startswith(f"{self.ID}_")}
for manifest in self._manifests:
contents = manifest.read_text("utf-8")
try:
name, appid, stateflags = (
self._parse(contents, key)
for key in ("name", "appid", "stateflags")
)
stateflags = int(stateflags)
except ValueError:
continue
duplicate = appid in appids
installed = stateflags & self._INSTALLED_MASK
if duplicate or not installed:
continue
game = Game(
added=added,
executable=f"{OPEN} steam://rungameid/{appid}",
game_id=f"{self.ID}_{appid}",
source=self.ID,
name=name,
)
for path in itertools.chain.from_iterable(
(librarycache / appid).rglob(filename)
for filename in self._CAPSULE_NAMES
):
try:
game.cover = Gdk.Texture.new_from_filename(str(path))
except GLib.Error:
continue
else:
break
yield game
appids.add(appid)
@staticmethod
def _parse(manifest: str, key: str) -> str:
match = re.search(rf'"{key}"\s+"(.*)"\n', manifest, re.IGNORECASE)
if match and isinstance(group := match.group(1), str):
return group
raise ValueError

View File

@@ -0,0 +1,36 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 kramo
import os
import sys
from collections.abc import Generator, Iterable
from pathlib import Path
from typing import Protocol
from gi.repository import GLib
from cartridges.games import Game
DATA = Path(GLib.get_user_data_dir())
FLATPAK = Path.home() / ".var" / "app"
PROGRAM_FILES_X86 = Path(os.getenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
APPLICATION_SUPPORT = Path.home() / "Library" / "Application Support"
OPEN = (
"open"
if sys.platform.startswith("darwin")
else "start"
if sys.platform.startswith("win32")
else "xdg-open"
)
class Source(Protocol):
"""A source of games to import."""
ID: str
@staticmethod
def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]:
"""Installed games, except those in `skip_ids`."""
...

107
cartridges/sources/steam.py Normal file
View File

@@ -0,0 +1,107 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2023 Geoffrey Coulaud
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
import itertools
import re
import time
from collections.abc import Generator, Iterable
from pathlib import Path
from gi.repository import Gdk, GLib
from cartridges.games import Game
from . import APPLICATION_SUPPORT, DATA, FLATPAK, OPEN, PROGRAM_FILES_X86
ID: str = "steam"
_INSTALLED_MASK = 4
_CAPSULE_NAMES = "library_600x900.jpg", "library_capsule.jpg"
_DATA_PATHS = (
Path.home() / ".steam" / "steam",
DATA / "Steam",
FLATPAK / "com.valvesoftware.Steam" / "data" / "Steam",
PROGRAM_FILES_X86 / "Steam",
APPLICATION_SUPPORT / "Steam",
)
def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]:
"""Installed Steam games."""
added = int(time.time())
librarycache = _data_dir() / "appcache" / "librarycache"
appids = {i.rsplit("_", 1)[-1] for i in skip_ids if i.startswith(f"{ID}_")}
for manifest in _manifests():
contents = manifest.read_text("utf-8")
try:
name, appid, stateflags = (
_parse(contents, key) for key in ("name", "appid", "stateflags")
)
stateflags = int(stateflags)
except ValueError:
continue
duplicate = appid in appids
installed = stateflags & _INSTALLED_MASK
if duplicate or not installed:
continue
game = Game(
added=added,
executable=f"{OPEN} steam://rungameid/{appid}",
game_id=f"{ID}_{appid}",
source=ID,
name=name,
)
for path in itertools.chain.from_iterable(
(librarycache / appid).rglob(filename) for filename in _CAPSULE_NAMES
):
try:
game.cover = Gdk.Texture.new_from_filename(str(path))
except GLib.Error:
continue
else:
break
yield game
appids.add(appid)
def _data_dir() -> Path:
for path in _DATA_PATHS:
if path.is_dir():
return path
raise FileNotFoundError
def _library_folders() -> Generator[Path]:
return (
steamapps
for folder in re.findall(
r'"path"\s+"(.*)"\n',
(_data_dir() / "steamapps" / "libraryfolders.vdf").read_text("utf-8"),
re.IGNORECASE,
)
if (steamapps := Path(folder) / "steamapps").is_dir()
)
def _manifests() -> Generator[Path]:
return (
manifest
for folder in _library_folders()
for manifest in folder.glob("appmanifest_*.acf")
if manifest.is_file()
)
def _parse(manifest: str, key: str) -> str:
match = re.search(rf'"{key}"\s+"(.*)"\n', manifest, re.IGNORECASE)
if match and isinstance(group := match.group(1), str):
return group
raise ValueError

View File

@@ -1,6 +1,7 @@
cartridges/application.py cartridges/application.py
cartridges/games.py cartridges/games.py
cartridges/sources.py cartridges/sources/__init__.py
cartridges/sources/steam.py
cartridges/ui/cover.blp cartridges/ui/cover.blp
cartridges/ui/cover.py cartridges/ui/cover.py
cartridges/ui/game-details.blp cartridges/ui/game-details.blp