diff --git a/rare/__main__.py b/rare/__main__.py index a88e67ea..cbb387f8 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -7,9 +7,15 @@ from rare.utils import singleton def main(): parser = ArgumentParser() - parser.add_argument("-V", "--version", action="store_true") - parser.add_argument("-S", "--silent", action="store_true") - parser.add_argument("--offline", action="store_true") + parser.add_argument("-V", "--version", action="store_true", help="Shows version and exits") + parser.add_argument("-S", "--silent", action="store_true", + help="Launch Rare in background. Open it from System Tray Icon") + parser.add_argument("--offline", action="store_true", help="Launch Rare in offline mode") + if os.name != "nt": + parser.add_argument("--disable-protondb", action="store_true", dest="disable_protondb", + help="Do not download and check data from ProtonDB. Disable it, if you don't need grades") + parser.add_argument("--enable-protondb", action="store_true", dest="enable_protondb", + help="Enable ProtonDB data, after disabled") subparsers = parser.add_subparsers(title="Commands", dest="subparser") launch_parser = subparsers.add_parser("launch") diff --git a/rare/app.py b/rare/app.py index 4f6c65f1..5acebeb7 100644 --- a/rare/app.py +++ b/rare/app.py @@ -60,6 +60,11 @@ class App(QApplication): self.setOrganizationName("Rare") settings = QSettings() + if args.disable_protondb: + settings.setValue("disable_protondb", True) + if args.enable_protondb: + settings.remove("disable_protondb") + # Translator self.translator = QTranslator() lang = settings.value("language", get_lang(), type=str) diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index d5c2fa89..6842c851 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -1,31 +1,69 @@ +import json +import os from logging import getLogger -from PyQt5.QtCore import QThread, pyqtSignal -from PyQt5.QtWidgets import QDialog, QLabel, QProgressBar, QVBoxLayout +from PyQt5.QtCore import QThread, pyqtSignal, QSettings +from PyQt5.QtWidgets import QDialog from requests.exceptions import ConnectionError from custom_legendary.core import LegendaryCore from rare.components.dialogs.login import LoginDialog -from rare.utils.utils import download_images, check_grades +from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog +from rare.utils import steam_grades +from rare.utils.utils import download_images logger = getLogger("Login") -class LaunchThread(QThread): +class ImageThread(QThread): download_progess = pyqtSignal(int) - action = pyqtSignal(str) def __init__(self, core: LegendaryCore, parent=None): - super(LaunchThread, self).__init__(parent) + super(ImageThread, self).__init__(parent) self.core = core def run(self): - self.action.emit("Login") - self.action.emit(self.tr("Downloading Images")) download_images(self.download_progess, self.core) - #self.action.emit(self.tr("Read data from ProtonDB")) - #check_grades(self.core, self.download_progess) - self.action.emit("finish") + self.download_progess.emit(100) + + +class SteamThread(QThread): + progress = pyqtSignal(int) + action = pyqtSignal(str) + + def __init__(self, core: LegendaryCore, parent): + super(SteamThread, self).__init__(parent) + self.core = core + + def run(self) -> None: + gamelist = self.core.get_game_list(True) + if not os.path.exists(os.path.expanduser("~/.cache/rare/game_list.json")): + self.action.emit(self.tr("Getting data from ProtonDB")) + steam_grades.upgrade_all([(i.app_title, i.app_name) for i in gamelist], self.progress) + self.progress.emit(99) + + self.action.emit(self.tr("Checking Games for data")) + grades = json.load(open(os.path.expanduser("~/.cache/rare/game_list.json"))) + ids = json.load(open(os.path.expanduser("~/.cache/rare/steam_ids.json"))) + for game in gamelist: + if not grades.get(game.app_name): + steam_id = steam_grades.get_steam_id(game.app_title, ids) + grade = steam_grades.get_grade(steam_id) + grades[game.app_name] = { + "steam_id": steam_id, + "grade": grade + } + if not grades[game.app_name].get("steam_id"): + grades[game.app_name]["steam_id"] = steam_grades.get_steam_id(game.app_title) + if not grades[game.app_name].get("grade"): + grades[game.app_name]["grade"] = steam_grades.get_grade(game.app_title) + + with open(os.path.expanduser("~/.cache/rare/game_list.json"), "w") as f: + f.write(json.dumps(grades)) + f.close() + + self.action.emit("Ready") + self.progress.emit(100) class LoginThread(QThread): @@ -52,11 +90,17 @@ class LoginThread(QThread): self.start_app.emit(True) -class LaunchDialog(QDialog): +class LaunchDialog(QDialog, Ui_LaunchDialog): start_app = pyqtSignal(bool) + finished = False def __init__(self, core: LegendaryCore, offline): super(LaunchDialog, self).__init__() + self.setupUi(self) + if os.name == "nt": + self.finished = True + self.steam_info.setVisible(False) + self.steam_prog_bar.setVisible(False) self.core = core if not offline: self.login_thread = LoginThread(core) @@ -64,18 +108,7 @@ class LaunchDialog(QDialog): self.login_thread.start_app.connect(self.launch) self.login_thread.start() - self.title = QLabel("

