From 9e6e689bcdeb9c7a66cd995c16f762fd4a05255a Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:23:06 +0200 Subject: [PATCH 1/6] ImportGroup: refactor local variable and method names --- .../tabs/integrations/import_group.py | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/rare/components/tabs/integrations/import_group.py b/rare/components/tabs/integrations/import_group.py index 46722ae291..29a7c662ab 100644 --- a/rare/components/tabs/integrations/import_group.py +++ b/rare/components/tabs/integrations/import_group.py @@ -102,16 +102,16 @@ def run(self) -> None: for i, child in enumerate(folders): if not child.is_dir(): continue - result = self.__try_import(child, None) + result = self._try_import(child, None) result_list.append(result) self.signals.progress.emit(result, int(100 * i // number_of_folders)) else: - result = self.__try_import(self.path, self.app_name) + result = self._try_import(self.path, self.app_name) result_list.append(result) self.signals.progress.emit(result, 100) self.signals.result.emit(result_list) - def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: + def _try_import(self, path: Path, app_name: str = None) -> ImportedGame: result = ImportedGame(ImportResult.ERROR) result.path = str(path) if app_name or (app_name := find_app_name(self.core, str(path))): @@ -121,7 +121,7 @@ def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: platform = self.platform if platform not in self.core.get_game(app_name, update_meta=False).asset_infos: platform = "Windows" - success, message = self.__import_game(path, app_name, platform) + success, message = self._import_game(path, app_name, platform) if not success: result.result = ImportResult.FAILED result.message = message @@ -129,7 +129,7 @@ def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: result.result = ImportResult.SUCCESS return result - def __import_game(self, path: Path, app_name: str, platform: str): + def _import_game(self, path: Path, app_name: str, platform: str): cli = LegendaryCLI(self.core) status = LgndrIndirectStatus() args = LgndrImportGameArgs( @@ -158,17 +158,17 @@ def __init__(self, rcore: RareCore, parent=None): self.worker: Optional[ImportWorker] = None self.threadpool = QThreadPool.globalInstance() - self.__app_names: Dict[str, str] = None - self.__app_titles: Dict[str, str] = None - self.__install_dirs: Set[str] = None + self._app_names: Dict[str, str] = {} + self._app_titles: Dict[str, str] = {} + self._install_dirs: Set[str] = set() self.path_edit = PathEdit( path=self.core.get_default_install_dir(self.core.default_platform), file_mode=QFileDialog.FileMode.Directory, - edit_func=self.__path_edit_callback, + edit_func=self._path_edit_callback, parent=self, ) - self.path_edit.textChanged.connect(self.__path_changed) + self.path_edit.textChanged.connect(self._path_changed) self.ui.import_layout.setWidget( self.ui.import_layout.getWidgetPosition(self.ui.path_edit_label)[0], QFormLayout.ItemRole.FieldRole, @@ -177,11 +177,11 @@ def __init__(self, rcore: RareCore, parent=None): self.app_name_edit = IndicatorLineEdit( placeholder=self.tr("Use in case the app name was not found automatically"), - edit_func=self.__app_name_edit_callback, - save_func=self.__app_name_save_callback, + edit_func=self._app_name_edit_callback, + save_func=self._app_name_save_callback, parent=self, ) - self.app_name_edit.textChanged.connect(self.__app_name_changed) + self.app_name_edit.textChanged.connect(self._app_name_changed) self.ui.import_layout.setWidget( self.ui.import_layout.getWidgetPosition(self.ui.app_name_label)[0], QFormLayout.ItemRole.FieldRole, @@ -194,7 +194,7 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.import_button_label.setText("") self.ui.import_button.setEnabled(False) - self.ui.import_button.clicked.connect(lambda: self.__import(self.path_edit.text())) + self.ui.import_button.clicked.connect(lambda: self._import(self.path_edit.text())) self.button_info_stack = QStackedWidget(self) self.button_info_stack.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) @@ -210,11 +210,14 @@ def __init__(self, rcore: RareCore, parent=None): def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - self.__app_names = {rgame.app_title: rgame.app_name for rgame in self.rcore.games} - self.__app_titles = {rgame.app_name: rgame.app_title for rgame in self.rcore.games} - self.__install_dirs = {rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc} - self.app_name_edit.setCompleter(ColumnCompleter(items=self.__app_names.items())) - super().showEvent(a0) + if not self._app_names: + self._app_names = {rgame.app_title: rgame.app_name for rgame in self.rcore.games} + if not self._app_titles: + self._app_titles = {rgame.app_name: rgame.app_title for rgame in self.rcore.games} + if not self._install_dirs: + self._install_dirs = {rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc} + self.app_name_edit.setCompleter(ColumnCompleter(items=self._app_names)) + return super().showEvent(a0) def set_game(self, app_name: str): if app_name: @@ -227,17 +230,17 @@ def set_game(self, app_name: str): ) self.app_name_edit.setText(app_name) - def __path_edit_callback(self, path) -> Tuple[bool, str, int]: + def _path_edit_callback(self, path) -> Tuple[bool, str, int]: if not os.path.exists(path): return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS if os.path.exists(os.path.join(path, ".egstore")): return True, path, IndicatorReasonsCommon.VALID - elif os.path.basename(path) in self.__install_dirs: + elif os.path.basename(path) in self._install_dirs: return True, path, IndicatorReasonsCommon.VALID return False, path, IndicatorReasonsCommon.INVALID @Slot(str) - def __path_changed(self, path: str): + def _path_changed(self, path: str): self.info_label.setText("") self.ui.import_folder_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) @@ -246,18 +249,18 @@ def __path_changed(self, path: str): else: self.app_name_edit.setText("") - def __app_name_edit_callback(self, text) -> Tuple[bool, str, int]: + def _app_name_edit_callback(self, text) -> Tuple[bool, str, int]: self.app_name_edit.setInfo("") if not text: return False, text, IndicatorReasonsCommon.UNDEFINED - if text in self.__app_names.keys(): - return True, self.__app_names[text], IndicatorReasonsCommon.VALID - if text in self.__app_titles.keys(): + if text in self._app_names.keys(): + return True, self._app_names[text], IndicatorReasonsCommon.VALID + if text in self._app_titles.keys(): return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.GAME_NOT_EXISTS - def __app_name_save_callback(self, text) -> None: + def _app_name_save_callback(self, text) -> None: rgame = self.rcore.get_game(text) self.app_name_edit.setInfo(rgame.app_title) self.ui.platform_combo.clear() @@ -265,7 +268,7 @@ def __app_name_save_callback(self, text) -> None: self.ui.platform_combo.setCurrentText(rgame.default_platform) @Slot(str) - def __app_name_changed(self, app_name: str): + def _app_name_changed(self, app_name: str): self.info_label.setText("") self.ui.import_dlcs_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) @@ -303,7 +306,7 @@ def import_dlcs_changed(self, state: Qt.CheckState): ) @Slot(str) - def __import(self, path: Optional[str] = None): + def _import(self, path: Optional[str] = None): self.ui.import_button.setDisabled(True) self.info_label.setText(self.tr("Status: Importing games")) self.info_progress.setValue(0) @@ -320,12 +323,12 @@ def __import(self, path: Optional[str] = None): import_dlcs=self.ui.import_dlcs_check.isChecked(), import_force=self.ui.import_force_check.isChecked(), ) - self.worker.signals.progress.connect(self.__on_import_progress) - self.worker.signals.result.connect(self.__on_import_result) + self.worker.signals.progress.connect(self._on_import_progress) + self.worker.signals.result.connect(self._on_import_result) self.threadpool.start(self.worker) @Slot(ImportedGame, int) - def __on_import_progress(self, imported: ImportedGame, progress: int): + def _on_import_progress(self, imported: ImportedGame, progress: int): self.info_progress.setValue(progress) if imported.result == ImportResult.SUCCESS: self.rcore.get_game(imported.app_name).set_installed(True) @@ -333,7 +336,7 @@ def __on_import_progress(self, imported: ImportedGame, progress: int): logger.info(f"Import {status}: {imported.app_title}: {imported.path} ({imported.message})") @Slot(list) - def __on_import_result(self, result: List[ImportedGame]): + def _on_import_result(self, result: List[ImportedGame]): self.worker = None self.button_info_stack.setCurrentWidget(self.info_label) if len(result) == 1: From 068636db1a1c996ce7716c9bea81e7dd40f9fc00 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:25:47 +0200 Subject: [PATCH 2/6] ColumnCompleter: override setModel to accept a dictionary --- rare/widgets/indicator_edit.py | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/rare/widgets/indicator_edit.py b/rare/widgets/indicator_edit.py index e5eb998fae..09a5525b63 100644 --- a/rare/widgets/indicator_edit.py +++ b/rare/widgets/indicator_edit.py @@ -1,5 +1,4 @@ import os -from collections.abc import ItemsView from enum import Enum, IntEnum from logging import getLogger from typing import Callable, Dict, Optional, Tuple @@ -221,6 +220,9 @@ def setCompleter(self, completer: Optional[QCompleter]): completer.popup().setAlternatingRowColors(True) self.line_edit.setCompleter(completer) + def completer(self) -> QCompleter: + return self.line_edit.completer() + @property def reasons(self): return self.__reasons @@ -405,23 +407,29 @@ def __set_path(self): class ColumnCompleter(QCompleter): - def __init__(self, items: ItemsView[str, str], parent=None): + def __init__(self, items: Optional[Dict[str, str]], parent=None): super(ColumnCompleter, self).__init__(parent) - model = QStandardItemModel(len(items), 2, self) - for idx, item in enumerate(items): - app_title, app_name = item - model.setData(model.index(idx, 0), app_title) - model.setData(model.index(idx, 1), app_name) - self.setModel(model) + self._treeview = QTreeView() + self.setPopup(self._treeview) + self._treeview.setRootIsDecorated(False) + self._treeview.header().hide() + # self._treeview.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + # self._treeview.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) - treeview = QTreeView() - self.setPopup(treeview) - treeview.setRootIsDecorated(False) - treeview.header().hide() - treeview.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) - treeview.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + if items: + self.setModel(items) self.setFilterMode(Qt.MatchFlag.MatchContains) self.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) # self.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + + def setModel(self, items: Dict[str, str]): + model = QStandardItemModel(len(items), 2, self) + for idx, item in enumerate(items.items()): + app_title, app_name = item + model.setData(model.index(idx, 0), app_title) + model.setData(model.index(idx, 1), app_name) + super().setModel(model) + self._treeview.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self._treeview.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) From 91fb84b6f9e904b91cd18be4c2950a9c20a87996 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:24:11 +0200 Subject: [PATCH 3/6] SteamGrades: refactor into a class --- rare/models/game.py | 4 +- rare/shared/workers/fetch.py | 4 +- rare/utils/steam_grades.py | 202 +++++++++++++++++------------------ 3 files changed, 104 insertions(+), 106 deletions(-) diff --git a/rare/models/game.py b/rare/models/game.py index 267a2563d2..f2cbdf130c 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -20,7 +20,7 @@ from rare.shared.image_manager import ImageManager from rare.utils import config_helper as config from rare.utils.paths import compat_shaders_dir, data_dir, get_rare_executable -from rare.utils.steam_grades import get_rating +from rare.utils.steam_grades import SteamGrades from rare.utils.workarounds import apply_workarounds @@ -496,7 +496,7 @@ def get_steam_grade(self) -> str: return self.metadata.steam_grade def set_steam_grade(self) -> None: - appid, grade = get_rating(self.core, self.app_name, self.steam_appid) + appid, grade = SteamGrades().get_rating(self.core, self.app_name, self.steam_appid) if appid and self.steam_appid is None: self.steam_appid = appid self.metadata.steam_grade = grade diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py index b293face98..0f2f4ab97b 100644 --- a/rare/shared/workers/fetch.py +++ b/rare/shared/workers/fetch.py @@ -7,8 +7,8 @@ from rare.lgndr.core import LegendaryCore from rare.models.settings import RareAppSettings, app_settings -from rare.utils import steam_grades from rare.utils.metrics import timelogger +from rare.utils.steam_grades import SteamGrades from .worker import Worker @@ -39,7 +39,7 @@ def run_real(self): with timelogger(self.logger, "Request Steam AppIds"): try: with timelogger(self.logger, "steam grades load: "): - steam_grades.load_steam_appids() + SteamGrades().load_steam_appids() except Exception as e: self.logger.warning(e) self.signals.result.emit((), FetchWorker.Result.STEAMAPPIDS) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 6eed0b8664..499b444373 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -14,16 +14,6 @@ logger = getLogger("SteamGrades") -replace_chars = ",;.:-_ " -steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" -protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" - -__steam_appids: Dict = None -__steam_titles: Dict = None -__steam_appids_version: int = 3 -__active_download: bool = False - - class ProtondbRatings(int, Enum): # internal PENDING = ("pending", -2) @@ -49,95 +39,103 @@ def __int__(self): return self._value_ -def get_rating(core: LegendaryCore, app_name: str, steam_appid: int = None) -> Tuple[int, str]: - game = core.get_game(app_name) - try: - if steam_appid is None: - steam_appid = get_steam_id(game.app_title) - if not steam_appid: - raise RuntimeError - grade = get_grade(steam_appid) - except Exception as e: - logger.debug(e) - logger.error("Failed to get ProtonDB rating for %s", game.app_title) - return 0, "fail" - else: - return steam_appid, grade - - -# you should initiate the module with the game's steam code -def get_grade(steam_code): - if steam_code == 0: - return "fail" - steam_code = str(steam_code) - res = requests.get(f"{protondb_url}{steam_code}.json") - try: - app = orjson.loads(res.text) - except orjson.JSONDecodeError as e: - logger.debug(e) - logger.error("Failed to get ProtonDB response for %s", steam_code) - return "fail" - - return app.get("tier", "fail") - - -def download_steam_appids() -> bytes: - global __active_download - if __active_download: - return b"" - __active_download = True - resp = requests.get(steamids_url) - __active_download = False - return resp.content - - -def load_steam_appids() -> Tuple[Dict, Dict]: - global __steam_appids, __steam_titles - - if __steam_appids and __steam_titles: - return __steam_appids, __steam_titles - - file = os.path.join(cache_dir(), "steam_appids.json") - version = __steam_appids_version - elapsed_days = 0 - - if os.path.exists(file): - mod_time = datetime.fromtimestamp(os.path.getmtime(file)) - elapsed_days = abs(datetime.now() - mod_time).days - json = orjson.loads(open(file, "r").read()) - version = json.get("version", 0) - if version >= __steam_appids_version: - __steam_appids = json["games"] - - if not os.path.exists(file) or elapsed_days > 3 or version < __steam_appids_version: - if content := download_steam_appids(): - text = lzma.decompress(content).decode("utf-8") - with open(file, "w", encoding="utf-8") as f: - f.write(text) - json = orjson.loads(text) - __steam_appids = json["games"] - - __steam_titles = {v: k for k, v in __steam_appids.items()} - - return __steam_appids, __steam_titles - - -def get_steam_id(title: str) -> int: - # workarounds for satisfactory - # FIXME: This has to be made smarter. - title = title.replace("Early Access", "").replace("Experimental", "").strip() - # title = title.split(":")[0] - # title = title.split("-")[0] - global __steam_appids, __steam_titles - if not __steam_appids or not __steam_titles: - __steam_appids, __steam_titles = load_steam_appids() - - if title in __steam_titles.keys(): - steam_name = [title] - else: - steam_name = difflib.get_close_matches(title, __steam_appids.keys(), n=1, cutoff=0.5) - - if steam_name: - return __steam_appids[steam_name[0]] - else: - return 0 +class SteamGrades: + __steam_appids: Dict = None + __steam_appids_version: int = 3 + __active_download: bool = False + + def __init__(self): + self.replace_chars = ",;.:-_ " + self.steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" + self.protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" + + def __download_steam_appids(self) -> bytes: + if SteamGrades.__active_download: + return b"" + SteamGrades.__active_download = True + resp = requests.get(self.steamids_url) + SteamGrades.__active_download = False + return resp.content + + def load_steam_appids(self) -> Dict: + + if SteamGrades.__steam_appids: + return SteamGrades.__steam_appids + + file = os.path.join(cache_dir(), "steam_appids.json") + version = SteamGrades.__steam_appids_version + elapsed_days = 0 + + if os.path.exists(file): + mod_time = datetime.fromtimestamp(os.path.getmtime(file)) + elapsed_days = abs(datetime.now() - mod_time).days + json = orjson.loads(open(file, "r").read()) + version = json.get("version", 0) + if version >= SteamGrades.__steam_appids_version: + SteamGrades.__steam_appids = json["games"] + + if not os.path.exists(file) or elapsed_days > 3 or version < SteamGrades.__steam_appids_version: + if content := self.__download_steam_appids(): + text = lzma.decompress(content).decode("utf-8") + with open(file, "w", encoding="utf-8") as f: + f.write(text) + json = orjson.loads(text) + SteamGrades.__steam_appids = json["games"] + + return SteamGrades.__steam_appids + + @property + def steam_appids(self) -> Dict: + if not SteamGrades.__steam_appids: + SteamGrades.__steam_appids = self.load_steam_appids() + return SteamGrades.__steam_appids + + @property + def steam_titles(self) -> Dict: + return {v: k for k, v in self.steam_appids.items()} + + def get_steam_id(self, title: str) -> int: + # workarounds for satisfactory + # FIXME: This has to be made smarter. + title = title.replace("Early Access", "").replace("Experimental", "").strip() + # title = title.split(":")[0] + # title = title.split("-")[0] + + if title in self.steam_titles.keys(): + steam_name = [title] + else: + steam_name = difflib.get_close_matches(title, self.steam_appids.keys(), n=1, cutoff=0.5) + + if steam_name: + return self.steam_appids[steam_name[0]] + else: + return 0 + + def get_grade(self, steam_code): + if steam_code == 0: + return "fail" + steam_code = str(steam_code) + res = requests.get(f"{self.protondb_url}/{steam_code}.json") + try: + app = orjson.loads(res.text) + except orjson.JSONDecodeError as e: + logger.error(repr(e)) + logger.error("Failed to get ProtonDB response for %s", steam_code) + return "fail" + + return app.get("tier", "fail") + + def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: int = None) -> Tuple[int, str]: + game = core.get_game(app_name) + try: + if steam_appid is None: + steam_appid = self.get_steam_id(game.app_title) + if not steam_appid: + raise RuntimeError + grade = self.get_grade(steam_appid) + except Exception as e: + logger.error(repr(e)) + logger.error("Failed to get ProtonDB rating for %s", game.app_title) + return 0, "fail" + else: + return steam_appid, grade From dba8d065a61ab40a1889e00626bb47ce4160a65a Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:29:06 +0200 Subject: [PATCH 4/6] UpdateWidget: emit InstallOptionsModel correctly --- rare/components/tabs/downloads/widgets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rare/components/tabs/downloads/widgets.py b/rare/components/tabs/downloads/widgets.py index 7f35e0f221..00f67f0987 100644 --- a/rare/components/tabs/downloads/widgets.py +++ b/rare/components/tabs/downloads/widgets.py @@ -96,7 +96,13 @@ def __init__(self, imgmgr: ImageManager, game: Game, igame: InstalledGame, paren def update_game(self, auto: bool): self.ui.update_button.setDisabled(True) self.ui.settings_button.setDisabled(True) - self.enqueue.emit(InstallOptionsModel(app_name=self.game.app_name, update=True, silent=auto)) # True if settings + self.enqueue.emit(InstallOptionsModel( + app_name=self.game.app_name, + base_path=self.igame.install_path, + platform=self.igame.platform, + update=True, + silent=auto, # True if settings + )) def set_enabled(self, enabled: bool): self.ui.update_button.setEnabled(enabled) From c38a2870936119d172e93b0bf58ad8118175c5ed Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:46:35 +0200 Subject: [PATCH 5/6] RunnerSettings: reduce memory usage caused by multiple copies of steam appids in the completer The completer needs to use it's own copy of steam app ids which increases the RAM usage by about 150MB. Using a bespoke item model doesn't work, as altering the view of an ordered dictionary is a not efficient enough, causing the UI to lock up. For that reason, we generate a reduce list of completions based on the first three letters of the application's name offer only those. The completer is replaced when the the view is shown and destroyed when the view get hidden. --- .../components/tabs/library/details/compat.py | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/rare/components/tabs/library/details/compat.py b/rare/components/tabs/library/details/compat.py index dcbfe6ce9c..b406cb20ad 100644 --- a/rare/components/tabs/library/details/compat.py +++ b/rare/components/tabs/library/details/compat.py @@ -3,7 +3,7 @@ from typing import Tuple from PySide6.QtCore import QSignalBlocker, Qt, Slot -from PySide6.QtGui import QShowEvent +from PySide6.QtGui import QHideEvent, QShowEvent from rare.components.tabs.settings.compat import CompatSettingsBase from rare.components.tabs.settings.widgets.overlay import ( @@ -17,8 +17,8 @@ from rare.models.settings import RareAppSettings, app_settings from rare.shared import RareCore from rare.utils import config_helper as config -from rare.utils import steam_grades from rare.utils.paths import compat_shaders_dir, proton_compat_dir, wine_prefix_dir +from rare.utils.steam_grades import SteamGrades from rare.widgets.indicator_edit import ( ColumnCompleter, IndicatorLineEdit, @@ -74,36 +74,44 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): save_func=self.__steam_appid_save_callback, parent=self, ) - self.__steam_appids, self.__steam_titles = steam_grades.load_steam_appids() - self.steam_appid_edit.setCompleter(ColumnCompleter(items=self.__steam_appids.items())) self.form_layout.addRow(self.tr("Steam AppID"), self.steam_appid_edit) - def showEvent(self, a0: QShowEvent): - if a0.spontaneous(): - return super().showEvent(a0) + self.__grades = SteamGrades() + + def showEvent(self, e: QShowEvent): + if e.spontaneous(): + return super().showEvent(e) _ = QSignalBlocker(self.shader_cache_check) is_local_cache_enabled = self.settings.get_with_global(app_settings.local_shader_cache, self.rgame.app_name) has_local_cache_path = bool(config.get_envvar(self.app_name, "STEAM_COMPAT_SHADER_PATH", False)) self.shader_cache_check.setChecked(is_local_cache_enabled or has_local_cache_path) self.shader_cache_check.setChecked(is_local_cache_enabled or has_local_cache_path) _ = QSignalBlocker(self.steam_appid_edit) + items = {k: v for k, v in self.__grades.steam_appids.items() if self.rgame.app_title.lower()[0:4] in k.lower()[0:4]} + self.steam_appid_edit.setCompleter(ColumnCompleter(items=items)) self.steam_appid_edit.setText(self.rgame.steam_appid if self.rgame.steam_appid else "") - self.steam_appid_edit.setInfo(self.__steam_titles.get(self.rgame.steam_appid, "")) - return super().showEvent(a0) + self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(self.rgame.steam_appid, "")) + return super().showEvent(e) + + def hideEvent(self, e: QHideEvent): + if e.spontaneous(): + return super().hideEvent(e) + self.steam_appid_edit.setCompleter(None) + return super().hideEvent(e) def __steam_appid_edit_callback(self, text: str) -> Tuple[bool, str, int]: self.steam_appid_edit.setInfo("") - if not text: + if not text or len(text) < 3: return True, text, IndicatorReasonsCommon.UNDEFINED - if text in self.__steam_appids.keys(): - return True, self.__steam_appids[text], IndicatorReasonsCommon.VALID - if text in self.__steam_titles.keys(): + if text in self.__grades.steam_appids.keys(): + return True, self.__grades.steam_appids[text], IndicatorReasonsCommon.VALID + if text in self.__grades.steam_titles.keys(): return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.GAME_NOT_EXISTS def __steam_appid_save_callback(self, text: str) -> None: - self.steam_appid_edit.setInfo(self.__steam_titles.get(text, "")) + self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(text, "")) if text == self.rgame.steam_appid: return self.rgame.steam_appid = text From 63af66e3fc35f275fac11debb6a8062dffd6de9d Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:11:26 +0200 Subject: [PATCH 6/6] SteamGrades: update typing --- rare/shared/workers/fetch.py | 3 +-- rare/utils/steam_grades.py | 39 ++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py index 0f2f4ab97b..4393cabad0 100644 --- a/rare/shared/workers/fetch.py +++ b/rare/shared/workers/fetch.py @@ -38,8 +38,7 @@ def run_real(self): self.signals.progress.emit(0, self.signals.tr("Updating Steam AppIds")) with timelogger(self.logger, "Request Steam AppIds"): try: - with timelogger(self.logger, "steam grades load: "): - SteamGrades().load_steam_appids() + SteamGrades().load_steam_appids() except Exception as e: self.logger.warning(e) self.signals.result.emit((), FetchWorker.Result.STEAMAPPIDS) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 499b444373..7a0dd18a76 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -40,20 +40,21 @@ def __int__(self): class SteamGrades: - __steam_appids: Dict = None + __steam_appids: Dict[str, str] = {} __steam_appids_version: int = 3 __active_download: bool = False + __replace_chars = ",;.:-_ " + __steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" + __protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" def __init__(self): - self.replace_chars = ",;.:-_ " - self.steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" - self.protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" + pass - def __download_steam_appids(self) -> bytes: + def _download_steam_appids(self) -> bytes: if SteamGrades.__active_download: return b"" SteamGrades.__active_download = True - resp = requests.get(self.steamids_url) + resp = requests.get(self.__steamids_url) SteamGrades.__active_download = False return resp.content @@ -75,7 +76,7 @@ def load_steam_appids(self) -> Dict: SteamGrades.__steam_appids = json["games"] if not os.path.exists(file) or elapsed_days > 3 or version < SteamGrades.__steam_appids_version: - if content := self.__download_steam_appids(): + if content := self._download_steam_appids(): text = lzma.decompress(content).decode("utf-8") with open(file, "w", encoding="utf-8") as f: f.write(text) @@ -85,7 +86,7 @@ def load_steam_appids(self) -> Dict: return SteamGrades.__steam_appids @property - def steam_appids(self) -> Dict: + def steam_appids(self) -> Dict[str, str]: if not SteamGrades.__steam_appids: SteamGrades.__steam_appids = self.load_steam_appids() return SteamGrades.__steam_appids @@ -94,7 +95,7 @@ def steam_appids(self) -> Dict: def steam_titles(self) -> Dict: return {v: k for k, v in self.steam_appids.items()} - def get_steam_id(self, title: str) -> int: + def _get_steam_appid(self, title: str) -> str: # workarounds for satisfactory # FIXME: This has to be made smarter. title = title.replace("Early Access", "").replace("Experimental", "").strip() @@ -109,33 +110,33 @@ def get_steam_id(self, title: str) -> int: if steam_name: return self.steam_appids[steam_name[0]] else: - return 0 + return "0" - def get_grade(self, steam_code): - if steam_code == 0: + def _get_grade(self, steam_appid: str): + if steam_appid == "0": return "fail" - steam_code = str(steam_code) - res = requests.get(f"{self.protondb_url}/{steam_code}.json") + steam_appid = str(steam_appid) + res = requests.get(f"{self.__protondb_url}/{steam_appid}.json") try: app = orjson.loads(res.text) except orjson.JSONDecodeError as e: logger.error(repr(e)) - logger.error("Failed to get ProtonDB response for %s", steam_code) + logger.error("Failed to get ProtonDB response for %s", steam_appid) return "fail" return app.get("tier", "fail") - def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: int = None) -> Tuple[int, str]: + def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: str = None) -> Tuple[str, str]: game = core.get_game(app_name) try: if steam_appid is None: - steam_appid = self.get_steam_id(game.app_title) + steam_appid = self._get_steam_appid(game.app_title) if not steam_appid: raise RuntimeError - grade = self.get_grade(steam_appid) + grade = self._get_grade(steam_appid) except Exception as e: logger.error(repr(e)) logger.error("Failed to get ProtonDB rating for %s", game.app_title) - return 0, "fail" + return "0", "fail" else: return steam_appid, grade