sources: Move to module
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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@',
|
||||||
|
|||||||
@@ -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
|
|
||||||
36
cartridges/sources/__init__.py
Normal file
36
cartridges/sources/__init__.py
Normal 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
107
cartridges/sources/steam.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user