diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index 5ef822f7..bbf7b36a 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -8,6 +8,7 @@ from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSl from PyQt5.QtGui import QCloseEvent, QKeyEvent from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox +from rare.lgndr.api_arguments import LgndrInstallGameArgs from rare.lgndr.cli import LegendaryCLI from rare.lgndr.core import LegendaryCore from legendary.models.downloading import ConditionCheckResult @@ -311,7 +312,7 @@ class InstallInfoWorker(QRunnable): cli = LegendaryCLISingleton() download = InstallDownloadModel( # *self.core.prepare_download( - *cli.prepare_install( + *cli.prepare_install(LgndrInstallGameArgs( app_name=self.dl_item.options.app_name, base_path=self.dl_item.options.base_path, force=self.dl_item.options.force, @@ -336,7 +337,7 @@ class InstallInfoWorker(QRunnable): # disable_delta=, # override_delta_manifest=, # reset_sdl=, - sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list, + sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,) ) ) diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index 771036fd..666555f6 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -143,20 +143,27 @@ class GameInfo(QWidget, Ui_GameInfo): return self.verify_widget.setCurrentIndex(1) verify_worker = VerifyWorker(self.game.app_name) - verify_worker.signals.status.connect(self.verify_statistics) - verify_worker.signals.summary.connect(self.finish_verify) + verify_worker.signals.status.connect(self.verify_status) + verify_worker.signals.result.connect(self.verify_result) + verify_worker.signals.error.connect(self.verify_error) self.verify_progress.setValue(0) self.verify_threads[self.game.app_name] = verify_worker self.verify_pool.start(verify_worker) self.move_button.setEnabled(False) - def verify_statistics(self, num, total, app_name): + @pyqtSlot(str, str) + def verify_error(self, app_name, message): + pass + + @pyqtSlot(str, int, int, float, float) + def verify_status(self, app_name, num, total, percentage, speed): # checked, max, app_name if app_name == self.game.app_name: self.verify_progress.setValue(num * 100 // total) - def finish_verify(self, failed, missing, app_name): - if failed == missing == 0: + @pyqtSlot(str, bool, int, int) + def verify_result(self, app_name, success, failed, missing): + if success: QMessageBox.information( self, "Summary", diff --git a/rare/components/tabs/games/import_sync/egl_sync_group.py b/rare/components/tabs/games/import_sync/egl_sync_group.py index 3c92a734..fc3b7991 100644 --- a/rare/components/tabs/games/import_sync/egl_sync_group.py +++ b/rare/components/tabs/games/import_sync/egl_sync_group.py @@ -6,7 +6,7 @@ from typing import Tuple, Iterable, List from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox -from rare.lgndr.exception import LgndrException +from rare.lgndr.api_exception import LgndrException from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton from rare.ui.components.tabs.games.import_sync.egl_sync_group import Ui_EGLSyncGroup from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import ( diff --git a/rare/components/tabs/games/import_sync/import_group.py b/rare/components/tabs/games/import_sync/import_group.py index 7f78acf4..678204ba 100644 --- a/rare/components/tabs/games/import_sync/import_group.py +++ b/rare/components/tabs/games/import_sync/import_group.py @@ -11,7 +11,8 @@ from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThrea from PyQt5.QtGui import QStandardItemModel from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox -from rare.lgndr.exception import LgndrException +from lgndr.api_arguments import LgndrImportGameArgs +from rare.lgndr.api_exception import LgndrException from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup from rare.utils import legendary_utils @@ -105,23 +106,12 @@ class ImportWorker(QRunnable): # else: # return err - # TODO: This should be moved into RareCore and wrap import_game - def import_game_args(self, app_path: str, app_name: str, platfrom: str = "Windows", - disable_check: bool = False, skip_dlcs: bool = False, with_dlcs: bool = False, yes: bool = False): - args = Namespace( - app_path=app_path, - app_name=app_name, - platform=platfrom, - disable_check=disable_check, - skip_dlcs=skip_dlcs, - with_dlcs=with_dlcs, - yes=yes, - ) - return args - def __import_game(self, app_name: str, path: Path): cli = LegendaryCLISingleton() - args = self.import_game_args(str(path), app_name) + args = LgndrImportGameArgs( + app_path=str(path), + app_name=app_name, + ) try: cli.import_game(args) return "" diff --git a/rare/lgndr/__init__.py b/rare/lgndr/__init__.py index e69de29b..18d65d6b 100644 --- a/rare/lgndr/__init__.py +++ b/rare/lgndr/__init__.py @@ -0,0 +1,5 @@ +""" +Module that overloads and monkeypatches legendary's classes/methods to work with Rare + +Files with the 'api_' prefix are not part of legendary's source, and contain facilities relating to Rare. +""" \ No newline at end of file diff --git a/rare/lgndr/api_arguments.py b/rare/lgndr/api_arguments.py new file mode 100644 index 00000000..81380493 --- /dev/null +++ b/rare/lgndr/api_arguments.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from multiprocessing import Queue +from typing import Callable, List + + +@dataclass(kw_only=True) +class LgndrCommonArgs: + # keep this here for future reference + # when we move to 3.10 we can use 'kw_only' to do dataclass inheritance + app_name: str + platform: str = "Windows" + yes: bool = False + + +@dataclass +class LgndrImportGameArgs: + app_path: str + app_name: str + platform: str = "Windows" + disable_check: bool = False + skip_dlcs: bool = False + with_dlcs: bool = False + yes: bool = False + + +@dataclass +class LgndrVerifyGameArgs: + app_name: str + # Rare's extra arguments + verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print( + f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r" + ) + + +@dataclass +class LgndrInstallGameArgs: + app_name: str + base_path: str = "" + status_q: Queue = None + shared_memory: int = 0 + max_workers: int = 0 + force: bool = False + disable_patching: bool = False + game_folder: str = "" + override_manifest: str = "" + override_old_manifest: str = "" + override_base_url: str = "" + platform: str = "Windows" + file_prefix: List = None + file_exclude_prefix: List = None + install_tag: List = None + order_opt: bool = False + dl_timeout: int = 10 + repair_mode: bool = False + repair_and_update: bool = False + disable_delta: bool = False + override_delta_manifest: str = "" + egl_guid: str = "" + preferred_cdn: str = None + no_install: bool = False + ignore_space: bool = False + disable_sdl: bool = False + reset_sdl: bool = False + skip_sdl: bool = False + disable_https: bool = False + yes: bool = True + # Rare's extra arguments + sdl_prompt: Callable[[str, str], List[str]] = lambda a0, a1: [] + verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print( + f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r" + ) diff --git a/rare/lgndr/exception.py b/rare/lgndr/api_exception.py similarity index 100% rename from rare/lgndr/exception.py rename to rare/lgndr/api_exception.py diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py index f2b6bb2a..bf5f4d01 100644 --- a/rare/lgndr/cli.py +++ b/rare/lgndr/cli.py @@ -1,9 +1,7 @@ import os import logging import time -from argparse import Namespace -from multiprocessing import Queue -from typing import Optional, Callable, List +from typing import Optional, Union import legendary.cli from PyQt5.QtWidgets import QLabel, QMessageBox @@ -14,8 +12,9 @@ from legendary.utils.lfs import validate_files from legendary.utils.selective_dl import get_sdl_appname from .core import LegendaryCore -from .exception import LgndrException, LgndrLogHandler from .manager import DLManager +from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs +from .api_exception import LgndrException, LgndrLogHandler def get_boolean_choice(message): @@ -40,49 +39,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): self.handler = LgndrLogHandler() self.logger.addHandler(self.handler) - # def __init__(self, core: LegendaryCore): - # self.core = core - # self.logger = logging.getLogger('cli') - # self.logging_queue = None - # self.handler = LgndrLogHandler() - # self.logger.addHandler(self.handler) - - # app_name, repair, repair_mode, no_install, repair_and_update, file_prefix, file_exclude_prefix, platform - # sdl_prompt, status_q - def prepare_install(self, app_name: str, base_path: str = '', - status_q: Queue = None, shared_memory: int = 0, max_workers: int = 0, - force: bool = False, disable_patching: bool = False, - game_folder: str = '', override_manifest: str = '', - override_old_manifest: str = '', override_base_url: str = '', - platform: str = 'Windows', file_prefix: list = None, - file_exclude_prefix: list = None, install_tag: list = None, - order_opt: bool = False, dl_timeout: int = 10, - repair_mode: bool = False, repair_and_update: bool = False, - disable_delta: bool = False, override_delta_manifest: str = '', - egl_guid: str = '', preferred_cdn: str = None, - no_install: bool = False, ignore_space: bool = False, - disable_sdl: bool = False, reset_sdl: bool = False, skip_sdl: bool = False, - sdl_prompt: Callable[[str, str], List[str]] = list, - yes: bool = True, verify_callback: Callable[[int, int, float, float], None] = None, - disable_https: bool = False) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult): - args = Namespace( - app_name=app_name, base_path=base_path, - status_q=status_q, shared_memory=shared_memory, max_workers=max_workers, - force=force, disable_patching=disable_patching, - game_folder=game_folder, override_manifest=override_manifest, - override_old_manifest=override_old_manifest, override_base_url=override_base_url, - platform=platform, file_prefix=file_prefix, - file_exclude_prefix=file_exclude_prefix, install_tag=install_tag, - order_opt=order_opt, dl_timeout=dl_timeout, - repair_mode=repair_mode, repair_and_update=repair_and_update, - disable_delta=disable_delta, override_delta_manifest=override_delta_manifest, - preferred_cdn=preferred_cdn, - no_install=no_install, ignore_space=ignore_space, - disable_sdl=disable_sdl, reset_sdl=reset_sdl, skip_sdl=skip_sdl, - sdl_prompt=sdl_prompt, - yes=yes, callback=verify_callback, - disable_https=disable_https - ) + def prepare_install(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult): old_choice = legendary.cli.get_boolean_choice legendary.cli.get_boolean_choice = get_boolean_choice try: @@ -92,7 +49,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): finally: legendary.cli.get_boolean_choice = old_choice - def install_game(self, args): + def install_game(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult): args.app_name = self._resolve_aliases(args.app_name) if self.core.is_installed(args.app_name): igame = self.core.get_installed_game(args.app_name) @@ -267,10 +224,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): def uninstall_game(self, args): super(LegendaryCLI, self).uninstall_game(args) - def verify_game(self, args, print_command=True, repair_mode=False, repair_online=False): - if not hasattr(args, 'callback') or args.callback is None: - args.callback = print - + def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False): args.app_name = self._resolve_aliases(args.app_name) if not self.core.is_installed(args.app_name): logger.error(f'Game "{args.app_name}" is not installed') @@ -339,8 +293,8 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): speed = (processed - last_processed) / 1024 / 1024 / delta last_processed = processed - if args.callback: - args.callback(num, total, percentage, speed) + if args.verify_stdout: + args.verify_stdout(num, total, percentage, speed) if result == VerifyResult.HASH_MATCH: repair_file.append(f'{result_hash}:{path}') @@ -356,8 +310,8 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): logger.info(f'Other failure (see log), treating file as missing: "{path}"') missing.append(path) - if args.callback: - args.callback(num, total, percentage, speed) + if args.verify_stdout: + args.verify_stdout(num, total, percentage, speed) # always write repair file, even if all match if repair_file: @@ -368,12 +322,14 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): if not missing and not failed: logger.info('Verification finished successfully.') + return True, 0, 0 else: - logger.error(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.') + logger.warning(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.') if print_command: logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.') + return False, len(failed), len(missing) - def import_game(self, args): + def import_game(self, args: LgndrImportGameArgs): old_choice = legendary.cli.get_boolean_choice legendary.cli.get_boolean_choice = get_boolean_choice try: diff --git a/rare/lgndr/core.py b/rare/lgndr/core.py index 7434e1a5..d1e6ad95 100644 --- a/rare/lgndr/core.py +++ b/rare/lgndr/core.py @@ -5,7 +5,7 @@ from legendary.models.downloading import AnalysisResult from legendary.models.game import Game from legendary.models.manifest import ManifestMeta -from .exception import LgndrException, LgndrLogHandler +from .api_exception import LgndrException, LgndrLogHandler from .manager import DLManager diff --git a/rare/utils/legendary_utils.py b/rare/utils/legendary_utils.py index 90c80963..d56b2841 100644 --- a/rare/utils/legendary_utils.py +++ b/rare/utils/legendary_utils.py @@ -1,17 +1,14 @@ import os import platform -from argparse import Namespace from logging import getLogger from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths from legendary.core import LegendaryCore -from legendary.models.game import VerifyResult -from legendary.utils.lfs import validate_files -from parse import parse +from rare.lgndr.api_arguments import LgndrVerifyGameArgs +from rare.lgndr.api_exception import LgndrException from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton from rare.utils import config_helper -from rare.lgndr.exception import LgndrException logger = getLogger("Legendary Utils") @@ -88,8 +85,9 @@ def update_manifest(app_name: str, core: LegendaryCore): class VerifyWorker(QRunnable): class Signals(QObject): - status = pyqtSignal(int, int, str) - summary = pyqtSignal(int, int, str) + status = pyqtSignal(str, int, int, float, float) + result = pyqtSignal(str, bool, int, int) + error = pyqtSignal(str, str) num: int = 0 total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized @@ -103,25 +101,22 @@ class VerifyWorker(QRunnable): self.app_name = app_name def status_callback(self, num: int, total: int, percentage: float, speed: float): - self.signals.status.emit(num, total, self.app_name) + self.signals.status.emit(self.app_name, num, total, percentage, speed) def run(self): - args = Namespace(app_name=self.app_name, - callback=self.status_callback) + args = LgndrVerifyGameArgs(app_name=self.app_name, + verify_stdout=self.status_callback) try: # TODO: offer this as an alternative when manifest doesn't exist # TODO: requires the client to be online. To do it this way, we need to # TODO: somehow detect the error and offer a dialog in which case `verify_games` is # TODO: re-run with `repair_mode` and `repair_online` - self.cli.verify_game(args, print_command=False, repair_mode=True, repair_online=True) - # self.cli.verify_game(args, print_command=False) - self.signals.summary.emit(0, 0, self.app_name) + success, failed, missing = self.cli.verify_game( + args, print_command=False, repair_mode=True, repair_online=True) + # success, failed, missing = self.cli.verify_game(args, print_command=False) + self.signals.result.emit(self.app_name, success, failed, missing) except LgndrException as ret: - r = parse('Verification failed, {:d} file(s) corrupted, {:d} file(s) are missing.', ret.message) - if r is None: - raise ret - else: - self.signals.summary.emit(r[0], r[1], self.app_name) + self.signals.error.emit(self.app_name, ret.message) # FIXME: lk: ah ef me sideways, we can't even import this thing properly