diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp
index 5a87688..fffb5b9 100644
--- a/data/gtk/preferences.blp
+++ b/data/gtk/preferences.blp
@@ -202,4 +202,38 @@ template PreferencesWindow : Adw.PreferencesWindow {
}
}
}
+
+ Adw.PreferencesPage sgdb_page {
+ name: "sgdb";
+ title: _("SteamGridDB");
+ icon-name: "image-x-generic-symbolic";
+
+ Adw.PreferencesGroup sgdb_key_group {
+ title: _("Authentication");
+ description: _("An API Key is required to use SteamGridDB. You can generate one here.");
+
+ Adw.EntryRow sgdb_key_entry_row {
+ title: _("API Key");
+ }
+ }
+
+ Adw.PreferencesGroup sgdb_behavior_group {
+ title: _("Behavior");
+
+ Adw.ActionRow {
+ title: _("Download Images on Import");
+
+ Switch sgdb_download_switch {
+ valign: center;
+ }
+ }
+ Adw.ActionRow {
+ title: _("Prefer Over Official Images");
+
+ Switch sgdb_prefer_switch {
+ valign: center;
+ }
+ }
+ }
+ }
}
diff --git a/data/hu.kramo.Cartridges.gschema.xml b/data/hu.kramo.Cartridges.gschema.xml
index d05ef97..e4363ac 100644
--- a/data/hu.kramo.Cartridges.gschema.xml
+++ b/data/hu.kramo.Cartridges.gschema.xml
@@ -57,6 +57,15 @@
"~/.var/app/io.itch.itch/config/itch/"
+
+
+ ""
+
+
+ false
+
+
+ false
diff --git a/hu.kramo.Cartridges.json b/hu.kramo.Cartridges.json
index 1182fcb..c36f1e1 100644
--- a/hu.kramo.Cartridges.json
+++ b/hu.kramo.Cartridges.json
@@ -63,6 +63,45 @@
"*"
]
},
+ {
+ "name": "python3-python-steamgriddb",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"python-steamgriddb\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/71/4c/3db2b8021bd6f2f0ceb0e088d6b2d49147671f25832fb17970e9b583d742/certifi-2022.12.7-py3-none-any.whl",
+ "sha256": "4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/ff/d7/8d757f8bd45be079d76309248845a04f09619a7b17d6dfc8c9ff6433cac2/charset-normalizer-3.1.0.tar.gz",
+ "sha256": "34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl",
+ "sha256": "90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/d6/90/a84d927799ca177d4f7a111f99fee0a3f19da63e42c3b325c9c1bfe3bba0/python-steamgriddb-1.0.5.tar.gz",
+ "sha256": "036db7bb09865da73b40b68cf04fb9675cd18b4908275092d91f37bf16245069"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/d2/f4/274d1dbe96b41cf4e0efb70cbced278ffd61b5c7bb70338b62af94ccb25b/requests-2.28.2-py3-none-any.whl",
+ "sha256": "64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/7b/f5/890a0baca17a61c1f92f72b81d3c31523c99bec609e60c292ea55b387ae8/urllib3-1.26.15-py2.py3-none-any.whl",
+ "sha256": "aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"
+ }
+ ]
+ },
{
"name" : "cartridges",
"builddir" : true,
diff --git a/src/meson.build b/src/meson.build
index 708174b..bd8b536 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -23,6 +23,7 @@ cartridges_sources = [
'preferences.py',
'game.py',
'utils/importer.py',
+ 'utils/steamgriddb.py',
'utils/steam_parser.py',
'utils/lutris_parser.py',
'utils/heroic_parser.py',
diff --git a/src/preferences.py b/src/preferences.py
index 1c275aa..fc01057 100644
--- a/src/preferences.py
+++ b/src/preferences.py
@@ -83,6 +83,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
general_page = Gtk.Template.Child()
import_page = Gtk.Template.Child()
+ sgdb_page = Gtk.Template.Child()
sources_group = Gtk.Template.Child()
@@ -113,6 +114,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
itch_expander_row = Gtk.Template.Child()
itch_file_chooser_button = Gtk.Template.Child()
+ sgdb_key_entry_row = Gtk.Template.Child()
+ sgdb_download_switch = Gtk.Template.Child()
+ sgdb_prefer_switch = Gtk.Template.Child()
+
def __init__(self, parent_widget, **kwargs):
super().__init__(**kwargs)
self.schema = parent_widget.schema
@@ -296,6 +301,27 @@ class PreferencesWindow(Adw.PreferencesWindow):
True,
)
+ # SteamGridDB
+ self.schema.bind(
+ "sgdb-import",
+ self.sgdb_download_switch,
+ "active",
+ Gio.SettingsBindFlags.DEFAULT,
+ )
+
+ self.schema.bind(
+ "sgdb-prefer",
+ self.sgdb_prefer_switch,
+ "active",
+ Gio.SettingsBindFlags.DEFAULT,
+ )
+
+ def sgdb_key_changed(_widget):
+ self.schema.set_string("sgdb-key", self.sgdb_key_entry_row.get_text())
+
+ self.sgdb_key_entry_row.set_text(self.schema.get_string("sgdb-key"))
+ self.sgdb_key_entry_row.connect("changed", sgdb_key_changed)
+
def choose_folder(self, _widget, function):
self.file_chooser.select_folder(self.parent_widget, None, function, None)
diff --git a/src/utils/bottles_parser.py b/src/utils/bottles_parser.py
index 9d0ef51..ed45ab7 100644
--- a/src/utils/bottles_parser.py
+++ b/src/utils/bottles_parser.py
@@ -77,15 +77,15 @@ def bottles_parser(parent_widget):
values["added"] = current_time
values["last_played"] = 0
- if game["thumbnail"]:
- importer.save_cover(
- values["game_id"],
- (
- bottles_dir
- / "bottles"
- / game["bottle"]["path"]
- / "grids"
- / game["thumbnail"].split(":")[1]
- ),
+ importer.save_game(
+ values,
+ (
+ bottles_dir
+ / "bottles"
+ / game["bottle"]["path"]
+ / "grids"
+ / game["thumbnail"].split(":")[1]
)
- importer.save_game(values)
+ if game["thumbnail"]
+ else None,
+ )
diff --git a/src/utils/create_details_window.py b/src/utils/create_details_window.py
index 3fd8b8e..a5ae905 100644
--- a/src/utils/create_details_window.py
+++ b/src/utils/create_details_window.py
@@ -37,7 +37,7 @@ def create_details_window(parent_widget, game_id=None):
games = parent_widget.games
pixbuf = None
- if game_id is None:
+ if not game_id:
window.set_title(_("Add New Game"))
cover = Gtk.Picture.new_for_pixbuf(parent_widget.placeholder_pixbuf)
name = Gtk.Entry()
@@ -215,13 +215,13 @@ def create_details_window(parent_widget, game_id=None):
create_dialog(
window,
_("Couldn't Add Game")
- if game_id is None
+ if not game_id
else _("Couldn't Apply Preferences"),
f'{_("Executable")}: {exception}.',
)
return
- if game_id is None:
+ if not game_id:
if final_name == "":
create_dialog(
window, _("Couldn't Add Game"), _("Game title cannot be empty.")
@@ -267,7 +267,7 @@ def create_details_window(parent_widget, game_id=None):
)
return
- if pixbuf is not None:
+ if pixbuf:
save_cover(parent_widget, game_id, None, pixbuf)
values["name"] = final_name
diff --git a/src/utils/heroic_parser.py b/src/utils/heroic_parser.py
index 0221ce8..663eb02 100644
--- a/src/utils/heroic_parser.py
+++ b/src/utils/heroic_parser.py
@@ -100,10 +100,9 @@ def heroic_parser(parent_widget):
(f'{game["art_square"]}?h=400&resize=1&w=300').encode()
).hexdigest()
)
- if image_path.exists():
- importer.save_cover(values["game_id"], image_path)
- importer.save_game(values)
+ importer.save_game(values, image_path if image_path.exists() else None)
+
except KeyError:
pass
@@ -142,9 +141,6 @@ def heroic_parser(parent_widget):
/ "images-cache"
/ hashlib.sha256(game["art_square"].encode()).hexdigest()
)
- if image_path.exists():
- importer.save_cover(values["game_id"], image_path)
- break
values["executable"] = (
["start", f"heroic://launch/{app_name}"]
@@ -156,7 +152,7 @@ def heroic_parser(parent_widget):
values["added"] = current_time
values["last_played"] = 0
- importer.save_game(values)
+ importer.save_game(values, image_path if image_path.exists() else None)
# Import sideloaded games
if not schema.get_boolean("heroic-import-sideload"):
@@ -196,7 +192,5 @@ def heroic_parser(parent_widget):
/ "images-cache"
/ hashlib.sha256(item["art_square"].encode()).hexdigest()
)
- if image_path.exists():
- importer.save_cover(values["game_id"], image_path)
- importer.save_game(values)
+ importer.save_game(values, image_path if image_path.exists() else None)
diff --git a/src/utils/importer.py b/src/utils/importer.py
index 951b06d..5dde5e7 100644
--- a/src/utils/importer.py
+++ b/src/utils/importer.py
@@ -24,6 +24,7 @@ from gi.repository import Adw, Gtk
from .create_dialog import create_dialog
from .save_cover import save_cover
from .save_game import save_game
+from .steamgriddb import SGDBSave
class Importer:
@@ -33,15 +34,17 @@ class Importer:
self.queue = 0
self.games_no = 0
self.blocker = False
+ self.games = set()
+ self.sgdb_exception = None
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
- import_statuspage = Adw.StatusPage(
+ self.import_statuspage = Adw.StatusPage(
title=_("Importing Games…"),
child=self.progressbar,
)
self.import_dialog = Adw.Window(
- content=import_statuspage,
+ content=self.import_statuspage,
modal=True,
default_width=350,
default_height=-1,
@@ -51,21 +54,35 @@ class Importer:
self.import_dialog.present()
- def save_cover(self, game_id, cover_path=None, pixbuf=None):
- save_cover(self.parent_widget, game_id, cover_path, pixbuf)
-
- def save_game(self, values=None):
+ def save_game(self, values=None, cover_path=None, pixbuf=None):
if values:
- self.games_no += 1
save_game(self.parent_widget, values)
- self.parent_widget.update_games([values["game_id"]])
+
+ if cover_path or pixbuf:
+ save_cover(self.parent_widget, values["game_id"], cover_path, pixbuf)
+
+ self.games.add((values["game_id"], values["name"]))
+
+ self.games_no += 1
if "blacklisted" in values:
self.games_no -= 1
self.queue -= 1
- self.progressbar.set_fraction(1 - (self.queue / self.total_queue))
+ self.update_progressbar()
if self.queue == 0 and not self.blocker:
+ if self.games:
+ self.total_queue = len(self.games)
+ self.queue = len(self.games)
+ self.import_statuspage.set_title(_("Importing Covers…"))
+ self.update_progressbar()
+ SGDBSave(self, self.games)
+ else:
+ self.done()
+
+ def done(self):
+ self.update_progressbar()
+ if self.queue == 0:
self.import_dialog.close()
if self.games_no == 0:
@@ -75,14 +92,14 @@ class Importer:
_("No new games were found on your system."),
"open_preferences",
_("Preferences"),
- ).connect("response", self.response)
+ ).connect("response", self.response, "import")
elif self.games_no == 1:
create_dialog(
self.parent_widget,
_("Game Imported"),
_("Successfully imported 1 game."),
- ).connect("response", self.response)
+ ).connect("response", self.response, "import")
elif self.games_no > 1:
games_no = self.games_no
create_dialog(
@@ -90,13 +107,22 @@ class Importer:
_("Games Imported"),
# The variable is the number of games
_("Successfully imported {} games.").format(games_no),
- ).connect("response", self.response)
+ ).connect("response", self.response, "import")
- def response(self, _widget, response, expander_row=None):
+ def response(self, _widget, response, page_name=None, expander_row=None):
if response == "open_preferences":
self.parent_widget.get_application().on_preferences_action(
- None, page_name="import", expander_row=expander_row
+ None, page_name=page_name, expander_row=expander_row
)
+ elif self.sgdb_exception:
+ create_dialog(
+ self.parent_widget,
+ _("Couldn't Connect to SteamGridDB"),
+ self.sgdb_exception,
+ "open_preferences",
+ _("Preferences"),
+ ).connect("response", self.response, "sgdb")
+ self.sgdb_exception = None
elif (
self.parent_widget.schema.get_boolean("steam")
and self.parent_widget.schema.get_boolean("steam-extra-dirs-hint")
@@ -120,4 +146,7 @@ class Importer:
),
"open_preferences",
_("Preferences"),
- ).connect("response", self.response, "steam_expander_row")
+ ).connect("response", self.response, "import", "steam_expander_row")
+
+ def update_progressbar(self):
+ self.progressbar.set_fraction(1 - (self.queue / self.total_queue))
diff --git a/src/utils/itch_parser.py b/src/utils/itch_parser.py
index c165b07..ce7d46a 100644
--- a/src/utils/itch_parser.py
+++ b/src/utils/itch_parser.py
@@ -35,7 +35,7 @@ def get_game(task, current_time, parent_widget, row, importer):
values["game_id"] in parent_widget.games
and not parent_widget.games[values["game_id"]].removed
):
- task.return_value(None)
+ task.return_value((None, None))
return
values["added"] = current_time
@@ -51,7 +51,7 @@ def get_game(task, current_time, parent_widget, row, importer):
with urllib.request.urlopen(row[3] or row[2], timeout=5) as open_file:
Path(tmp_file.get_path()).write_bytes(open_file.read())
except urllib.error.URLError:
- task.return_value(values)
+ task.return_value((values, None))
return
cover_pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
@@ -77,8 +77,10 @@ def get_game(task, current_time, parent_widget, row, importer):
GdkPixbuf.InterpType.BILINEAR,
255,
)
- importer.save_cover(values["game_id"], pixbuf=cover_pixbuf)
- task.return_value(values)
+ else:
+ cover_pixbuf = None
+
+ task.return_value((values, cover_pixbuf))
def get_games_async(parent_widget, rows, importer):
@@ -100,7 +102,7 @@ def get_games_async(parent_widget, rows, importer):
def update_games(_task, result):
final_values = result.propagate_value()[1]
# No need for an if statement as final_value would be None for games we don't want to save
- importer.save_game(final_values)
+ importer.save_game(final_values[0], pixbuf=final_values[1])
for row in rows:
task = Gio.Task.new(None, None, update_games)
diff --git a/src/utils/lutris_parser.py b/src/utils/lutris_parser.py
index 8961fe1..56f47d4 100644
--- a/src/utils/lutris_parser.py
+++ b/src/utils/lutris_parser.py
@@ -108,9 +108,5 @@ def lutris_parser(parent_widget):
values["name"] = row[1]
values["source"] = f"lutris_{row[3]}"
- if (cache_dir / "coverart" / f"{row[2]}.jpg").is_file():
- importer.save_cover(
- values["game_id"], (cache_dir / "coverart" / f"{row[2]}.jpg")
- )
-
- importer.save_game(values)
+ image_path = cache_dir / "coverart" / f"{row[2]}.jpg"
+ importer.save_game(values, image_path if image_path.exists() else None)
diff --git a/src/utils/save_cover.py b/src/utils/save_cover.py
index 22b3638..0681a3d 100644
--- a/src/utils/save_cover.py
+++ b/src/utils/save_cover.py
@@ -26,7 +26,7 @@ def save_cover(parent_widget, game_id, cover_path=None, pixbuf=None):
covers_dir.mkdir(parents=True, exist_ok=True)
- if pixbuf is None:
+ if not pixbuf:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
str(cover_path), 400, 600, False
)
diff --git a/src/utils/steam_parser.py b/src/utils/steam_parser.py
index f3bce0a..ec12d55 100644
--- a/src/utils/steam_parser.py
+++ b/src/utils/steam_parser.py
@@ -57,7 +57,7 @@ def get_game(
values["game_id"] in parent_widget.games
and not parent_widget.games[values["game_id"]].removed
):
- task.return_value(None)
+ task.return_value((None, None))
return
values["executable"] = (
@@ -70,21 +70,12 @@ def get_game(
values["added"] = current_time
values["last_played"] = 0
- if (
+ image_path = (
steam_dir
/ "appcache"
/ "librarycache"
/ f'{values["appid"]}_library_600x900.jpg'
- ).is_file():
- importer.save_cover(
- values["game_id"],
- (
- steam_dir
- / "appcache"
- / "librarycache"
- / f'{values["appid"]}_library_600x900.jpg'
- ),
- )
+ )
try:
with urllib.request.urlopen(
@@ -93,11 +84,11 @@ def get_game(
) as open_file:
content = open_file.read().decode("utf-8")
except urllib.error.URLError:
- task.return_value(values)
+ task.return_value((values, image_path if image_path.exists() else None))
return
values = update_values_from_data(content, values)
- task.return_value(values)
+ task.return_value((values, image_path if image_path.exists() else None))
def get_games_async(parent_widget, appmanifests, steam_dir, importer):
@@ -122,7 +113,7 @@ def get_games_async(parent_widget, appmanifests, steam_dir, importer):
def update_games(_task, result):
final_values = result.propagate_value()[1]
# No need for an if statement as final_value would be None for games we don't want to save
- importer.save_game(final_values)
+ importer.save_game(final_values[0], final_values[1])
for appmanifest in appmanifests:
task = Gio.Task.new(None, None, update_games)
diff --git a/src/utils/steamgriddb.py b/src/utils/steamgriddb.py
new file mode 100644
index 0000000..6417338
--- /dev/null
+++ b/src/utils/steamgriddb.py
@@ -0,0 +1,69 @@
+from pathlib import Path
+
+import requests
+from gi.repository import Gio
+from steamgrid import SteamGridDB, http
+
+from .save_cover import save_cover
+
+
+class SGDBSave:
+ def __init__(self, importer, games):
+ self.importer = importer
+ self.sgdb = SteamGridDB(importer.parent_widget.schema.get_string("sgdb-key"))
+
+ # Wrap the function in another one as Gio.Task.run_in_thread does not allow for passing args
+ def create_func(game):
+ def wrapper(task, *_unused):
+ self.update_cover(
+ task,
+ game,
+ )
+
+ return wrapper
+
+ for game in games:
+ Gio.Task.new(None, None, self.task_done).run_in_thread(create_func(game))
+
+ def update_cover(self, task, game):
+ if self.importer.parent_widget.schema.get_boolean("sgdb-prefer") or (
+ self.importer.parent_widget.schema.get_boolean("sgdb-import")
+ and self.importer.parent_widget.games[game[0]].pixbuf
+ == self.importer.parent_widget.placeholder_pixbuf
+ ):
+ try:
+ search_result = self.sgdb.search_game(game[1])
+ except requests.exceptions.RequestException:
+ task.return_value(game[0])
+ return
+ except http.HTTPException as exception:
+ self.importer.sgdb_exception = str(exception)
+ task.return_value(game[0])
+ return
+
+ try:
+ grid = self.sgdb.get_grids_by_gameid(
+ [search_result[0].id], is_nsfw=False
+ )[0]
+ except (TypeError, IndexError):
+ task.return_value(game[0])
+ return
+
+ tmp_file = Gio.File.new_tmp(None)[0]
+
+ try:
+ response = requests.get(str(grid), timeout=5)
+ except requests.exceptions.RequestException:
+ task.return_value(game[0])
+ return
+
+ Path(tmp_file.get_path()).write_bytes(response.content)
+ save_cover(self.importer.parent_widget, game[0], tmp_file.get_path())
+
+ task.return_value(game[0])
+
+ def task_done(self, _task, result):
+ game_id = result.propagate_value()[1]
+ self.importer.parent_widget.update_games([game_id])
+ self.importer.queue -= 1
+ self.importer.done()