diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ddaec180..640f145a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -88,7 +88,6 @@ jobs: python-version: '3.9' - name: Dependencies run: | - pip3 install certifi==2022.6.15 pip3 install -r requirements.txt pip3 install -r requirements-presence.txt - name: Build Dependencies diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index 9b2ea92b..557b59dc 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -2,8 +2,9 @@ import os import platform from logging import getLogger -from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings, pyqtSlot, QCoreApplication +from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings, pyqtSlot from PyQt5.QtWidgets import QDialog, QApplication +from legendary.models.game import Game from requests.exceptions import ConnectionError, HTTPError from rare.components.dialogs.login import LoginDialog @@ -29,19 +30,41 @@ class LaunchWorker(QRunnable): self.signals = LaunchWorker.Signals() self.core = LegendaryCoreSingleton() - def run(self): + def run_real(self): pass + def run(self): + self.run_real() + self.signals.deleteLater() + class ImageWorker(LaunchWorker): + # FIXME: this is a middle-ground solution for concurrent downloads + class DownloadSlot(QObject): + def __init__(self, signals: LaunchWorker.Signals): + super(ImageWorker.DownloadSlot, self).__init__() + self.signals = signals + self.counter = 0 + self.length = 0 + + @pyqtSlot(object) + def counter_inc(self, game: Game): + self.signals.progress.emit( + int(self.counter / self.length * 50), + self.tr("Downloading image for {}").format(game.app_title) + ) + self.counter += 1 + def __init__(self): super(ImageWorker, self).__init__() + # FIXME: this is a middle-ground solution for concurrent downloads + self.dl_slot = ImageWorker.DownloadSlot(self.signals) self.image_manager = ImageManagerSingleton() def tr(self, t) -> str: - return QCoreApplication.translate(self.__class__.__name__, t) + return QApplication.instance().translate(self.__class__.__name__, t) - def run(self): + def run_real(self): # Download Images games, dlcs = self.core.get_game_and_dlc_list(update_assets=True, skip_ue=False) self.signals.result.emit((games, dlcs), "gamelist") @@ -53,20 +76,21 @@ class ImageWorker(LaunchWorker): game_list = games + dlc_list + na_games + na_dlc_list + self.dl_slot.length = len(game_list) for i, game in enumerate(game_list): if game.app_title == "Unreal Engine": game.app_title += f" {game.app_name.split('_')[-1]}" self.core.lgd.set_game_meta(game.app_name, game) - self.signals.progress.emit( - int(i / len(game_list) * 50), - self.tr("Downloading image for {}").format(game.app_title) - ) - self.image_manager.download_image_blocking(game) + # self.image_manager.download_image_blocking(game) + self.image_manager.download_image(game, self.dl_slot.counter_inc, priority=0) + # FIXME: this is a middle-ground solution for concurrent downloads + while self.dl_slot.counter < len(game_list): + QApplication.instance().processEvents() + self.dl_slot.deleteLater() igame_list = self.core.get_installed_list(include_dlc=True) # FIXME: incorporate installed game status checking here for now, still slow - # if igame := self.core.get_installed_game(game.app_name, skip_sync=True): for i, igame in enumerate(igame_list): self.signals.progress.emit( int(i / len(igame_list) * 50) + 50, @@ -100,7 +124,7 @@ class ApiRequestWorker(LaunchWorker): super(ApiRequestWorker, self).__init__() self.settings = QSettings() - def run(self) -> None: + def run_real(self) -> None: if self.settings.value("mac_meta", platform.system() == "Darwin", bool): try: result = self.core.get_game_and_dlc_list(update_assets=False, platform="Mac") diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py index 1412a96e..7e3054b8 100644 --- a/rare/shared/image_manager.py +++ b/rare/shared/image_manager.py @@ -1,9 +1,8 @@ -from __future__ import annotations - import hashlib import json import pickle import zlib +# from concurrent import futures from logging import getLogger from pathlib import Path from typing import TYPE_CHECKING @@ -30,6 +29,8 @@ from rare.lgndr.core import LegendaryCore from rare.models.signals import GlobalSignals from rare.utils.paths import image_dir, resources_path +# from requests_futures.sessions import FuturesSession + if TYPE_CHECKING: pass @@ -86,8 +87,8 @@ class ImageSize: class ImageManager(QObject): class Worker(QRunnable): class Signals(QObject): - # str: app_name - completed = pyqtSignal(str) + # object: Game + completed = pyqtSignal(object) def __init__(self, func: Callable, updates: List, json_data: Dict, game: Game): super(ImageManager.Worker, self).__init__() @@ -101,7 +102,7 @@ class ImageManager(QObject): def run(self): self.func(self.updates, self.json_data, self.game) logger.debug(f" Emitting singal for game {self.game.app_name} - {self.game.app_title}") - self.signals.completed.emit(self.game.app_name) + self.signals.completed.emit(self.game) def __init__(self, signals: GlobalSignals, core: LegendaryCore): # lk: the ordering in __img_types matters for the order of fallbacks @@ -179,7 +180,7 @@ class ImageManager(QObject): return updates, json_data - def __download(self, updates, json_data, game) -> bool: + def __download(self, updates, json_data, game, use_async: bool = False) -> bool: # Decompress existing image.cache if not self.__img_cache(game.app_name).is_file(): cache_data = dict(zip(self.__img_types, [None] * len(self.__img_types))) @@ -193,6 +194,22 @@ class ImageManager(QObject): if cache_data[image["type"]] is None or json_data[image["type"]] != image["md5"] ] + # Download + # # lk: Keep this so I don't have to go looking for it again, + # # lk: it might be useful in the future. + # if use_async and len(updates) > 1: + # session = FuturesSession(max_workers=len(self.__img_types)) + # image_requests = [] + # for image in updates: + # logger.info(f"Downloading {image['type']} for {game.app_title}") + # json_data[image["type"]] = image["md5"] + # payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()} + # req = session.get(image["url"], params=payload) + # req.image_type = image["type"] + # image_requests.append(req) + # for req in futures.as_completed(image_requests): + # cache_data[req.image_type] = req.result().content + # else: for image in updates: logger.info(f"Downloading {image['type']} for {game.app_title}") json_data[image["type"]] = image["md5"] @@ -291,18 +308,18 @@ class ImageManager(QObject): return data def download_image( - self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False + self, game: Game, load_callback: Callable[[Game], None], priority: int, force: bool = False ) -> None: updates, json_data = self.__prepare_download(game, force) if not updates: - load_callback() + load_callback(game) return if updates and game.app_name not in self.__worker_app_names: image_worker = ImageManager.Worker(self.__download, updates, json_data, game) self.__worker_app_names.append(game.app_name) image_worker.signals.completed.connect(load_callback) - image_worker.signals.completed.connect(lambda app_name: self.__worker_app_names.remove(app_name)) + image_worker.signals.completed.connect(lambda g: self.__worker_app_names.remove(g.app_name)) self.threadpool.start(image_worker, priority) def download_image_blocking(self, game: Game, force: bool = False) -> None: @@ -310,7 +327,7 @@ class ImageManager(QObject): if not updates: return if updates: - self.__download(updates, json_data, game) + self.__download(updates, json_data, game, use_async=True) def __get_cover( self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool = True