Basic SteamGridDB support
This commit is contained in:
@@ -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 <a href=\"https://www.steamgriddb.com/profile/preferences/api\">here</a>.");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,15 @@
|
||||
</key>
|
||||
<key name="itch-location" type="s">
|
||||
<default>"~/.var/app/io.itch.itch/config/itch/"</default>
|
||||
</key>
|
||||
<key name="sgdb-key" type="s">
|
||||
<default>""</default>
|
||||
</key>
|
||||
<key name="sgdb-import" type="b">
|
||||
<default>false</default>
|
||||
</key>
|
||||
<key name="sgdb-prefer" type="b">
|
||||
<default>false</default>
|
||||
</key>
|
||||
</schema>
|
||||
<schema id="hu.kramo.Cartridge.State" path="/hu/kramo/Cartridges/State/">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
69
src/utils/steamgriddb.py
Normal 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()
|
||||
Reference in New Issue
Block a user