" + self.tr("Launching Rare") + "

") - self.info_pb = QProgressBar() - self.info_text = QLabel(self.tr("Logging in")) - self.layout = QVBoxLayout() - - self.layout.addWidget(self.title) - self.layout.addWidget(self.info_pb) - self.layout.addWidget(self.info_text) - - self.setLayout(self.layout) - - if offline: + else: self.launch(offline) def login(self): @@ -88,20 +121,40 @@ class LaunchDialog(QDialog): def launch(self, offline=False): # self.core = core + if not os.path.exists(p := os.path.expanduser("~/.cache/rare/images")): + os.makedirs(p) self.offline = offline - self.info_text.setText(self.tr("Downloading Images")) - self.thread = LaunchThread(self.core, self) - self.thread.download_progess.connect(self.update_pb) - self.thread.action.connect(self.info) - self.thread.start() - def update_pb(self, i: int): - self.info_pb.setValue(i) + if not offline: - def info(self, text: str): - if text == "finish": - self.info_text.setText(self.tr("Starting...")) - self.info_pb.setValue(100) + self.image_info.setText(self.tr("Downloading Images")) + self.img_thread = ImageThread(self.core, self) + self.img_thread.download_progess.connect(self.update_image_progbar) + self.img_thread.finished.connect(self.finish) + self.img_thread.start() + # not disabled and not windows + if (not QSettings().value("disable_protondb", False, bool)) and (not os.name == "nt"): + self.steam_thread = SteamThread(self.core, self) + self.steam_thread.progress.connect(self.update_steam_prog_bar) + self.steam_thread.action.connect(lambda x: self.steam_info.setText(x)) + self.steam_thread.finished.connect(self.finish) + self.steam_thread.start() + else: + self.finished = True + self.steam_info.setVisible(False) + self.steam_prog_bar.setVisible(False) + + def update_steam_prog_bar(self, value): + self.steam_prog_bar.setValue(value) + + def update_image_progbar(self, i: int): + self.image_prog_bar.setValue(i) + + def finish(self): + if self.finished: + self.image_info.setText(self.tr("Starting...")) + self.image_prog_bar.setValue(100) + self.steam_prog_bar.setValue(100) self.start_app.emit(self.offline) else: - self.info_text.setText(text) + self.finished = True diff --git a/rare/components/tabs/games/game_info/__init__.py b/rare/components/tabs/games/game_info/__init__.py index 626dce74..e658e1c0 100644 --- a/rare/components/tabs/games/game_info/__init__.py +++ b/rare/components/tabs/games/game_info/__init__.py @@ -63,8 +63,6 @@ class GameInfo(QWidget, Ui_GameInfo): verify_game = pyqtSignal(str) verify_threads = {} - grade_table = json.load(open(os.path.expanduser("~/.cache/rare/game_list.json"))) - def __init__(self, core: LegendaryCore, parent): super(GameInfo, self).__init__(parent=parent) self.setupUi(self) @@ -74,7 +72,10 @@ class GameInfo(QWidget, Ui_GameInfo): "bronze": self.tr("Bronze"), "fail": self.tr("Could not get grade from ProtonDB"), "pending": "Not enough reports"} - + if os.path.exists(p := os.path.expanduser("~/.cache/rare/game_list.json")): + self.grade_table = json.load(open(p)) + else: + self.grade_table = {} self.widget = QWidget() self.core = core if os.name == "nt": @@ -155,7 +156,7 @@ class GameInfo(QWidget, Ui_GameInfo): self.install_size.setText(get_size(self.igame.install_size)) self.install_path.setText(self.igame.install_path) - if os.name != "nt": + if os.name != "nt" and self.grade_table: try: grade = self.ratings.get(self.grade_table[app_name].get("grade")) except KeyError: diff --git a/rare/components/tabs/games/game_info/uninstalled_info.py b/rare/components/tabs/games/game_info/uninstalled_info.py index 15ff62e1..fcb5b781 100644 --- a/rare/components/tabs/games/game_info/uninstalled_info.py +++ b/rare/components/tabs/games/game_info/uninstalled_info.py @@ -56,7 +56,11 @@ class UninstalledInfo(QWidget): def __init__(self, core: LegendaryCore, parent): super(UninstalledInfo, self).__init__(parent=parent) self.layout = QVBoxLayout() - self.grade_table = json.load(open(os.path.expanduser("~/.cache/rare/game_list.json"))) + + if os.path.exists(p := os.path.expanduser("~/.cache/rare/game_list.json")): + self.grade_table = json.load(open(p)) + else: + self.grade_table = {} self.ratings = {"platinum": self.tr("Platinum"), "gold": self.tr("Gold"), @@ -82,9 +86,9 @@ class UninstalledInfo(QWidget): self.app_name = QLabel("Error") self.right_layout.addWidget(self.app_name) - - self.rating = QLabel("Rating: Error") - self.right_layout.addWidget(self.rating) + if os.name != "nt": + self.rating = QLabel("Rating: Error") + self.right_layout.addWidget(self.rating) self.install_button = QPushButton(self.tr("Install")) self.install_button.setFixedWidth(300) @@ -124,11 +128,12 @@ class UninstalledInfo(QWidget): self.image.setPixmap(pixmap) self.version.setText(self.game.asset_info.build_version) - try: - rating = self.grade_table[app_name]["grade"] - except KeyError: - rating = "fail" - if rating not in ["fail", "pending"]: - self.rating.setText(self.tr("Rating from ProtonDB: ") + self.ratings[rating]) - else: - self.rating.setText(self.ratings[rating]) + if self.grade_table and (not os.name == "nt"): + try: + rating = self.grade_table[app_name]["grade"] + except KeyError: + rating = "fail" + if rating not in ["fail", "pending"]: + self.rating.setText(self.tr("Rating from ProtonDB: ") + self.ratings[rating]) + else: + self.rating.setText(self.ratings[rating]) diff --git a/rare/utils/id.py b/rare/utils/id.py deleted file mode 100644 index 65bd69ea..00000000 --- a/rare/utils/id.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -import os - -import requests -import json -from datetime import date - - -replace_chars = ",;.:-_ " - -file = os.path.expanduser("~/.cache/rare/game_list.json") -url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/" - - -def get_id(game_name): - global file - - if check_time() == 1: - upgrade_content() - - text = open(file, 'r') - game_list = json.load(text) - - return game_list[game_name.lower()] - - -def download_ids(): - response = requests.get(url) - with open(os.path.expanduser("~/.cache/rare/steam_ids.json"), "w") as f: - f.write(response.text) - f.close() - - -def upgrade_content(games: list, status_signal): # this function uploads the ids database, aka game_list.json - global url - global file - response = requests.get(url) - with open(os.path.expanduser("~/.cache/rare/steam_ids.json"), "w") as f: - f.write(response.text) - f.close() - - content = json.loads(response.text) - game_list = {} # {CrabEA: {id: 1234, grade: platinum}, ..} - - steam_games = {} - for i in content["applist"]["apps"]: - name: str = i["name"].lower() - for c in replace_chars: - name = name.replace(c, "") - name = name.encode("ascii", "ignore").decode("ascii", "ignore") - steam_games[name] = i["appid"] - - for i in games: - if i.app_title.lower() in steam_games.keys(): - game_list[i.app_name] = {} - game_list[i.app_name]["id"] = steam_games[i.app_title.lower()] - continue - else: - app_title = i.app_title.lower() - app_title = app_title.encode("ascii", "ignore").decode("ascii", "ignore") - for c in replace_chars: - app_title = app_title.replace(c, "") - if app_title in steam_games.keys(): - game_list[i.app_name] = {} - game_list[i.app_name]["id"] = steam_games[app_title] - else: - for game in steam_games: - if app_title.startswith(game): - game_list[i.app_name] = {} - game_list[i.app_name]["id"] = steam_games[game] - - for i, game in enumerate(game_list): - try: - grade = get_grade(game_list[game]["id"]) - except json.decoder.JSONDecodeError as e: - logging.error(str(e)) - game_list[game]["grade"] = "fail" - print(game) # debug - else: - game_list[game]["grade"] = grade - status_signal.emit(50 + i/len(game_list)*50) - - # print(game_list) - - # for game in content['applist']['apps']: - # game_list[game['name'].lower()] = game['appid'] - - # uploding date on json - today = date.today() - game_list['data'] = {} - for i in "ymd": - game_list["data"][i] = today.strftime('%' + i) - - table = open(file, 'w') - - json.dump(game_list, table) - table.close() - - -def check_time(): # this function check if it's time to update - global file - text = open(file, 'r') - json_table = json.load(text) - text.close() - - 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 - - -# you should iniciate the module with the game's steam code -def get_grade(steam_code): - steam_code = str(steam_code) - url = 'https://www.protondb.com/api/v1/reports/summaries/' - res = requests.get(url + steam_code + '.json') - text = res.text - lista = json.loads(text) - # print(lista['tier']) # just for debug pourpouses!!! - - return lista['tier'] - - -def id(game_name): - return get_grade(get_id(game_name)) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py new file mode 100644 index 00000000..49feda5a --- /dev/null +++ b/rare/utils/steam_grades.py @@ -0,0 +1,116 @@ +import difflib +import json +import os +from datetime import date + +import requests +from PyQt5.QtCore import pyqtSignal + +replace_chars = ",;.:-_ " + +file = os.path.expanduser("~/.cache/rare/game_list.json") +url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/" + + +# you should iniciate the module with the game's steam code +def get_grade(steam_code): + if steam_code == 0: + return "fail" + steam_code = str(steam_code) + url = 'https://www.protondb.com/api/v1/reports/summaries/' + res = requests.get(url + steam_code + '.json') + try: + lista = json.loads(res.text) + except json.decoder.JSONDecodeError: + return "fail" + + return lista['tier'] + + +def load_json() -> dict: + if not os.path.exists(p := os.path.expanduser("~/.cache/rare/steam_ids.json")): + response = requests.get(url) + steam_ids = json.loads(response.text)["applist"]["apps"] + ids = {} + for game in steam_ids: + ids[game["name"]] = game["appid"] + + with open(os.path.expanduser(p), "w") as f: + f.write(json.dumps(ids)) + f.close() + return ids + else: + return json.loads(open(os.path.expanduser("~/.cache/rare/steam_ids.json"), "r").read()) + + +def upgrade_all(games, progress: pyqtSignal = None): + ids = load_json() + data = {} + for i, (title, app_name) in enumerate(games): + title = title.replace("Early Access", "").replace("Experimental", "").strip() + data[app_name] = {} + + steam_id = get_steam_id(title, ids) + + data[app_name] = { + "steam_id": steam_id, + "grade": get_grade(steam_id)} + + if progress: + progress.emit(int(i / len(games) * 100)) + + with open(os.path.expanduser("~/.cache/rare/game_list.json"), "w") as f: + f.write(json.dumps(data)) + f.close() + + +def get_steam_id(title: str, json_data=None): + title = title.replace("Early Access", "").replace("Experimental", "").strip() + if not json_data: + if not os.path.exists(p := os.path.expanduser("~/.cache/rare/steam_ids.json")): + response = requests.get(url) + ids = {} + steam_ids = json.loads(response.text)["applist"]["apps"] + for game in steam_ids: + ids[game["name"]] = game["appid"] + + with open(os.path.expanduser(p), "w") as f: + f.write(json.dumps(steam_ids)) + f.close() + else: + ids = json.loads(open(os.path.expanduser("~/.cache/rare/steam_ids.json"), "r").read()) + else: + ids = json_data + steam_name = difflib.get_close_matches(title, ids.keys(), n=1) + if steam_name: + return ids[steam_name[0]] + else: + return 0 + # print(x) + + # for game in steam_ids: + # num = difflib.SequenceMatcher(None, game["name"], title).ratio() + # if num > most_similar[2] and num > 0.5: + # most_similar = (game["appid"], game["name"], num) + # print(time.time()-t) + # name = difflib.get_close_matches(steam_ids.keys(), title) + # return most_similar + + +def check_time(): # this function check if it's time to update + global file + text = open(file, 'r') + json_table = json.load(text) + text.close() + + 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 diff --git a/rare/utils/utils.py b/rare/utils/utils.py index e2cf9c9f..5c2a5fbc 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -8,15 +8,12 @@ import requests from PIL import Image, UnidentifiedImageError from PyQt5.QtCore import pyqtSignal, QLocale, QSettings from PyQt5.QtGui import QPalette, QColor -from rare import style_path - -from rare.utils.id import upgrade_content, check_time, download_ids # Windows if os.name == "nt": from win32com.client import Dispatch -from rare import lang_path, __version__, style_path +from rare import lang_path, style_path from custom_legendary.core import LegendaryCore logger = getLogger("Utils") @@ -42,22 +39,7 @@ def download_images(signal: pyqtSignal, core: LegendaryCore): except json.decoder.JSONDecodeError: shutil.rmtree(f"{IMAGE_DIR}/{game.app_name}") download_image(game) - signal.emit(i/len(game_list)*50) - - -def check_grades(core: LegendaryCore, signal): - games = core.get_game_list(True) - # upgrade_content(games, signal) - if not os.path.exists("~/.cache/rare/steam_ids.json"): - download_ids() - - game_table = json.loads(open(os.path.expanduser("~/.cache/rare/game_list.json")).read()) - for game in games: - if game.app_name not in game_table.keys(): - # TODO: Find id from method; Not upgrade all - game_table[game.app_name] = {"grade": "pending", "id": "1234"} - - json.dump(game_table, open(os.path.expanduser("~/.cache/rare/game_list.json"), "w")) + signal.emit(i / len(game_list) * 100) def download_image(game, force=False): @@ -88,7 +70,7 @@ def download_image(game, force=False): f.write(requests.get(url).content) try: img = Image.open(f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png") - img = img.resize((200, int(200*4/3))) + img = img.resize((200, int(200 * 4 / 3))) img.save(f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png") except UnidentifiedImageError as e: logger.warning(e) @@ -103,7 +85,7 @@ def download_image(game, force=False): bg = Image.open(f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png") uninstalledArt = bg.convert('L') - uninstalledArt = uninstalledArt.resize((200, int(200*4/3))) + uninstalledArt = uninstalledArt.resize((200, int(200 * 4 / 3))) uninstalledArt.save(f'{IMAGE_DIR}/{game.app_name}/UninstalledArt.png') elif os.path.isfile(f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png"): bg: Image.Image = Image.open(f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png") @@ -292,9 +274,9 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): shortcut.WorkingDirectory = os.getcwd() # Icon - if not os.path.exists(icon+".ico"): - img = Image.open(icon+".png") - img.save(icon+".ico") + if not os.path.exists(icon + ".ico"): + img = Image.open(icon + ".png") + img.save(icon + ".ico") logger.info("Create Icon") shortcut.IconLocation = os.path.join(icon + ".ico") shortcut.save()