Basic SteamGridDB support

This commit is contained in:
kramo
2023-04-04 17:35:36 +02:00
parent 1e484e03f2
commit 45be2eb165
14 changed files with 257 additions and 67 deletions

View File

@@ -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',

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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)

69
src/utils/steamgriddb.py Normal file
View File

@@ -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()