1
0
Fork 0
mirror of synced 2024-05-19 12:02:54 +12:00

Lgndr: Create argument models for consistent function arguments and cleaner implementation.

Lgndr: Prefix files that aren't part of legendary with 'api_'
Lgndr: Return statistics from 'verify_game' instead of parsing the exception
VerifyWorker: Add 'error' signal for exceptions.
This commit is contained in:
loathingKernel 2022-07-10 19:32:42 +03:00
parent d7aba41aa4
commit 3aae3887f6
10 changed files with 126 additions and 101 deletions

View file

@ -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,)
)
)

View file

@ -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",

View file

@ -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 (

View file

@ -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 ""

View file

@ -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.
"""

View file

@ -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"
)

View file

@ -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:

View file

@ -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

View file

@ -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