SteamGrades: Return the Steam game ID with the grade and store it.
This allows compatibility tools that use the SteamAppId environment variable to make decisions or apply fixes do their job more accurately. Also use the stored variable to provide a link to protondb through the grade label in GameInfo. The steam grades now use the orjson library to load Steam's ~6MB database faster.
This commit is contained in:
parent
07b5d381f0
commit
36ad33b8f3
|
@ -20,7 +20,7 @@ from rare.models.game import RareGame
|
||||||
from rare.shared import RareCore
|
from rare.shared import RareCore
|
||||||
from rare.shared.workers import VerifyWorker, MoveWorker
|
from rare.shared.workers import VerifyWorker, MoveWorker
|
||||||
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
|
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
|
||||||
from rare.utils.misc import format_size, icon
|
from rare.utils.misc import format_size, icon, style_hyperlink
|
||||||
from rare.widgets.image_widget import ImageWidget, ImageSize
|
from rare.widgets.image_widget import ImageWidget, ImageSize
|
||||||
from rare.widgets.side_tab import SideTabContents
|
from rare.widgets.side_tab import SideTabContents
|
||||||
from rare.components.dialogs.move_dialog import MoveDialog, is_game_dir
|
from rare.components.dialogs.move_dialog import MoveDialog, is_game_dir
|
||||||
|
@ -302,7 +302,12 @@ class GameInfo(QWidget, SideTabContents):
|
||||||
self.ui.grade.setDisabled(
|
self.ui.grade.setDisabled(
|
||||||
self.rgame.is_unreal or platform.system() == "Windows"
|
self.rgame.is_unreal or platform.system() == "Windows"
|
||||||
)
|
)
|
||||||
self.ui.grade.setText(self.steam_grade_ratings[self.rgame.steam_grade()])
|
self.ui.grade.setText(
|
||||||
|
style_hyperlink(
|
||||||
|
f"https://www.protondb.com/app/{self.rgame.steam_appid}",
|
||||||
|
self.steam_grade_ratings[self.rgame.steam_grade()]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.ui.install_button.setEnabled(
|
self.ui.install_button.setEnabled(
|
||||||
(not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle
|
(not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle
|
||||||
|
|
|
@ -19,6 +19,7 @@ from rare.shared.game_process import GameProcess
|
||||||
from rare.shared.image_manager import ImageManager
|
from rare.shared.image_manager import ImageManager
|
||||||
from rare.utils.paths import data_dir, get_rare_executable
|
from rare.utils.paths import data_dir, get_rare_executable
|
||||||
from rare.utils.steam_grades import get_rating
|
from rare.utils.steam_grades import get_rating
|
||||||
|
from rare.utils.config_helper import add_envvar, remove_envvar
|
||||||
|
|
||||||
logger = getLogger("RareGame")
|
logger = getLogger("RareGame")
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ class RareGame(RareGameSlim):
|
||||||
queue_pos: Optional[int] = None
|
queue_pos: Optional[int] = None
|
||||||
last_played: datetime = datetime.min
|
last_played: datetime = datetime.min
|
||||||
grant_date: Optional[datetime] = None
|
grant_date: Optional[datetime] = None
|
||||||
|
steam_appid: int = 0
|
||||||
steam_grade: Optional[str] = None
|
steam_grade: Optional[str] = None
|
||||||
steam_date: datetime = datetime.min
|
steam_date: datetime = datetime.min
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
@ -43,6 +45,7 @@ class RareGame(RareGameSlim):
|
||||||
queue_pos=data.get("queue_pos", None),
|
queue_pos=data.get("queue_pos", None),
|
||||||
last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else datetime.min,
|
last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else datetime.min,
|
||||||
grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else None,
|
grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else None,
|
||||||
|
steam_appid=data.get("steam_appid", 0),
|
||||||
steam_grade=data.get("steam_grade", None),
|
steam_grade=data.get("steam_grade", None),
|
||||||
steam_date=datetime.fromisoformat(data["steam_date"]) if data.get("steam_date", None) else datetime.min,
|
steam_date=datetime.fromisoformat(data["steam_date"]) if data.get("steam_date", None) else datetime.min,
|
||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
|
@ -55,6 +58,7 @@ class RareGame(RareGameSlim):
|
||||||
queue_pos=self.queue_pos,
|
queue_pos=self.queue_pos,
|
||||||
last_played=self.last_played.isoformat() if self.last_played else datetime.min,
|
last_played=self.last_played.isoformat() if self.last_played else datetime.min,
|
||||||
grant_date=self.grant_date.isoformat() if self.grant_date else None,
|
grant_date=self.grant_date.isoformat() if self.grant_date else None,
|
||||||
|
steam_appid=self.steam_appid,
|
||||||
steam_grade=self.steam_grade,
|
steam_grade=self.steam_grade,
|
||||||
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
|
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
|
||||||
tags=self.tags,
|
tags=self.tags,
|
||||||
|
@ -77,6 +81,8 @@ class RareGame(RareGameSlim):
|
||||||
self.pixmap: QPixmap = QPixmap()
|
self.pixmap: QPixmap = QPixmap()
|
||||||
self.metadata: RareGame.Metadata = RareGame.Metadata()
|
self.metadata: RareGame.Metadata = RareGame.Metadata()
|
||||||
self.__load_metadata()
|
self.__load_metadata()
|
||||||
|
if self.metadata.grant_date is None:
|
||||||
|
self.grant_date()
|
||||||
|
|
||||||
self.owned_dlcs: Set[RareGame] = set()
|
self.owned_dlcs: Set[RareGame] = set()
|
||||||
|
|
||||||
|
@ -427,18 +433,29 @@ class RareGame(RareGameSlim):
|
||||||
if platform.system() == "Windows" or self.is_unreal:
|
if platform.system() == "Windows" or self.is_unreal:
|
||||||
return "na"
|
return "na"
|
||||||
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
|
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
|
||||||
if self.metadata.steam_grade is not None and elapsed_time.days < 3:
|
if (
|
||||||
|
self.metadata.steam_grade is not None
|
||||||
|
and self.metadata.steam_appid != 0
|
||||||
|
and elapsed_time.days < 3
|
||||||
|
):
|
||||||
return self.metadata.steam_grade
|
return self.metadata.steam_grade
|
||||||
|
|
||||||
def _set_steam_grade():
|
def _set_steam_grade():
|
||||||
rating = get_rating(self.core, self.app_name)
|
appid, rating = get_rating(self.core, self.app_name)
|
||||||
self.set_steam_grade(rating)
|
self.set_steam_grade(appid, rating)
|
||||||
|
|
||||||
worker = QRunnable.create(_set_steam_grade)
|
worker = QRunnable.create(_set_steam_grade)
|
||||||
QThreadPool.globalInstance().start(worker)
|
QThreadPool.globalInstance().start(worker)
|
||||||
return "pending"
|
return "pending"
|
||||||
|
|
||||||
def set_steam_grade(self, grade: str) -> None:
|
@property
|
||||||
|
def steam_appid(self) -> Optional[int]:
|
||||||
|
return self.metadata.steam_appid
|
||||||
|
|
||||||
|
def set_steam_grade(self, appid: int, grade: str) -> None:
|
||||||
|
if appid and not self.steam_appid:
|
||||||
|
add_envvar(self.app_name, "SteamAppId", str(appid))
|
||||||
|
self.metadata.steam_appid = appid
|
||||||
self.metadata.steam_grade = grade
|
self.metadata.steam_grade = grade
|
||||||
self.metadata.steam_date = datetime.utcnow()
|
self.metadata.steam_date = datetime.utcnow()
|
||||||
self.__save_metadata()
|
self.__save_metadata()
|
||||||
|
|
|
@ -133,6 +133,7 @@ class Ui_GameInfo(object):
|
||||||
self.info_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_grade)
|
self.info_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_grade)
|
||||||
self.grade = QtWidgets.QLabel(GameInfo)
|
self.grade = QtWidgets.QLabel(GameInfo)
|
||||||
self.grade.setText("error")
|
self.grade.setText("error")
|
||||||
|
self.grade.setOpenExternalLinks(True)
|
||||||
self.grade.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
|
self.grade.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
|
||||||
self.grade.setObjectName("grade")
|
self.grade.setObjectName("grade")
|
||||||
self.info_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.grade)
|
self.info_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.grade)
|
||||||
|
|
|
@ -232,6 +232,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true">error</string>
|
<string notr="true">error</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="textInteractionFlags">
|
<property name="textInteractionFlags">
|
||||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
@ -204,3 +204,7 @@ def widget_object_name(widget: Union[QObject, wrappertype, Type], suffix: str) -
|
||||||
def elide_text(label: QLabel, text: str) -> str:
|
def elide_text(label: QLabel, text: str) -> str:
|
||||||
metrics = QFontMetrics(label.font())
|
metrics = QFontMetrics(label.font())
|
||||||
return metrics.elidedText(text, Qt.ElideRight, label.sizeHint().width())
|
return metrics.elidedText(text, Qt.ElideRight, label.sizeHint().width())
|
||||||
|
|
||||||
|
|
||||||
|
def style_hyperlink(link: str, title: str) -> str:
|
||||||
|
return "<a href='{}' style='color: #2980b9; text-decoration:none'>{}</a>".format(link, title)
|
||||||
|
|
|
@ -1,46 +1,34 @@
|
||||||
import difflib
|
import difflib
|
||||||
import orjson
|
|
||||||
import os
|
import os
|
||||||
from datetime import date
|
from datetime import datetime
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import orjson
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from rare.lgndr.core import LegendaryCore
|
from rare.lgndr.core import LegendaryCore
|
||||||
from rare.utils.paths import data_dir, cache_dir
|
from rare.utils.paths import cache_dir
|
||||||
|
|
||||||
replace_chars = ",;.:-_ "
|
replace_chars = ",;.:-_ "
|
||||||
url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
|
steamapi_url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
|
||||||
|
protondb_url = "https://www.protondb.com/api/v1/reports/summaries/"
|
||||||
|
|
||||||
__steam_ids_json = None
|
__steam_ids_json = None
|
||||||
__grades_json = None
|
__grades_json = None
|
||||||
|
__active_download = False
|
||||||
|
|
||||||
|
|
||||||
def get_rating(core: LegendaryCore, app_name: str):
|
def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
|
||||||
global __grades_json
|
game = core.get_game(app_name)
|
||||||
if __grades_json is None:
|
try:
|
||||||
if os.path.exists(p := os.path.join(data_dir(), "steam_ids.json")):
|
steam_id = get_steam_id(game.app_title)
|
||||||
grades = orjson.loads(open(p).read())
|
if not steam_id:
|
||||||
__grades_json = grades
|
raise Exception
|
||||||
else:
|
grade = get_grade(steam_id)
|
||||||
grades = {}
|
except Exception as e:
|
||||||
__grades_json = grades
|
return 0, "fail"
|
||||||
else:
|
else:
|
||||||
grades = __grades_json
|
return steam_id, grade
|
||||||
|
|
||||||
if not grades.get(app_name):
|
|
||||||
game = core.get_game(app_name)
|
|
||||||
try:
|
|
||||||
steam_id = get_steam_id(game.app_title)
|
|
||||||
grade = get_grade(steam_id)
|
|
||||||
except:
|
|
||||||
return "fail"
|
|
||||||
grades[app_name] = {"steam_id": steam_id, "grade": grade}
|
|
||||||
with open(os.path.join(data_dir(), "steam_ids.json"), "w") as f:
|
|
||||||
f.write(orjson.dumps(grades).decode("utf-8"))
|
|
||||||
return grade
|
|
||||||
else:
|
|
||||||
return grades[app_name].get("grade")
|
|
||||||
|
|
||||||
|
|
||||||
# you should iniciate the module with the game's steam code
|
# you should iniciate the module with the game's steam code
|
||||||
|
@ -48,8 +36,7 @@ def get_grade(steam_code):
|
||||||
if steam_code == 0:
|
if steam_code == 0:
|
||||||
return "fail"
|
return "fail"
|
||||||
steam_code = str(steam_code)
|
steam_code = str(steam_code)
|
||||||
url = "https://www.protondb.com/api/v1/reports/summaries/"
|
res = requests.get(f"{protondb_url}{steam_code}.json")
|
||||||
res = requests.get(f"{url}{steam_code}.json")
|
|
||||||
try:
|
try:
|
||||||
lista = orjson.loads(res.text)
|
lista = orjson.loads(res.text)
|
||||||
except orjson.JSONDecodeError:
|
except orjson.JSONDecodeError:
|
||||||
|
@ -60,13 +47,19 @@ def get_grade(steam_code):
|
||||||
|
|
||||||
def load_json() -> dict:
|
def load_json() -> dict:
|
||||||
file = os.path.join(cache_dir(), "game_list.json")
|
file = os.path.join(cache_dir(), "game_list.json")
|
||||||
if not os.path.exists(file):
|
mod_time = datetime.fromtimestamp(os.path.getmtime(file))
|
||||||
response = requests.get(url)
|
elapsed_time = abs(datetime.now() - mod_time)
|
||||||
|
global __active_download
|
||||||
|
if __active_download:
|
||||||
|
return {}
|
||||||
|
if not os.path.exists(file) or elapsed_time.days > 7:
|
||||||
|
__active_download = True
|
||||||
|
response = requests.get(steamapi_url)
|
||||||
|
__active_download = False
|
||||||
steam_ids = orjson.loads(response.text)["applist"]["apps"]
|
steam_ids = orjson.loads(response.text)["applist"]["apps"]
|
||||||
ids = {}
|
ids = {}
|
||||||
for game in steam_ids:
|
for game in steam_ids:
|
||||||
ids[game["name"]] = game["appid"]
|
ids[game["name"]] = game["appid"]
|
||||||
|
|
||||||
with open(file, "w") as f:
|
with open(file, "w") as f:
|
||||||
f.write(orjson.dumps(ids).decode("utf-8"))
|
f.write(orjson.dumps(ids).decode("utf-8"))
|
||||||
return ids
|
return ids
|
||||||
|
@ -74,51 +67,22 @@ def load_json() -> dict:
|
||||||
return orjson.loads(open(file, "r").read())
|
return orjson.loads(open(file, "r").read())
|
||||||
|
|
||||||
|
|
||||||
def get_steam_id(title: str):
|
def get_steam_id(title: str) -> int:
|
||||||
file = os.path.join(cache_dir(), "game_list.json")
|
|
||||||
# workarounds for satisfactory
|
# workarounds for satisfactory
|
||||||
|
# FIXME: This has to be made smarter.
|
||||||
title = title.replace("Early Access", "").replace("Experimental", "").strip()
|
title = title.replace("Early Access", "").replace("Experimental", "").strip()
|
||||||
|
# title = title.split(":")[0]
|
||||||
|
# title = title.split("-")[0]
|
||||||
global __steam_ids_json
|
global __steam_ids_json
|
||||||
if __steam_ids_json is None:
|
if __steam_ids_json is None:
|
||||||
if not os.path.exists(file):
|
__steam_ids_json = load_json()
|
||||||
response = requests.get(url)
|
ids = __steam_ids_json
|
||||||
ids = {}
|
|
||||||
steam_ids = orjson.loads(response.text)["applist"]["apps"]
|
|
||||||
for game in steam_ids:
|
|
||||||
ids[game["name"]] = game["appid"]
|
|
||||||
__steam_ids_json = ids
|
|
||||||
|
|
||||||
with open(file, "w") as f:
|
|
||||||
f.write(orjson.dumps(ids).decode("utf-8"))
|
|
||||||
else:
|
|
||||||
ids = orjson.loads(open(file, "r").read())
|
|
||||||
__steam_ids_json = ids
|
|
||||||
else:
|
|
||||||
ids = __steam_ids_json
|
|
||||||
|
|
||||||
if title in ids.keys():
|
if title in ids.keys():
|
||||||
steam_name = [title]
|
steam_name = [title]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
steam_name = difflib.get_close_matches(title, ids.keys(), n=1)
|
steam_name = difflib.get_close_matches(title, ids.keys(), n=1, cutoff=0.5)
|
||||||
if steam_name:
|
if steam_name:
|
||||||
return ids[steam_name[0]]
|
return ids[steam_name[0]]
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def check_time(): # this function check if it's time to update
|
|
||||||
file = os.path.join(cache_dir(), "game_list.json")
|
|
||||||
json_table = orjson.loads(open(file, "r").read())
|
|
||||||
|
|
||||||
today = date.today()
|
|
||||||
day = 0 # it controls how many days it's necessary for an update
|
|
||||||
for i in "ymd":
|
|
||||||
if i == "d":
|
|
||||||
day = 7
|
|
||||||
else:
|
|
||||||
day = 0
|
|
||||||
if int(today.strftime("%" + i)) > int(json_table["data"][i]) + day:
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
Loading…
Reference in a new issue