Merge pull request #197 from loathingKernel/apes_together_strong
Implement shim legendary classes with overloaded/modified functions
This commit is contained in:
commit
04cd397a2f
61 changed files with 1997 additions and 815 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -1,4 +0,0 @@
|
||||||
[submodule "legendary"]
|
|
||||||
path = rare/legendary
|
|
||||||
url = https://github.com/dummerle/legendary
|
|
||||||
branch = rare
|
|
|
@ -23,3 +23,13 @@ To contribute fork the repository and clone **your** repo: `git clone https://gi
|
||||||
and upload it to GitHub with `git commit -m "message"` and `git push`. Some IDEs like PyCharm can do this automatically.
|
and upload it to GitHub with `git commit -m "message"` and `git push`. Some IDEs like PyCharm can do this automatically.
|
||||||
|
|
||||||
If you uploaded your changes, create a pull request
|
If you uploaded your changes, create a pull request
|
||||||
|
|
||||||
|
# Code Style Guidelines
|
||||||
|
|
||||||
|
## Signals and threads
|
||||||
|
|
||||||
|
## Function naming
|
||||||
|
|
||||||
|
## UI Classes
|
||||||
|
|
||||||
|
### Widget and Layout naming
|
|
@ -34,3 +34,13 @@ start = "rare.__main__:main"
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42", "wheel", "nuitka", "toml"]
|
||||||
|
build-backend = "nuitka.distutils.Build"
|
||||||
|
|
||||||
|
[nuitka]
|
||||||
|
show-scons = true
|
||||||
|
enable-plugin = pyqt5,anti-bloat
|
||||||
|
show-anti-bloat-changes = true
|
||||||
|
nofollow-import-to = ["*.tests", "*.distutils"]
|
||||||
|
|
|
@ -13,8 +13,8 @@ def main():
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
# insert legendary for installed via pip/setup.py submodule to path
|
# insert legendary for installed via pip/setup.py submodule to path
|
||||||
if not __name__ == "__main__":
|
# if not __name__ == "__main__":
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
|
# sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
|
||||||
|
|
||||||
# CLI Options
|
# CLI Options
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
|
@ -70,20 +70,18 @@ def main():
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.desktop_shortcut:
|
if args.desktop_shortcut or args.startmenu_shortcut:
|
||||||
from rare.utils import utils
|
from rare.utils.misc import create_desktop_link
|
||||||
|
|
||||||
|
if args.desktop_shortcut:
|
||||||
|
create_desktop_link(type_of_link="desktop", for_rare=True)
|
||||||
|
|
||||||
|
if args.startmenu_shortcut:
|
||||||
|
create_desktop_link(type_of_link="start_menu", for_rare=True)
|
||||||
|
|
||||||
utils.create_desktop_link(type_of_link="desktop", for_rare=True)
|
|
||||||
print("Link created")
|
print("Link created")
|
||||||
return
|
return
|
||||||
|
|
||||||
if args.startmenu_shortcut:
|
|
||||||
from rare.utils import utils
|
|
||||||
|
|
||||||
utils.create_desktop_link(type_of_link="start_menu", for_rare=True)
|
|
||||||
print("link created")
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.version:
|
if args.version:
|
||||||
from rare import __version__, code_name
|
from rare import __version__, code_name
|
||||||
|
|
||||||
|
@ -123,10 +121,10 @@ def main():
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# run from source
|
# run from source
|
||||||
# insert raw legendary submodule
|
# insert raw legendary submodule
|
||||||
sys.path.insert(
|
# sys.path.insert(
|
||||||
0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
|
# 0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
|
||||||
)
|
# )
|
||||||
# insert source directory
|
# insert source directory
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
|
#sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
17
rare/app.py
17
rare/app.py
|
@ -149,17 +149,25 @@ class App(RareApp):
|
||||||
def start_app(self):
|
def start_app(self):
|
||||||
for igame in self.core.get_installed_list():
|
for igame in self.core.get_installed_list():
|
||||||
if not os.path.exists(igame.install_path):
|
if not os.path.exists(igame.install_path):
|
||||||
legendary_utils.uninstall(igame.app_name, self.core)
|
# lk; since install_path is lost anyway, set keep_files to True
|
||||||
|
# lk: to avoid spamming the log with "file not found" errors
|
||||||
|
legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True)
|
||||||
logger.info(f"Uninstalled {igame.title}, because no game files exist")
|
logger.info(f"Uninstalled {igame.title}, because no game files exist")
|
||||||
continue
|
continue
|
||||||
if not os.path.exists(os.path.join(igame.install_path, igame.executable.replace("\\", "/").lstrip("/"))):
|
# lk: games that don't have an override and can't find their executable due to case sensitivity
|
||||||
|
# lk: will still erroneously require verification. This might need to be removed completely
|
||||||
|
# lk: or be decoupled from the verification requirement
|
||||||
|
if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""):
|
||||||
|
igame_executable = override_exe
|
||||||
|
else:
|
||||||
|
igame_executable = igame.executable
|
||||||
|
if not os.path.exists(os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))):
|
||||||
igame.needs_verification = True
|
igame.needs_verification = True
|
||||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
self.core.lgd.set_installed_game(igame.app_name, igame)
|
||||||
logger.info(f"{igame.title} needs verification")
|
logger.info(f"{igame.title} needs verification")
|
||||||
|
|
||||||
self.mainwindow = MainWindow()
|
self.mainwindow = MainWindow()
|
||||||
self.launch_dialog.close()
|
self.tray_icon: TrayIcon = TrayIcon(self)
|
||||||
self.tray_icon = TrayIcon(self)
|
|
||||||
self.tray_icon.exit_action.triggered.connect(self.exit_app)
|
self.tray_icon.exit_action.triggered.connect(self.exit_app)
|
||||||
self.tray_icon.start_rare.triggered.connect(self.show_mainwindow)
|
self.tray_icon.start_rare.triggered.connect(self.show_mainwindow)
|
||||||
self.tray_icon.activated.connect(
|
self.tray_icon.activated.connect(
|
||||||
|
@ -226,6 +234,7 @@ class App(RareApp):
|
||||||
self.mainwindow.hide()
|
self.mainwindow.hide()
|
||||||
threadpool = QThreadPool.globalInstance()
|
threadpool = QThreadPool.globalInstance()
|
||||||
threadpool.waitForDone()
|
threadpool.waitForDone()
|
||||||
|
self.core.exit()
|
||||||
if self.mainwindow is not None:
|
if self.mainwindow is not None:
|
||||||
self.mainwindow.close()
|
self.mainwindow.close()
|
||||||
if self.tray_icon is not None:
|
if self.tray_icon is not None:
|
||||||
|
|
|
@ -1,28 +1,32 @@
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform as pf
|
||||||
import sys
|
import sys
|
||||||
from multiprocessing import Queue as MPQueue
|
from typing import Tuple, List, Union, Optional
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtGui import QCloseEvent, QKeyEvent
|
from PyQt5.QtGui import QCloseEvent, QKeyEvent
|
||||||
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox
|
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox
|
||||||
from legendary.core import LegendaryCore
|
|
||||||
from legendary.models.downloading import ConditionCheckResult
|
from legendary.models.downloading import ConditionCheckResult
|
||||||
from legendary.models.game import Game
|
from legendary.models.game import Game
|
||||||
from legendary.utils.selective_dl import games
|
from legendary.utils.selective_dl import get_sdl_appname
|
||||||
|
|
||||||
|
from rare.lgndr.cli import LegendaryCLI
|
||||||
|
from rare.lgndr.api_arguments import LgndrInstallGameArgs
|
||||||
|
from rare.lgndr.api_exception import LgndrException
|
||||||
|
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||||
|
from rare.lgndr.core import LegendaryCore
|
||||||
from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
|
from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
|
||||||
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
|
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
|
||||||
from rare.utils.extra_widgets import PathEdit
|
from rare.utils.extra_widgets import PathEdit
|
||||||
from rare.utils.models import InstallDownloadModel, InstallQueueItemModel
|
from rare.models.install import InstallDownloadModel, InstallQueueItemModel
|
||||||
from rare.utils.utils import get_size
|
from rare.utils.misc import get_size
|
||||||
|
from rare.utils import config_helper
|
||||||
|
|
||||||
|
|
||||||
class InstallDialog(QDialog, Ui_InstallDialog):
|
class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
result_ready = pyqtSignal(InstallQueueItemModel)
|
result_ready = pyqtSignal(InstallQueueItemModel)
|
||||||
|
|
||||||
def __init__(self, dl_item: InstallQueueItemModel, update=False, silent=False, parent=None):
|
def __init__(self, dl_item: InstallQueueItemModel, update=False, repair=False, silent=False, parent=None):
|
||||||
super(InstallDialog, self).__init__(parent)
|
super(InstallDialog, self).__init__(parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||||
|
@ -31,7 +35,6 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.core = LegendaryCoreSingleton()
|
self.core = LegendaryCoreSingleton()
|
||||||
self.api_results = ApiResultsSingleton()
|
self.api_results = ApiResultsSingleton()
|
||||||
self.dl_item = dl_item
|
self.dl_item = dl_item
|
||||||
self.dl_item.status_q = MPQueue()
|
|
||||||
self.app_name = self.dl_item.options.app_name
|
self.app_name = self.dl_item.options.app_name
|
||||||
self.game = (
|
self.game = (
|
||||||
self.core.get_game(self.app_name)
|
self.core.get_game(self.app_name)
|
||||||
|
@ -42,6 +45,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.game_path = self.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value", "")
|
self.game_path = self.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value", "")
|
||||||
|
|
||||||
self.update = update
|
self.update = update
|
||||||
|
self.repair = repair
|
||||||
self.silent = silent
|
self.silent = silent
|
||||||
|
|
||||||
self.options_changed = False
|
self.options_changed = False
|
||||||
|
@ -84,18 +88,20 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
platforms.append("Mac")
|
platforms.append("Mac")
|
||||||
self.platform_combo_box.addItems(platforms)
|
self.platform_combo_box.addItems(platforms)
|
||||||
self.platform_combo_box.currentIndexChanged.connect(lambda: self.option_changed(None))
|
self.platform_combo_box.currentIndexChanged.connect(lambda: self.option_changed(None))
|
||||||
|
self.platform_combo_box.currentIndexChanged.connect(lambda: self.error_box())
|
||||||
self.platform_combo_box.currentIndexChanged.connect(
|
self.platform_combo_box.currentIndexChanged.connect(
|
||||||
lambda i: self.error_box(
|
lambda i: self.error_box(
|
||||||
self.tr("Warning"),
|
self.tr("Warning"),
|
||||||
self.tr("You will not be able to run the Game if you choose {}").format(
|
self.tr("You will not be able to run the game if you select <b>{}</b> as platform").format(
|
||||||
self.platform_combo_box.itemText(i)
|
self.platform_combo_box.itemText(i)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (self.platform_combo_box.currentText() == "Mac" and platform.system() != "Darwin")
|
if (self.platform_combo_box.currentText() == "Mac" and pf.system() != "Darwin")
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
self.platform_combo_box.currentTextChanged.connect(self.setup_sdl_list)
|
||||||
|
|
||||||
if platform.system() == "Darwin" and "Mac" in platforms:
|
if pf.system() == "Darwin" and "Mac" in platforms:
|
||||||
self.platform_combo_box.setCurrentIndex(platforms.index("Mac"))
|
self.platform_combo_box.setCurrentIndex(platforms.index("Mac"))
|
||||||
|
|
||||||
self.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0))
|
self.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0))
|
||||||
|
@ -109,22 +115,10 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.ignore_space_check.stateChanged.connect(self.option_changed)
|
self.ignore_space_check.stateChanged.connect(self.option_changed)
|
||||||
self.download_only_check.stateChanged.connect(lambda: self.non_reload_option_changed("download_only"))
|
self.download_only_check.stateChanged.connect(lambda: self.non_reload_option_changed("download_only"))
|
||||||
self.shortcut_cb.stateChanged.connect(lambda: self.non_reload_option_changed("shortcut"))
|
self.shortcut_cb.stateChanged.connect(lambda: self.non_reload_option_changed("shortcut"))
|
||||||
self.sdl_list_checks = list()
|
|
||||||
try:
|
self.sdl_list_cbs: List[TagCheckBox] = []
|
||||||
for key, info in games[self.app_name].items():
|
self.config_tags: Optional[List[str]] = None
|
||||||
cb = QDataCheckBox(info["name"], info["tags"])
|
self.setup_sdl_list("Mac" if pf.system() == "Darwin" and "Mac" in platforms else "Windows")
|
||||||
if key == "__required":
|
|
||||||
self.dl_item.options.sdl_list.extend(info["tags"])
|
|
||||||
cb.setChecked(True)
|
|
||||||
cb.setDisabled(True)
|
|
||||||
self.sdl_list_layout.addWidget(cb)
|
|
||||||
self.sdl_list_checks.append(cb)
|
|
||||||
self.sdl_list_frame.resize(self.sdl_list_frame.minimumSize())
|
|
||||||
for cb in self.sdl_list_checks:
|
|
||||||
cb.stateChanged.connect(self.option_changed)
|
|
||||||
except (KeyError, AttributeError):
|
|
||||||
self.sdl_list_frame.setVisible(False)
|
|
||||||
self.sdl_list_label.setVisible(False)
|
|
||||||
|
|
||||||
self.install_button.setEnabled(False)
|
self.install_button.setEnabled(False)
|
||||||
|
|
||||||
|
@ -138,7 +132,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.shortcut_cb.setVisible(False)
|
self.shortcut_cb.setVisible(False)
|
||||||
self.shortcut_lbl.setVisible(False)
|
self.shortcut_lbl.setVisible(False)
|
||||||
|
|
||||||
if platform.system() == "Darwin":
|
if pf.system() == "Darwin":
|
||||||
self.shortcut_cb.setDisabled(True)
|
self.shortcut_cb.setDisabled(True)
|
||||||
self.shortcut_cb.setChecked(False)
|
self.shortcut_cb.setChecked(False)
|
||||||
self.shortcut_cb.setToolTip(self.tr("Creating a shortcut is not supported on MacOS"))
|
self.shortcut_cb.setToolTip(self.tr("Creating a shortcut is not supported on MacOS"))
|
||||||
|
@ -153,7 +147,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.verify_button.clicked.connect(self.verify_clicked)
|
self.verify_button.clicked.connect(self.verify_clicked)
|
||||||
self.install_button.clicked.connect(self.install_clicked)
|
self.install_button.clicked.connect(self.install_clicked)
|
||||||
|
|
||||||
self.resize(self.minimumSize())
|
self.install_dialog_layout.setSizeConstraint(self.install_dialog_layout.SetFixedSize)
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
if self.silent:
|
if self.silent:
|
||||||
|
@ -163,21 +157,54 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.verify_clicked()
|
self.verify_clicked()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setup_sdl_list(self, platform="Windows"):
|
||||||
|
for cb in self.sdl_list_cbs:
|
||||||
|
cb.disconnect()
|
||||||
|
cb.deleteLater()
|
||||||
|
self.sdl_list_cbs.clear()
|
||||||
|
|
||||||
|
if config_tags := self.core.lgd.config.get(self.game.app_name, 'install_tags', fallback=None):
|
||||||
|
self.config_tags = config_tags.split(",")
|
||||||
|
config_disable_sdl = self.core.lgd.config.getboolean(self.game.app_name, 'disable_sdl', fallback=False)
|
||||||
|
sdl_name = get_sdl_appname(self.game.app_name)
|
||||||
|
if not config_disable_sdl and sdl_name is not None:
|
||||||
|
# FIXME: this should be updated whenever platform changes
|
||||||
|
sdl_data = self.core.get_sdl_data(sdl_name, platform=platform)
|
||||||
|
if sdl_data:
|
||||||
|
for tag, info in sdl_data.items():
|
||||||
|
cb = TagCheckBox(info["name"], info["tags"])
|
||||||
|
if tag == "__required":
|
||||||
|
cb.setChecked(True)
|
||||||
|
cb.setDisabled(True)
|
||||||
|
if self.config_tags is not None:
|
||||||
|
if all(elem in self.config_tags for elem in info["tags"]):
|
||||||
|
cb.setChecked(True)
|
||||||
|
self.sdl_list_layout.addWidget(cb)
|
||||||
|
self.sdl_list_cbs.append(cb)
|
||||||
|
self.sdl_list_frame.resize(self.sdl_list_frame.minimumSize())
|
||||||
|
for cb in self.sdl_list_cbs:
|
||||||
|
cb.stateChanged.connect(self.option_changed)
|
||||||
|
else:
|
||||||
|
self.sdl_list_frame.setVisible(False)
|
||||||
|
self.sdl_list_label.setVisible(False)
|
||||||
|
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
self.dl_item.options.base_path = self.install_dir_edit.text() if not self.update else None
|
self.dl_item.options.base_path = self.install_dir_edit.text() if not self.update else None
|
||||||
|
|
||||||
self.dl_item.options.max_workers = self.max_workers_spin.value()
|
self.dl_item.options.max_workers = self.max_workers_spin.value()
|
||||||
self.dl_item.options.max_shm = self.max_memory_spin.value()
|
self.dl_item.options.shared_memory = self.max_memory_spin.value()
|
||||||
self.dl_item.options.dl_optimizations = self.dl_optimizations_check.isChecked()
|
self.dl_item.options.order_opt = self.dl_optimizations_check.isChecked()
|
||||||
self.dl_item.options.force = self.force_download_check.isChecked()
|
self.dl_item.options.force = self.force_download_check.isChecked()
|
||||||
self.dl_item.options.ignore_space_req = self.ignore_space_check.isChecked()
|
self.dl_item.options.ignore_space = self.ignore_space_check.isChecked()
|
||||||
self.dl_item.options.no_install = self.download_only_check.isChecked()
|
self.dl_item.options.no_install = self.download_only_check.isChecked()
|
||||||
self.dl_item.options.platform = self.platform_combo_box.currentText()
|
self.dl_item.options.platform = self.platform_combo_box.currentText()
|
||||||
self.dl_item.options.sdl_list = [""]
|
if self.sdl_list_cbs:
|
||||||
for cb in self.sdl_list_checks:
|
self.dl_item.options.install_tag = [""]
|
||||||
if data := cb.isChecked():
|
for cb in self.sdl_list_cbs:
|
||||||
# noinspection PyTypeChecker
|
if data := cb.isChecked():
|
||||||
self.dl_item.options.sdl_list.extend(data)
|
# noinspection PyTypeChecker
|
||||||
|
self.dl_item.options.install_tag.extend(data)
|
||||||
|
|
||||||
def get_download_info(self):
|
def get_download_info(self):
|
||||||
self.dl_item.download = None
|
self.dl_item.download = None
|
||||||
|
@ -218,6 +245,12 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.dl_item.options.install_preqs = self.install_preqs_check.isChecked()
|
self.dl_item.options.install_preqs = self.install_preqs_check.isChecked()
|
||||||
|
|
||||||
def cancel_clicked(self):
|
def cancel_clicked(self):
|
||||||
|
if self.config_tags:
|
||||||
|
config_helper.add_option(self.game.app_name, 'install_tags', ','.join(self.config_tags))
|
||||||
|
else:
|
||||||
|
# lk: this is purely for cleaning any install tags we might have added erroneously to the config
|
||||||
|
config_helper.remove_option(self.game.app_name, 'install_tags')
|
||||||
|
|
||||||
self.dl_item.download = None
|
self.dl_item.download = None
|
||||||
self.reject_close = False
|
self.reject_close = False
|
||||||
self.close()
|
self.close()
|
||||||
|
@ -243,7 +276,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
||||||
self.cancel_button.setEnabled(True)
|
self.cancel_button.setEnabled(True)
|
||||||
if self.silent:
|
if self.silent:
|
||||||
self.close()
|
self.close()
|
||||||
if platform.system() == "Windows" or ArgumentsSingleton().debug:
|
if pf.system() == "Windows" or ArgumentsSingleton().debug:
|
||||||
if dl_item.igame.prereq_info and not dl_item.igame.prereq_info.get("installed", False):
|
if dl_item.igame.prereq_info and not dl_item.igame.prereq_info.get("installed", False):
|
||||||
self.install_preqs_check.setVisible(True)
|
self.install_preqs_check.setVisible(True)
|
||||||
self.install_preqs_lbl.setVisible(True)
|
self.install_preqs_lbl.setVisible(True)
|
||||||
|
@ -299,59 +332,34 @@ class InstallInfoWorker(QRunnable):
|
||||||
self.signals = InstallInfoWorker.Signals()
|
self.signals = InstallInfoWorker.Signals()
|
||||||
self.core = core
|
self.core = core
|
||||||
self.dl_item = dl_item
|
self.dl_item = dl_item
|
||||||
self.is_overlay_install = self.dl_item.options.overlay
|
|
||||||
self.game = game
|
self.game = game
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
if not self.is_overlay_install:
|
if not self.dl_item.options.overlay:
|
||||||
download = InstallDownloadModel(
|
cli = LegendaryCLI(self.core)
|
||||||
*self.core.prepare_download(
|
status = LgndrIndirectStatus()
|
||||||
app_name=self.dl_item.options.app_name,
|
result = cli.install_game(
|
||||||
base_path=self.dl_item.options.base_path,
|
LgndrInstallGameArgs(**self.dl_item.options.as_install_kwargs(), indirect_status=status)
|
||||||
force=self.dl_item.options.force,
|
|
||||||
no_install=self.dl_item.options.no_install,
|
|
||||||
status_q=self.dl_item.status_q,
|
|
||||||
max_shm=self.dl_item.options.max_shm,
|
|
||||||
max_workers=self.dl_item.options.max_workers,
|
|
||||||
# game_folder=,
|
|
||||||
# disable_patching=,
|
|
||||||
# override_manifest=,
|
|
||||||
# override_old_manifest=,
|
|
||||||
# override_base_url=,
|
|
||||||
platform=self.dl_item.options.platform,
|
|
||||||
# file_prefix_filter=,
|
|
||||||
# file_exclude_filter=,
|
|
||||||
# file_install_tag=,
|
|
||||||
dl_optimizations=self.dl_item.options.dl_optimizations,
|
|
||||||
# dl_timeout=,
|
|
||||||
repair=self.dl_item.options.repair,
|
|
||||||
# repair_use_latest=,
|
|
||||||
ignore_space_req=self.dl_item.options.ignore_space_req,
|
|
||||||
# disable_delta=,
|
|
||||||
# override_delta_manifest=,
|
|
||||||
# reset_sdl=,
|
|
||||||
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if result:
|
||||||
|
download = InstallDownloadModel(*result)
|
||||||
|
else:
|
||||||
|
raise LgndrException(status.message)
|
||||||
else:
|
else:
|
||||||
if not os.path.exists(path := self.dl_item.options.base_path):
|
if not os.path.exists(path := self.dl_item.options.base_path):
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
|
|
||||||
dlm, analysis, igame = self.core.prepare_overlay_install(
|
dlm, analysis, igame = self.core.prepare_overlay_install(
|
||||||
path=self.dl_item.options.base_path,
|
path=self.dl_item.options.base_path
|
||||||
status_queue=self.dl_item.status_q,
|
|
||||||
max_workers=self.dl_item.options.max_workers,
|
|
||||||
force=self.dl_item.options.force,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
download = InstallDownloadModel(
|
download = InstallDownloadModel(
|
||||||
dlmanager=dlm,
|
dlm=dlm,
|
||||||
analysis=analysis,
|
analysis=analysis,
|
||||||
game=self.game,
|
|
||||||
igame=igame,
|
igame=igame,
|
||||||
|
game=self.game,
|
||||||
repair=False,
|
repair=False,
|
||||||
repair_file="",
|
repair_file="",
|
||||||
res=ConditionCheckResult(), # empty
|
res=ConditionCheckResult(), # empty
|
||||||
|
@ -361,19 +369,21 @@ class InstallInfoWorker(QRunnable):
|
||||||
self.signals.result.emit(download)
|
self.signals.result.emit(download)
|
||||||
else:
|
else:
|
||||||
self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
|
self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
|
||||||
|
except LgndrException as ret:
|
||||||
|
self.signals.failed.emit(ret.message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.signals.failed.emit(str(e))
|
self.signals.failed.emit(str(e))
|
||||||
self.signals.finished.emit()
|
self.signals.finished.emit()
|
||||||
|
|
||||||
|
|
||||||
class QDataCheckBox(QCheckBox):
|
class TagCheckBox(QCheckBox):
|
||||||
def __init__(self, text, data=None, parent=None):
|
def __init__(self, text, tags: List[str], parent=None):
|
||||||
super(QDataCheckBox, self).__init__(parent)
|
super(TagCheckBox, self).__init__(parent)
|
||||||
self.setText(text)
|
self.setText(text)
|
||||||
self.data = data
|
self.tags = tags
|
||||||
|
|
||||||
def isChecked(self):
|
def isChecked(self) -> Union[bool, List[str]]:
|
||||||
if super(QDataCheckBox, self).isChecked():
|
if super(TagCheckBox, self).isChecked():
|
||||||
return self.data
|
return self.tags
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -10,7 +10,7 @@ from rare.models.apiresults import ApiResults
|
||||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
|
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
|
||||||
from rare.shared.image_manager import ImageManagerSingleton
|
from rare.shared.image_manager import ImageManagerSingleton
|
||||||
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
|
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
|
||||||
from rare.utils.utils import CloudWorker
|
from rare.utils.misc import CloudWorker
|
||||||
|
|
||||||
logger = getLogger("Login")
|
logger = getLogger("Login")
|
||||||
|
|
||||||
|
|
|
@ -117,8 +117,9 @@ class LoginDialog(QDialog):
|
||||||
self.close()
|
self.close()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Login failed.")
|
raise ValueError("Login failed.")
|
||||||
except ValueError as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
self.core.lgd.invalidate_userdata()
|
||||||
self.ui.next_button.setEnabled(False)
|
self.ui.next_button.setEnabled(False)
|
||||||
self.logged_in = False
|
self.logged_in = False
|
||||||
QMessageBox.warning(self, "Error", str(e))
|
QMessageBox.warning(None, self.tr("Login error"), str(e))
|
||||||
|
|
|
@ -10,7 +10,7 @@ from legendary.utils import webview_login
|
||||||
|
|
||||||
from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin
|
from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin
|
||||||
from rare.utils.extra_widgets import IndicatorLineEdit
|
from rare.utils.extra_widgets import IndicatorLineEdit
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("BrowserLogin")
|
logger = getLogger("BrowserLogin")
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,38 @@
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QLabel,
|
QLabel,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QFormLayout,
|
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
)
|
)
|
||||||
|
|
||||||
from legendary.models.game import Game
|
from legendary.models.game import Game
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
|
|
||||||
class UninstallDialog(QDialog):
|
class UninstallDialog(QDialog):
|
||||||
def __init__(self, game: Game):
|
def __init__(self, game: Game):
|
||||||
super(UninstallDialog, self).__init__()
|
super(UninstallDialog, self).__init__()
|
||||||
self.setWindowTitle("Uninstall Game")
|
|
||||||
self.info = 0
|
|
||||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||||
self.layout = QVBoxLayout()
|
self.setWindowTitle("Uninstall Game")
|
||||||
|
layout = QVBoxLayout()
|
||||||
self.info_text = QLabel(
|
self.info_text = QLabel(
|
||||||
self.tr("Do you really want to uninstall {}").format(game.app_title)
|
self.tr("Do you really want to uninstall <b>{}</b> ?").format(game.app_title)
|
||||||
)
|
)
|
||||||
self.layout.addWidget(self.info_text)
|
layout.addWidget(self.info_text)
|
||||||
self.keep_files = QCheckBox(self.tr("Keep Files"))
|
self.keep_files = QCheckBox(self.tr("Keep game files?"))
|
||||||
self.form = QFormLayout()
|
self.keep_config = QCheckBox(self.tr("Keep game configuation?"))
|
||||||
self.form.setContentsMargins(0, 10, 0, 10)
|
form_layout = QVBoxLayout()
|
||||||
self.form.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files)
|
form_layout.setContentsMargins(6, 6, 0, 6)
|
||||||
self.layout.addLayout(self.form)
|
form_layout.addWidget(self.keep_files)
|
||||||
|
form_layout.addWidget(self.keep_config)
|
||||||
|
layout.addLayout(form_layout)
|
||||||
|
|
||||||
self.button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
self.ok_button = QPushButton(
|
self.ok_button = QPushButton(
|
||||||
icon("ei.remove-circle", color="red"), self.tr("Uninstall")
|
icon("ei.remove-circle", color="red"), self.tr("Uninstall")
|
||||||
)
|
)
|
||||||
|
@ -39,20 +41,22 @@ class UninstallDialog(QDialog):
|
||||||
self.cancel_button = QPushButton(self.tr("Cancel"))
|
self.cancel_button = QPushButton(self.tr("Cancel"))
|
||||||
self.cancel_button.clicked.connect(self.cancel)
|
self.cancel_button.clicked.connect(self.cancel)
|
||||||
|
|
||||||
self.button_layout.addStretch(1)
|
button_layout.addWidget(self.ok_button)
|
||||||
self.button_layout.addWidget(self.ok_button)
|
button_layout.addStretch(1)
|
||||||
self.button_layout.addWidget(self.cancel_button)
|
button_layout.addWidget(self.cancel_button)
|
||||||
self.layout.addLayout(self.button_layout)
|
layout.addLayout(button_layout)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def get_information(self):
|
self.options: Tuple[bool, bool, bool] = (False, False, False)
|
||||||
|
|
||||||
|
def get_options(self) -> Tuple[bool, bool, bool]:
|
||||||
self.exec_()
|
self.exec_()
|
||||||
return self.info
|
return self.options
|
||||||
|
|
||||||
def ok(self):
|
def ok(self):
|
||||||
self.info = {"keep_files": self.keep_files.isChecked()}
|
self.options = (True, self.keep_files.isChecked(), self.keep_config.isChecked())
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self.info = 0
|
self.options = (False, False, False)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
|
@ -2,14 +2,14 @@ from PyQt5.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction, QShortcut
|
from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction, QShortcut
|
||||||
|
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||||
from rare.components.tabs.account import MiniWidget
|
from rare.components.tabs.account import AccountWidget
|
||||||
from rare.components.tabs.downloads import DownloadsTab
|
from rare.components.tabs.downloads import DownloadsTab
|
||||||
from rare.components.tabs.games import GamesTab
|
from rare.components.tabs.games import GamesTab
|
||||||
from rare.components.tabs.settings import SettingsTab
|
from rare.components.tabs.settings import SettingsTab
|
||||||
from rare.components.tabs.settings.debug import DebugSettings
|
from rare.components.tabs.settings.debug import DebugSettings
|
||||||
from rare.components.tabs.shop import Shop
|
from rare.components.tabs.shop import Shop
|
||||||
from rare.components.tabs.tab_utils import MainTabBar, TabButtonWidget
|
from rare.components.tabs.tab_utils import MainTabBar, TabButtonWidget
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
|
|
||||||
class TabWidget(QTabWidget):
|
class TabWidget(QTabWidget):
|
||||||
|
@ -54,9 +54,9 @@ class TabWidget(QTabWidget):
|
||||||
self.addTab(self.account, "")
|
self.addTab(self.account, "")
|
||||||
self.setTabEnabled(disabled_tab + 1, False)
|
self.setTabEnabled(disabled_tab + 1, False)
|
||||||
|
|
||||||
self.mini_widget = MiniWidget()
|
self.account_widget = AccountWidget()
|
||||||
account_action = QWidgetAction(self)
|
account_action = QWidgetAction(self)
|
||||||
account_action.setDefaultWidget(self.mini_widget)
|
account_action.setDefaultWidget(self.account_widget)
|
||||||
account_button = TabButtonWidget("mdi.account-circle", "Account", fallback_icon="fa.user")
|
account_button = TabButtonWidget("mdi.account-circle", "Account", fallback_icon="fa.user")
|
||||||
account_button.setMenu(QMenu())
|
account_button.setMenu(QMenu())
|
||||||
account_button.menu().addAction(account_action)
|
account_button.menu().addAction(account_action)
|
||||||
|
|
|
@ -3,34 +3,33 @@ import webbrowser
|
||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QLabel, QPushButton
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QLabel, QPushButton
|
||||||
|
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
|
|
||||||
class MiniWidget(QWidget):
|
class AccountWidget(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MiniWidget, self).__init__()
|
super(AccountWidget, self).__init__()
|
||||||
self.layout = QVBoxLayout()
|
|
||||||
self.core = LegendaryCoreSingleton()
|
self.core = LegendaryCoreSingleton()
|
||||||
self.signals = GlobalSignalsSingleton()
|
self.signals = GlobalSignalsSingleton()
|
||||||
self.layout.addWidget(QLabel("Account"))
|
|
||||||
username = self.core.lgd.userdata.get("display_name")
|
username = self.core.lgd.userdata.get("display_name")
|
||||||
if not username:
|
if not username:
|
||||||
username = "Offline"
|
username = "Offline"
|
||||||
|
|
||||||
self.layout.addWidget(QLabel(self.tr("Logged in as {}").format(username)))
|
|
||||||
|
|
||||||
self.open_browser = QPushButton(icon("fa.external-link"), self.tr("Account settings"))
|
self.open_browser = QPushButton(icon("fa.external-link"), self.tr("Account settings"))
|
||||||
self.open_browser.clicked.connect(
|
self.open_browser.clicked.connect(
|
||||||
lambda: webbrowser.open(
|
lambda: webbrowser.open(
|
||||||
"https://www.epicgames.com/account/personal?productName=epicgames"
|
"https://www.epicgames.com/account/personal?productName=epicgames"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.layout.addWidget(self.open_browser)
|
|
||||||
|
|
||||||
self.logout_button = QPushButton(self.tr("Logout"))
|
self.logout_button = QPushButton(self.tr("Logout"))
|
||||||
self.logout_button.clicked.connect(self.logout)
|
self.logout_button.clicked.connect(self.logout)
|
||||||
self.layout.addWidget(self.logout_button)
|
|
||||||
self.setLayout(self.layout)
|
layout = QVBoxLayout(self)
|
||||||
|
layout.addWidget(QLabel(self.tr("Account")))
|
||||||
|
layout.addWidget(QLabel(self.tr("Logged in as <b>{}</b>").format(username)))
|
||||||
|
layout.addWidget(self.open_browser)
|
||||||
|
layout.addWidget(self.logout_button)
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import datetime
|
import datetime
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import List, Dict
|
from typing import List, Dict, Union
|
||||||
|
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, QSettings
|
from PyQt5.QtCore import QThread, pyqtSignal, QSettings, pyqtSlot
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
|
@ -11,17 +11,17 @@ from PyQt5.QtWidgets import (
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
from legendary.core import LegendaryCore
|
from legendary.core import LegendaryCore
|
||||||
from legendary.models.downloading import UIUpdate
|
|
||||||
from legendary.models.game import Game, InstalledGame
|
from legendary.models.game import Game, InstalledGame
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
|
||||||
from rare.components.dialogs.install_dialog import InstallDialog
|
from rare.components.dialogs.install_dialog import InstallDialog
|
||||||
from rare.components.tabs.downloads.dl_queue_widget import DlQueueWidget, DlWidget
|
from rare.components.tabs.downloads.dl_queue_widget import DlQueueWidget, DlWidget
|
||||||
from rare.components.tabs.downloads.download_thread import DownloadThread
|
from rare.components.tabs.downloads.download_thread import DownloadThread
|
||||||
|
from rare.lgndr.downloading import UIUpdate
|
||||||
|
from rare.models.install import InstallOptionsModel, InstallQueueItemModel
|
||||||
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||||
from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab
|
from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab
|
||||||
from rare.utils.models import InstallOptionsModel, InstallQueueItemModel
|
from rare.utils.misc import get_size, create_desktop_link
|
||||||
from rare.utils.utils import get_size
|
|
||||||
|
|
||||||
logger = getLogger("Download")
|
logger = getLogger("Download")
|
||||||
|
|
||||||
|
@ -56,8 +56,8 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
self.update_layout.addWidget(self.update_text)
|
self.update_layout.addWidget(self.update_text)
|
||||||
self.update_text.setVisible(len(updates) == 0)
|
self.update_text.setVisible(len(updates) == 0)
|
||||||
|
|
||||||
for name in updates:
|
for app_name in updates:
|
||||||
self.add_update(self.core.get_installed_game(name))
|
self.add_update(app_name)
|
||||||
|
|
||||||
self.queue_widget.item_removed.connect(self.queue_item_removed)
|
self.queue_widget.item_removed.connect(self.queue_item_removed)
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
self.signals.game_uninstalled.connect(self.remove_update)
|
self.signals.game_uninstalled.connect(self.remove_update)
|
||||||
|
|
||||||
self.signals.add_download.connect(
|
self.signals.add_download.connect(
|
||||||
lambda app_name: self.add_update(self.core.get_installed_game(app_name))
|
lambda app_name: self.add_update(app_name)
|
||||||
)
|
)
|
||||||
self.signals.game_uninstalled.connect(self.game_uninstalled)
|
self.signals.game_uninstalled.connect(self.game_uninstalled)
|
||||||
|
|
||||||
|
@ -77,14 +77,17 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
w.update_button.setDisabled(False)
|
w.update_button.setDisabled(False)
|
||||||
w.update_with_settings.setDisabled(False)
|
w.update_with_settings.setDisabled(False)
|
||||||
|
|
||||||
def add_update(self, igame: InstalledGame):
|
def add_update(self, app_name: str):
|
||||||
widget = UpdateWidget(self.core, igame, self)
|
if old_widget := self.update_widgets.get(app_name, False):
|
||||||
|
old_widget.deleteLater()
|
||||||
|
self.update_widgets.pop(app_name)
|
||||||
|
widget = UpdateWidget(self.core, app_name, self)
|
||||||
self.update_layout.addWidget(widget)
|
self.update_layout.addWidget(widget)
|
||||||
self.update_widgets[igame.app_name] = widget
|
self.update_widgets[app_name] = widget
|
||||||
widget.update_signal.connect(self.get_install_options)
|
widget.update_signal.connect(self.get_install_options)
|
||||||
if QSettings().value("auto_update", False, bool):
|
if QSettings().value("auto_update", False, bool):
|
||||||
self.get_install_options(
|
self.get_install_options(
|
||||||
InstallOptionsModel(app_name=igame.app_name, update=True, silent=True)
|
InstallOptionsModel(app_name=app_name, update=True, silent=True)
|
||||||
)
|
)
|
||||||
widget.update_button.setDisabled(True)
|
widget.update_button.setDisabled(True)
|
||||||
self.update_text.setVisible(False)
|
self.update_text.setVisible(False)
|
||||||
|
@ -97,14 +100,14 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
self.queue_widget.update_queue(self.dl_queue)
|
self.queue_widget.update_queue(self.dl_queue)
|
||||||
break
|
break
|
||||||
|
|
||||||
# game has available update
|
|
||||||
if app_name in self.update_widgets.keys():
|
|
||||||
self.remove_update(app_name)
|
|
||||||
|
|
||||||
# if game is updating
|
# if game is updating
|
||||||
if self.active_game and self.active_game.app_name == app_name:
|
if self.active_game and self.active_game.app_name == app_name:
|
||||||
self.stop_download()
|
self.stop_download()
|
||||||
|
|
||||||
|
# game has available update
|
||||||
|
if app_name in self.update_widgets.keys():
|
||||||
|
self.remove_update(app_name)
|
||||||
|
|
||||||
def remove_update(self, app_name):
|
def remove_update(self, app_name):
|
||||||
if w := self.update_widgets.get(app_name):
|
if w := self.update_widgets.get(app_name):
|
||||||
w.deleteLater()
|
w.deleteLater()
|
||||||
|
@ -120,6 +123,7 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
|
|
||||||
def stop_download(self):
|
def stop_download(self):
|
||||||
self.thread.kill()
|
self.thread.kill()
|
||||||
|
self.kill_button.setEnabled(False)
|
||||||
|
|
||||||
def install_game(self, queue_item: InstallQueueItemModel):
|
def install_game(self, queue_item: InstallQueueItemModel):
|
||||||
if self.active_game is None:
|
if self.active_game is None:
|
||||||
|
@ -134,8 +138,8 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
self.queue_widget.update_queue(self.dl_queue)
|
self.queue_widget.update_queue(self.dl_queue)
|
||||||
self.active_game = queue_item.download.game
|
self.active_game = queue_item.download.game
|
||||||
self.thread = DownloadThread(self.core, queue_item)
|
self.thread = DownloadThread(self.core, queue_item)
|
||||||
self.thread.status.connect(self.status)
|
self.thread.ret_status.connect(self.status)
|
||||||
self.thread.statistics.connect(self.statistics)
|
self.thread.ui_update.connect(self.progress_update)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
self.kill_button.setDisabled(False)
|
self.kill_button.setDisabled(False)
|
||||||
self.analysis = queue_item.download.analysis
|
self.analysis = queue_item.download.analysis
|
||||||
|
@ -143,8 +147,16 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
|
|
||||||
self.signals.installation_started.emit(self.active_game.app_name)
|
self.signals.installation_started.emit(self.active_game.app_name)
|
||||||
|
|
||||||
def status(self, text):
|
@pyqtSlot(DownloadThread.ReturnStatus)
|
||||||
if text == "finish":
|
def status(self, result: DownloadThread.ReturnStatus):
|
||||||
|
if result.ret_code == result.ReturnCode.FINISHED:
|
||||||
|
if result.shortcuts:
|
||||||
|
if not create_desktop_link(result.app_name, self.core, "desktop"):
|
||||||
|
# maybe add it to download summary, to show in finished downloads
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.info("Desktop shortcut written")
|
||||||
|
|
||||||
self.dl_name.setText(self.tr("Download finished. Reload library"))
|
self.dl_name.setText(self.tr("Download finished. Reload library"))
|
||||||
logger.info(f"Download finished: {self.active_game.app_title}")
|
logger.info(f"Download finished: {self.active_game.app_title}")
|
||||||
|
|
||||||
|
@ -179,10 +191,10 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
else:
|
else:
|
||||||
self.queue_widget.update_queue(self.dl_queue)
|
self.queue_widget.update_queue(self.dl_queue)
|
||||||
|
|
||||||
elif text[:5] == "error":
|
elif result.ret_code == result.ReturnCode.ERROR:
|
||||||
QMessageBox.warning(self, "warn", f"Download error: {text[6:]}")
|
QMessageBox.warning(self, self.tr("Error"), f"Download error: {result.message}")
|
||||||
|
|
||||||
elif text == "stop":
|
elif result.ret_code == result.ReturnCode.STOPPED:
|
||||||
self.reset_infos()
|
self.reset_infos()
|
||||||
if w := self.update_widgets.get(self.active_game.app_name):
|
if w := self.update_widgets.get(self.active_game.app_name):
|
||||||
w.update_button.setDisabled(False)
|
w.update_button.setDisabled(False)
|
||||||
|
@ -202,7 +214,7 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
self.downloaded.setText("n/a")
|
self.downloaded.setText("n/a")
|
||||||
self.analysis = None
|
self.analysis = None
|
||||||
|
|
||||||
def statistics(self, ui_update: UIUpdate):
|
def progress_update(self, ui_update: UIUpdate):
|
||||||
self.progress_bar.setValue(
|
self.progress_bar.setValue(
|
||||||
100 * ui_update.total_downloaded // self.analysis.dl_size
|
100 * ui_update.total_downloaded // self.analysis.dl_size
|
||||||
)
|
)
|
||||||
|
@ -218,12 +230,16 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
100 * ui_update.total_downloaded // self.analysis.dl_size
|
100 * ui_update.total_downloaded // self.analysis.dl_size
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_time(self, seconds: int) -> str:
|
def get_time(self, seconds: Union[int, float]) -> str:
|
||||||
return str(datetime.timedelta(seconds=seconds))
|
return str(datetime.timedelta(seconds=seconds))
|
||||||
|
|
||||||
def on_install_dialog_closed(self, download_item: InstallQueueItemModel):
|
def on_install_dialog_closed(self, download_item: InstallQueueItemModel):
|
||||||
if download_item:
|
if download_item:
|
||||||
self.install_game(download_item)
|
self.install_game(download_item)
|
||||||
|
# lk: In case the download in comming from game verification/repair
|
||||||
|
if w := self.update_widgets.get(download_item.options.app_name):
|
||||||
|
w.update_button.setDisabled(True)
|
||||||
|
w.update_with_settings.setDisabled(True)
|
||||||
self.signals.set_main_tab_index.emit(1)
|
self.signals.set_main_tab_index.emit(1)
|
||||||
else:
|
else:
|
||||||
if w := self.update_widgets.get(download_item.options.app_name):
|
if w := self.update_widgets.get(download_item.options.app_name):
|
||||||
|
@ -241,19 +257,6 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
install_dialog.result_ready.connect(self.on_install_dialog_closed)
|
install_dialog.result_ready.connect(self.on_install_dialog_closed)
|
||||||
install_dialog.execute()
|
install_dialog.execute()
|
||||||
|
|
||||||
def start_download(self, download_item: InstallQueueItemModel):
|
|
||||||
downloads = (
|
|
||||||
len(self.downloadTab.dl_queue)
|
|
||||||
+ len(self.downloadTab.update_widgets.keys())
|
|
||||||
+ 1
|
|
||||||
)
|
|
||||||
self.setTabText(
|
|
||||||
1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")
|
|
||||||
)
|
|
||||||
self.setCurrentIndex(1)
|
|
||||||
self.downloadTab.install_game(download_item)
|
|
||||||
self.games_tab.start_download(download_item.options.app_name)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_download_active(self):
|
def is_download_active(self):
|
||||||
return self.active_game is not None
|
return self.active_game is not None
|
||||||
|
@ -262,37 +265,37 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
||||||
class UpdateWidget(QWidget):
|
class UpdateWidget(QWidget):
|
||||||
update_signal = pyqtSignal(InstallOptionsModel)
|
update_signal = pyqtSignal(InstallOptionsModel)
|
||||||
|
|
||||||
def __init__(self, core: LegendaryCore, igame: InstalledGame, parent):
|
def __init__(self, core: LegendaryCore, app_name: str, parent):
|
||||||
super(UpdateWidget, self).__init__(parent=parent)
|
super(UpdateWidget, self).__init__(parent=parent)
|
||||||
self.core = core
|
self.core = core
|
||||||
self.game = igame
|
self.game: Game = core.get_game(app_name)
|
||||||
|
self.igame: InstalledGame = self.core.get_installed_game(app_name)
|
||||||
|
|
||||||
self.layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self.title = QLabel(self.game.title)
|
self.title = QLabel(self.igame.title)
|
||||||
self.layout.addWidget(self.title)
|
layout.addWidget(self.title)
|
||||||
|
|
||||||
self.update_button = QPushButton(self.tr("Update Game"))
|
self.update_button = QPushButton(self.tr("Update Game"))
|
||||||
self.update_button.clicked.connect(lambda: self.update_game(True))
|
self.update_button.clicked.connect(lambda: self.update_game(True))
|
||||||
self.update_with_settings = QPushButton("Update with settings")
|
self.update_with_settings = QPushButton("Update with settings")
|
||||||
self.update_with_settings.clicked.connect(lambda: self.update_game(False))
|
self.update_with_settings.clicked.connect(lambda: self.update_game(False))
|
||||||
self.layout.addWidget(self.update_button)
|
layout.addWidget(self.update_button)
|
||||||
self.layout.addWidget(self.update_with_settings)
|
layout.addWidget(self.update_with_settings)
|
||||||
self.layout.addWidget(
|
layout.addWidget(
|
||||||
QLabel(
|
QLabel(
|
||||||
self.tr("Version: ")
|
self.tr("Version: <b>")
|
||||||
+ self.game.version
|
+ self.igame.version
|
||||||
+ " -> "
|
+ "</b> >> <b>"
|
||||||
+ self.core.get_asset(
|
+ self.game.app_version(self.igame.platform)
|
||||||
self.game.app_name, self.game.platform, False
|
+ "</b>"
|
||||||
).build_version
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.setLayout(self.layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def update_game(self, auto: bool):
|
def update_game(self, auto: bool):
|
||||||
self.update_button.setDisabled(True)
|
self.update_button.setDisabled(True)
|
||||||
self.update_with_settings.setDisabled(True)
|
self.update_with_settings.setDisabled(True)
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
InstallOptionsModel(app_name=self.game.app_name, silent=auto)
|
InstallOptionsModel(app_name=self.igame.app_name, silent=auto)
|
||||||
) # True if settings
|
) # True if settings
|
||||||
|
|
|
@ -10,8 +10,8 @@ from PyQt5.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from rare.utils.models import InstallQueueItemModel
|
from rare.models.install import InstallQueueItemModel
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("QueueWidget")
|
logger = getLogger("QueueWidget")
|
||||||
|
|
||||||
|
|
|
@ -1,209 +1,150 @@
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import queue
|
import queue
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import IntEnum
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from queue import Empty
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
import psutil
|
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, QProcess
|
from PyQt5.QtCore import QThread, pyqtSignal, QProcess
|
||||||
from legendary.core import LegendaryCore
|
from legendary.core import LegendaryCore
|
||||||
from legendary.models.downloading import UIUpdate, WriterTask
|
|
||||||
|
|
||||||
from rare.shared import GlobalSignalsSingleton
|
from rare.lgndr.api_monkeys import DLManagerSignals
|
||||||
from rare.utils.models import InstallQueueItemModel
|
from rare.lgndr.cli import LegendaryCLI
|
||||||
from rare.utils.utils import create_desktop_link
|
from rare.lgndr.downloading import UIUpdate
|
||||||
|
from rare.models.install import InstallQueueItemModel
|
||||||
|
from rare.shared import GlobalSignalsSingleton, ArgumentsSingleton
|
||||||
|
|
||||||
logger = getLogger("DownloadThread")
|
logger = getLogger("DownloadThread")
|
||||||
|
|
||||||
|
|
||||||
class DownloadThread(QThread):
|
class DownloadThread(QThread):
|
||||||
status = pyqtSignal(str)
|
@dataclass
|
||||||
statistics = pyqtSignal(UIUpdate)
|
class ReturnStatus:
|
||||||
|
class ReturnCode(IntEnum):
|
||||||
|
ERROR = 1
|
||||||
|
STOPPED = 2
|
||||||
|
FINISHED = 3
|
||||||
|
|
||||||
def __init__(self, core: LegendaryCore, queue_item: InstallQueueItemModel):
|
app_name: str
|
||||||
|
ret_code: ReturnCode = ReturnCode.ERROR
|
||||||
|
message: str = ""
|
||||||
|
dlcs: Optional[List[Dict]] = None
|
||||||
|
sync_saves: bool = False
|
||||||
|
tip_url: str = ""
|
||||||
|
shortcuts: bool = False
|
||||||
|
|
||||||
|
ret_status = pyqtSignal(ReturnStatus)
|
||||||
|
ui_update = pyqtSignal(UIUpdate)
|
||||||
|
|
||||||
|
def __init__(self, core: LegendaryCore, item: InstallQueueItemModel):
|
||||||
super(DownloadThread, self).__init__()
|
super(DownloadThread, self).__init__()
|
||||||
self.core = core
|
|
||||||
self.signals = GlobalSignalsSingleton()
|
self.signals = GlobalSignalsSingleton()
|
||||||
self.dlm = queue_item.download.dlmanager
|
self.core: LegendaryCore = core
|
||||||
self.no_install = queue_item.options.no_install
|
self.item: InstallQueueItemModel = item
|
||||||
self.status_q = queue_item.status_q
|
self.dlm_signals: DLManagerSignals = DLManagerSignals()
|
||||||
self.igame = queue_item.download.igame
|
|
||||||
self.repair = queue_item.download.repair
|
|
||||||
self.repair_file = queue_item.download.repair_file
|
|
||||||
self.queue_item = queue_item
|
|
||||||
|
|
||||||
self._kill = False
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
start_time = time.time()
|
cli = LegendaryCLI(self.core)
|
||||||
dl_stopped = False
|
self.item.download.dlm.logging_queue = cli.logging_queue
|
||||||
|
self.item.download.dlm.proc_debug = ArgumentsSingleton().debug
|
||||||
|
ret = DownloadThread.ReturnStatus(self.item.download.game.app_name)
|
||||||
|
start_t = time.time()
|
||||||
try:
|
try:
|
||||||
|
self.item.download.dlm.start()
|
||||||
self.dlm.start()
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
while self.dlm.is_alive():
|
while self.item.download.dlm.is_alive():
|
||||||
if self._kill:
|
|
||||||
self.status.emit("stop")
|
|
||||||
logger.info("Download stopping...")
|
|
||||||
|
|
||||||
# The code below is a temporary solution.
|
|
||||||
# It should be removed once legendary supports stopping downloads more gracefully.
|
|
||||||
|
|
||||||
self.dlm.running = False
|
|
||||||
|
|
||||||
# send conditions to unlock threads if they aren't already
|
|
||||||
for cond in self.dlm.conditions:
|
|
||||||
with cond:
|
|
||||||
cond.notify()
|
|
||||||
|
|
||||||
# make sure threads are dead.
|
|
||||||
for t in self.dlm.threads:
|
|
||||||
t.join(timeout=5.0)
|
|
||||||
if t.is_alive():
|
|
||||||
logger.warning(f"Thread did not terminate! {repr(t)}")
|
|
||||||
|
|
||||||
# clean up all the queues, otherwise this process won't terminate properly
|
|
||||||
for name, q in zip(
|
|
||||||
(
|
|
||||||
"Download jobs",
|
|
||||||
"Writer jobs",
|
|
||||||
"Download results",
|
|
||||||
"Writer results",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
self.dlm.dl_worker_queue,
|
|
||||||
self.dlm.writer_queue,
|
|
||||||
self.dlm.dl_result_q,
|
|
||||||
self.dlm.writer_result_q,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
logger.debug(f'Cleaning up queue "{name}"')
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
_ = q.get_nowait()
|
|
||||||
except Empty:
|
|
||||||
q.close()
|
|
||||||
q.join_thread()
|
|
||||||
except AttributeError:
|
|
||||||
logger.warning(f"Queue {name} did not close")
|
|
||||||
|
|
||||||
if self.dlm.writer_queue:
|
|
||||||
# cancel installation
|
|
||||||
self.dlm.writer_queue.put_nowait(WriterTask("", kill=True))
|
|
||||||
|
|
||||||
# forcibly kill DL workers that are not actually dead yet
|
|
||||||
for child in self.dlm.children:
|
|
||||||
if child.exitcode is None:
|
|
||||||
child.terminate()
|
|
||||||
|
|
||||||
if self.dlm.shared_memory:
|
|
||||||
# close up shared memory
|
|
||||||
self.dlm.shared_memory.close()
|
|
||||||
self.dlm.shared_memory.unlink()
|
|
||||||
self.dlm.shared_memory = None
|
|
||||||
|
|
||||||
self.dlm.kill()
|
|
||||||
|
|
||||||
# force kill any threads that are somehow still alive
|
|
||||||
for proc in psutil.process_iter():
|
|
||||||
# check whether the process name matches
|
|
||||||
if (
|
|
||||||
sys.platform in ["linux", "darwin"]
|
|
||||||
and proc.name() == "DownloadThread"
|
|
||||||
):
|
|
||||||
proc.kill()
|
|
||||||
elif (
|
|
||||||
sys.platform == "win32"
|
|
||||||
and proc.name() == "python.exe"
|
|
||||||
and proc.create_time() >= start_time
|
|
||||||
):
|
|
||||||
proc.kill()
|
|
||||||
|
|
||||||
logger.info("Download stopped. It can be continued later.")
|
|
||||||
dl_stopped = True
|
|
||||||
try:
|
try:
|
||||||
if not dl_stopped:
|
self.ui_update.emit(self.item.download.dlm.status_queue.get(timeout=1.0))
|
||||||
self.statistics.emit(self.status_q.get(timeout=1))
|
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
if self.dlm_signals.update:
|
||||||
self.dlm.join()
|
try:
|
||||||
|
self.item.download.dlm.signals_queue.put(self.dlm_signals, block=False, timeout=1.0)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
time.sleep(self.item.download.dlm.update_interval / 10)
|
||||||
|
self.item.download.dlm.join()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
|
||||||
f"Installation failed after {time.time() - start_time:.02f} seconds: {e}"
|
|
||||||
)
|
|
||||||
self.status.emit(f"error {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
else:
|
|
||||||
if dl_stopped:
|
|
||||||
return
|
|
||||||
self.status.emit("dl_finished")
|
|
||||||
end_t = time.time()
|
end_t = time.time()
|
||||||
logger.info(f"Download finished in {end_t - start_time}s")
|
logger.error(f"Installation failed after {end_t - start_t:.02f} seconds.")
|
||||||
game = self.core.get_game(self.igame.app_name)
|
logger.warning(f"The following exception occurred while waiting for the downloader to finish: {e!r}.")
|
||||||
|
ret.ret_code = ret.ReturnCode.ERROR
|
||||||
|
ret.message = f"{e!r}"
|
||||||
|
self.ret_status.emit(ret)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
end_t = time.time()
|
||||||
|
if self.dlm_signals.kill is True:
|
||||||
|
logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.")
|
||||||
|
ret.ret_code = ret.ReturnCode.STOPPED
|
||||||
|
self.ret_status.emit(ret)
|
||||||
|
return
|
||||||
|
logger.info(f"Download finished in {end_t - start_t:.02f} seconds.")
|
||||||
|
|
||||||
if self.queue_item.options.overlay:
|
ret.ret_code = ret.ReturnCode.FINISHED
|
||||||
|
|
||||||
|
if self.item.options.overlay:
|
||||||
self.signals.overlay_installation_finished.emit()
|
self.signals.overlay_installation_finished.emit()
|
||||||
self.core.finish_overlay_install(self.igame)
|
self.core.finish_overlay_install(self.item.download.igame)
|
||||||
self.status.emit("finish")
|
self.ret_status.emit(ret)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.no_install:
|
if not self.item.options.no_install:
|
||||||
postinstall = self.core.install_game(self.igame)
|
postinstall = self.core.install_game(self.item.download.igame)
|
||||||
if postinstall:
|
if postinstall:
|
||||||
self._handle_postinstall(postinstall, self.igame)
|
# LegendaryCLI(self.core)._handle_postinstall(
|
||||||
|
# postinstall,
|
||||||
|
# self.item.download.igame,
|
||||||
|
# False,
|
||||||
|
# self.item.options.install_preqs,
|
||||||
|
# )
|
||||||
|
self._handle_postinstall(postinstall, self.item.download.igame)
|
||||||
|
|
||||||
dlcs = self.core.get_dlc_for_game(self.igame.app_name)
|
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
|
||||||
if dlcs:
|
if dlcs and not self.item.options.skip_dlcs:
|
||||||
print("The following DLCs are available for this game:")
|
ret.dlcs = []
|
||||||
for dlc in dlcs:
|
for dlc in dlcs:
|
||||||
print(
|
ret.dlcs.append(
|
||||||
f" - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})"
|
{
|
||||||
|
"app_name": dlc.app_name,
|
||||||
|
"app_title": dlc.app_title,
|
||||||
|
"app_version": dlc.app_version(self.item.options.platform),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
print(
|
|
||||||
"Manually installing DLCs works the same; just use the DLC app name instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
# install_dlcs = QMessageBox.question(self, "", "Do you want to install the prequisites", QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes
|
if (
|
||||||
# TODO
|
self.item.download.game.supports_cloud_saves
|
||||||
if game.supports_cloud_saves and not game.is_dlc:
|
or self.item.download.game.supports_mac_cloud_saves
|
||||||
logger.info(
|
) and not self.item.download.game.is_dlc:
|
||||||
'This game supports cloud saves, syncing is handled by the "sync-saves" command.'
|
ret.sync_saves = True
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
f'To download saves for this game run "legendary sync-saves {game.app_name}"'
|
|
||||||
)
|
|
||||||
old_igame = self.core.get_installed_game(game.app_name)
|
|
||||||
if old_igame and self.repair and os.path.exists(self.repair_file):
|
|
||||||
if old_igame.needs_verification:
|
|
||||||
old_igame.needs_verification = False
|
|
||||||
self.core.install_game(old_igame)
|
|
||||||
|
|
||||||
logger.debug("Removing repair file.")
|
# show tip again after installation finishes so users hopefully actually see it
|
||||||
os.remove(self.repair_file)
|
if tip_url := self.core.get_game_tip(self.item.download.igame.app_name):
|
||||||
if old_igame and old_igame.install_tags != self.igame.install_tags:
|
ret.tip_url = tip_url
|
||||||
old_igame.install_tags = self.igame.install_tags
|
|
||||||
self.logger.info("Deleting now untagged files.")
|
|
||||||
self.core.uninstall_tag(old_igame)
|
|
||||||
self.core.install_game(old_igame)
|
|
||||||
|
|
||||||
if not self.queue_item.options.update and self.queue_item.options.create_shortcut:
|
LegendaryCLI(self.core).install_game_cleanup(
|
||||||
if not create_desktop_link(self.queue_item.options.app_name, self.core, "desktop"):
|
self.item.download.game,
|
||||||
# maybe add it to download summary, to show in finished downloads
|
self.item.download.igame,
|
||||||
pass
|
self.item.download.repair,
|
||||||
else:
|
self.item.download.repair_file,
|
||||||
logger.info("Desktop shortcut written")
|
)
|
||||||
|
|
||||||
self.status.emit("finish")
|
if not self.item.options.update and self.item.options.create_shortcut:
|
||||||
|
ret.shortcuts = True
|
||||||
|
|
||||||
|
self.ret_status.emit(ret)
|
||||||
|
|
||||||
def _handle_postinstall(self, postinstall, igame):
|
def _handle_postinstall(self, postinstall, igame):
|
||||||
logger.info(f"Postinstall info: {postinstall}")
|
logger.info("This game lists the following prequisites to be installed:")
|
||||||
|
logger.info(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
if self.queue_item.options.install_preqs:
|
if not self.item.options.install_preqs:
|
||||||
|
logger.info("Marking prerequisites as installed...")
|
||||||
|
self.core.prereq_installed(self.item.download.igame.app_name)
|
||||||
|
else:
|
||||||
|
logger.info("Launching prerequisite executable..")
|
||||||
self.core.prereq_installed(igame.app_name)
|
self.core.prereq_installed(igame.app_name)
|
||||||
req_path, req_exec = os.path.split(postinstall["path"])
|
req_path, req_exec = os.path.split(postinstall["path"])
|
||||||
work_dir = os.path.join(igame.install_path, req_path)
|
work_dir = os.path.join(igame.install_path, req_path)
|
||||||
|
@ -211,15 +152,14 @@ class DownloadThread(QThread):
|
||||||
proc = QProcess()
|
proc = QProcess()
|
||||||
proc.setProcessChannelMode(QProcess.MergedChannels)
|
proc.setProcessChannelMode(QProcess.MergedChannels)
|
||||||
proc.readyReadStandardOutput.connect(
|
proc.readyReadStandardOutput.connect(
|
||||||
lambda: logger.debug(
|
lambda: logger.debug(str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))
|
||||||
str(proc.readAllStandardOutput().data(), "utf-8", "ignore")
|
)
|
||||||
))
|
proc.setNativeArguments(postinstall.get("args", []))
|
||||||
proc.start(fullpath, postinstall.get("args", []))
|
proc.setWorkingDirectory(work_dir)
|
||||||
|
proc.start(fullpath)
|
||||||
proc.waitForFinished() # wait, because it is inside the thread
|
proc.waitForFinished() # wait, because it is inside the thread
|
||||||
else:
|
|
||||||
self.core.prereq_installed(self.igame.app_name)
|
|
||||||
else:
|
else:
|
||||||
logger.info("Automatic installation not available on Linux.")
|
logger.info("Automatic installation not available on Linux.")
|
||||||
|
|
||||||
def kill(self):
|
def kill(self):
|
||||||
self._kill = True
|
self.dlm_signals.kill = True
|
||||||
|
|
|
@ -139,6 +139,16 @@ class GamesTab(QStackedWidget):
|
||||||
self.view_stack.setCurrentWidget(self.icon_view_scroll)
|
self.view_stack.setCurrentWidget(self.icon_view_scroll)
|
||||||
|
|
||||||
self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x))
|
self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x))
|
||||||
|
self.head_bar.search_bar.textChanged.connect(
|
||||||
|
lambda x: self.icon_view_scroll.verticalScrollBar().setSliderPosition(
|
||||||
|
self.icon_view_scroll.verticalScrollBar().minimum()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.head_bar.search_bar.textChanged.connect(
|
||||||
|
lambda x: self.list_view_scroll.verticalScrollBar().setSliderPosition(
|
||||||
|
self.list_view_scroll.verticalScrollBar().minimum()
|
||||||
|
)
|
||||||
|
)
|
||||||
self.head_bar.filterChanged.connect(self.filter_games)
|
self.head_bar.filterChanged.connect(self.filter_games)
|
||||||
self.head_bar.refresh_list.clicked.connect(self.update_list)
|
self.head_bar.refresh_list.clicked.connect(self.update_list)
|
||||||
self.head_bar.view.toggled.connect(self.toggle_view)
|
self.head_bar.view.toggled.connect(self.toggle_view)
|
||||||
|
@ -320,8 +330,8 @@ class GamesTab(QStackedWidget):
|
||||||
visible = True
|
visible = True
|
||||||
|
|
||||||
if (
|
if (
|
||||||
search_text not in widget.game.app_name.lower()
|
search_text.lower() not in widget.game.app_name.lower()
|
||||||
and search_text not in widget.game.app_title.lower()
|
and search_text.lower() not in widget.game.app_title.lower()
|
||||||
):
|
):
|
||||||
opacity = 0.25
|
opacity = 0.25
|
||||||
else:
|
else:
|
||||||
|
@ -345,7 +355,7 @@ class GamesTab(QStackedWidget):
|
||||||
# lk: it sorts by installed then by title
|
# lk: it sorts by installed then by title
|
||||||
installing_widget = self.icon_view.layout().remove(type(self.installing_widget).__name__)
|
installing_widget = self.icon_view.layout().remove(type(self.installing_widget).__name__)
|
||||||
if sort_by:
|
if sort_by:
|
||||||
self.icon_view.layout().sort(lambda x: (sort_by not in x.widget().game.app_title.lower(),))
|
self.icon_view.layout().sort(lambda x: (sort_by.lower() not in x.widget().game.app_title.lower(),))
|
||||||
else:
|
else:
|
||||||
self.icon_view.layout().sort(
|
self.icon_view.layout().sort(
|
||||||
lambda x: (
|
lambda x: (
|
||||||
|
|
|
@ -11,7 +11,7 @@ from legendary.core import LegendaryCore
|
||||||
from legendary.models.game import SaveGameStatus, InstalledGame, SaveGameFile
|
from legendary.models.game import SaveGameStatus, InstalledGame, SaveGameFile
|
||||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
|
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
|
||||||
from rare.ui.components.dialogs.sync_save_dialog import Ui_SyncSaveDialog
|
from rare.ui.components.dialogs.sync_save_dialog import Ui_SyncSaveDialog
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("Cloud Saves")
|
logger = getLogger("Cloud Saves")
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||||
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
||||||
from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
|
from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
|
||||||
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
|
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
|
||||||
from rare.utils.models import InstallOptionsModel
|
from rare.models.install import InstallOptionsModel
|
||||||
from rare.widgets.image_widget import ImageWidget
|
from rare.widgets.image_widget import ImageWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,9 @@ from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
||||||
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.extra_widgets import PathEdit
|
from rare.utils.extra_widgets import PathEdit
|
||||||
from rare.utils.legendary_utils import VerifyWorker
|
from rare.utils.legendary_utils import VerifyWorker
|
||||||
from rare.utils.models import InstallOptionsModel
|
from rare.models.install import InstallOptionsModel
|
||||||
from rare.utils.steam_grades import SteamWorker
|
from rare.utils.steam_grades import SteamWorker
|
||||||
from rare.utils.utils import get_size
|
from rare.utils.misc import get_size
|
||||||
from rare.widgets.image_widget import ImageWidget
|
from rare.widgets.image_widget import ImageWidget
|
||||||
|
|
||||||
logger = getLogger("GameInfo")
|
logger = getLogger("GameInfo")
|
||||||
|
@ -117,77 +117,111 @@ class GameInfo(QWidget, Ui_GameInfo):
|
||||||
self.game_utils.update_list.emit(self.game.app_name)
|
self.game_utils.update_list.emit(self.game.app_name)
|
||||||
self.uninstalled.emit(self.game.app_name)
|
self.uninstalled.emit(self.game.app_name)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def repair(self):
|
def repair(self):
|
||||||
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.game.app_name}.repair")
|
""" This function is to be called from the button only """
|
||||||
|
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.igame.app_name}.repair")
|
||||||
if not os.path.exists(repair_file):
|
if not os.path.exists(repair_file):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"Warning",
|
self.tr("Error - {}").format(self.igame.title),
|
||||||
self.tr(
|
self.tr(
|
||||||
"Repair file does not exist or game does not need a repair. Please verify game first"
|
"Repair file does not exist or game does not need a repair. Please verify game first"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
self.repair_game(self.igame)
|
||||||
|
|
||||||
|
def repair_game(self, igame: InstalledGame):
|
||||||
|
game = self.core.get_game(igame.app_name)
|
||||||
|
ans = False
|
||||||
|
if igame.version != game.app_version(igame.platform):
|
||||||
|
ans = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
self.tr("Repair and update?"),
|
||||||
|
self.tr(
|
||||||
|
"There is an update for <b>{}</b> from <b>{}</b> to <b>{}</b>."
|
||||||
|
"Do you want to update the game while repairing it?"
|
||||||
|
).format(igame.title, igame.version, game.app_version(igame.platform)),
|
||||||
|
) == QMessageBox.Yes
|
||||||
self.signals.install_game.emit(
|
self.signals.install_game.emit(
|
||||||
InstallOptionsModel(app_name=self.game.app_name, repair=True, update=True)
|
InstallOptionsModel(
|
||||||
|
app_name=igame.app_name, repair_mode=True, repair_and_update=ans, update=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def verify(self):
|
def verify(self):
|
||||||
|
""" This function is to be called from the button only """
|
||||||
if not os.path.exists(self.igame.install_path):
|
if not os.path.exists(self.igame.install_path):
|
||||||
logger.error("Path does not exist")
|
logger.error(f"Installation path {self.igame.install_path} for {self.igame.title} does not exist")
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"Warning",
|
self.tr("Error - {}").format(self.igame.title),
|
||||||
self.tr("Installation path of {} does not exist. Cannot verify").format(self.igame.title),
|
self.tr("Installation path for <b>{}</b> does not exist. Cannot continue.").format(self.igame.title),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
self.verify_game(self.igame)
|
||||||
|
|
||||||
|
def verify_game(self, igame: InstalledGame):
|
||||||
self.verify_widget.setCurrentIndex(1)
|
self.verify_widget.setCurrentIndex(1)
|
||||||
verify_worker = VerifyWorker(self.game.app_name)
|
verify_worker = VerifyWorker(igame.app_name)
|
||||||
verify_worker.signals.status.connect(self.verify_statistics)
|
verify_worker.signals.status.connect(self.verify_status)
|
||||||
verify_worker.signals.summary.connect(self.finish_verify)
|
verify_worker.signals.result.connect(self.verify_result)
|
||||||
|
verify_worker.signals.error.connect(self.verify_error)
|
||||||
self.verify_progress.setValue(0)
|
self.verify_progress.setValue(0)
|
||||||
self.verify_threads[self.game.app_name] = verify_worker
|
self.verify_threads[igame.app_name] = verify_worker
|
||||||
self.verify_pool.start(verify_worker)
|
self.verify_pool.start(verify_worker)
|
||||||
self.move_button.setEnabled(False)
|
self.move_button.setEnabled(False)
|
||||||
|
|
||||||
def verify_statistics(self, num, total, app_name):
|
def verify_cleanup(self, app_name: str):
|
||||||
|
self.verify_widget.setCurrentIndex(0)
|
||||||
|
self.verify_threads.pop(app_name)
|
||||||
|
self.move_button.setEnabled(True)
|
||||||
|
self.verify_button.setEnabled(True)
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def verify_error(self, app_name, message):
|
||||||
|
self.verify_cleanup(app_name)
|
||||||
|
igame = self.core.get_installed_game(app_name)
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
self.tr("Error - {}").format(igame.title),
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
|
@pyqtSlot(str, int, int, float, float)
|
||||||
|
def verify_status(self, app_name, num, total, percentage, speed):
|
||||||
# checked, max, app_name
|
# checked, max, app_name
|
||||||
if app_name == self.game.app_name:
|
if app_name == self.game.app_name:
|
||||||
self.verify_progress.setValue(num * 100 // total)
|
self.verify_progress.setValue(num * 100 // total)
|
||||||
|
|
||||||
def finish_verify(self, failed, missing, app_name):
|
@pyqtSlot(str, bool, int, int)
|
||||||
if failed == missing == 0:
|
def verify_result(self, app_name, success, failed, missing):
|
||||||
|
self.verify_cleanup(app_name)
|
||||||
|
self.repair_button.setDisabled(success)
|
||||||
|
igame = self.core.get_installed_game(app_name)
|
||||||
|
if success:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"Summary",
|
self.tr("Summary - {}").format(igame.title),
|
||||||
"Game was verified successfully. No missing or corrupt files found",
|
self.tr("<b>{}</b> has been verified successfully. "
|
||||||
|
"No missing or corrupt files found").format(igame.title),
|
||||||
)
|
)
|
||||||
igame = self.core.get_installed_game(app_name)
|
self.verification_finished.emit(igame)
|
||||||
if igame.needs_verification:
|
|
||||||
igame.needs_verification = False
|
|
||||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
|
||||||
self.verification_finished.emit(igame)
|
|
||||||
elif failed == missing == -1:
|
|
||||||
QMessageBox.warning(self, "Warning", self.tr("Something went wrong"))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ans = QMessageBox.question(
|
ans = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Summary",
|
self.tr("Summary - {}").format(igame.title),
|
||||||
self.tr(
|
self.tr(
|
||||||
"Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?"
|
"Verification failed, <b>{}</b> file(s) corrupted, <b>{}</b> file(s) are missing. "
|
||||||
|
"Do you want to repair them?"
|
||||||
).format(failed, missing),
|
).format(failed, missing),
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.Yes,
|
QMessageBox.Yes,
|
||||||
)
|
)
|
||||||
if ans == QMessageBox.Yes:
|
if ans == QMessageBox.Yes:
|
||||||
self.signals.install_game.emit(
|
self.repair_game(igame)
|
||||||
InstallOptionsModel(app_name=app_name, repair=True, update=True)
|
|
||||||
)
|
|
||||||
self.verify_widget.setCurrentIndex(0)
|
|
||||||
self.verify_threads.pop(app_name)
|
|
||||||
self.move_button.setEnabled(True)
|
|
||||||
self.verify_button.setEnabled(True)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def move_game(self, dest_path):
|
def move_game(self, dest_path):
|
||||||
|
@ -318,7 +352,9 @@ class GameInfo(QWidget, Ui_GameInfo):
|
||||||
self.uninstall_button.setDisabled(False)
|
self.uninstall_button.setDisabled(False)
|
||||||
self.verify_button.setDisabled(False)
|
self.verify_button.setDisabled(False)
|
||||||
if not self.args.offline:
|
if not self.args.offline:
|
||||||
self.repair_button.setDisabled(False)
|
self.repair_button.setDisabled(
|
||||||
|
not os.path.exists(os.path.join(self.core.lgd.get_tmp_path(), f"{self.igame.app_name}.repair"))
|
||||||
|
)
|
||||||
self.game_actions_stack.setCurrentIndex(0)
|
self.game_actions_stack.setCurrentIndex(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -10,7 +10,7 @@ from rare.components.tabs.settings import DefaultGameSettings
|
||||||
from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
|
from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
|
||||||
from rare.utils import config_helper
|
from rare.utils import config_helper
|
||||||
from rare.utils.extra_widgets import PathEdit
|
from rare.utils.extra_widgets import PathEdit
|
||||||
from rare.utils.utils import icon, WineResolver, get_raw_save_path
|
from rare.utils.misc import icon, WineResolver, get_raw_save_path
|
||||||
|
|
||||||
logger = getLogger("GameSettings")
|
logger = getLogger("GameSettings")
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
||||||
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.extra_widgets import SideTabWidget
|
from rare.utils.extra_widgets import SideTabWidget
|
||||||
from rare.utils.json_formatter import QJsonModel
|
from rare.utils.json_formatter import QJsonModel
|
||||||
from rare.utils.models import InstallOptionsModel
|
from rare.models.install import InstallOptionsModel
|
||||||
from rare.utils.steam_grades import SteamWorker
|
from rare.utils.steam_grades import SteamWorker
|
||||||
from rare.widgets.image_widget import ImageWidget
|
from rare.widgets.image_widget import ImageWidget
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from rare.components.tabs.games import CloudSaveUtils
|
||||||
from rare.game_launch_helper import message_models
|
from rare.game_launch_helper import message_models
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||||
from rare.utils import legendary_utils
|
from rare.utils import legendary_utils
|
||||||
from rare.utils import utils
|
from rare.utils import misc
|
||||||
from rare.utils.meta import RareGameMeta
|
from rare.utils.meta import RareGameMeta
|
||||||
|
|
||||||
logger = getLogger("GameUtils")
|
logger = getLogger("GameUtils")
|
||||||
|
@ -24,7 +24,7 @@ class GameProcess(QObject):
|
||||||
game_launched = pyqtSignal(str)
|
game_launched = pyqtSignal(str)
|
||||||
tried_connections = 0
|
tried_connections = 0
|
||||||
|
|
||||||
def __init__(self, app_name: str, on_startup=False, always_ask_sync: bool= False):
|
def __init__(self, app_name: str, on_startup=False, always_ask_sync: bool = False):
|
||||||
super(GameProcess, self).__init__()
|
super(GameProcess, self).__init__()
|
||||||
self.app_name = app_name
|
self.app_name = app_name
|
||||||
self.on_startup = on_startup
|
self.on_startup = on_startup
|
||||||
|
@ -152,7 +152,7 @@ class GameUtils(QObject):
|
||||||
if not os.path.exists(igame.install_path):
|
if not os.path.exists(igame.install_path):
|
||||||
if QMessageBox.Yes == QMessageBox.question(
|
if QMessageBox.Yes == QMessageBox.question(
|
||||||
None,
|
None,
|
||||||
"Uninstall",
|
self.tr("Uninstall - {}").format(igame.title),
|
||||||
self.tr(
|
self.tr(
|
||||||
"Game files of {} do not exist. Remove it from installed games?"
|
"Game files of {} do not exist. Remove it from installed games?"
|
||||||
).format(igame.title),
|
).format(igame.title),
|
||||||
|
@ -164,10 +164,12 @@ class GameUtils(QObject):
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
infos = UninstallDialog(game).get_information()
|
proceed, keep_files, keep_config = UninstallDialog(game).get_options()
|
||||||
if infos == 0:
|
if not proceed:
|
||||||
return False
|
return False
|
||||||
legendary_utils.uninstall(game.app_name, self.core, infos)
|
success, message = legendary_utils.uninstall_game(self.core, game.app_name, keep_files, keep_config)
|
||||||
|
if not success:
|
||||||
|
QMessageBox.warning(None, self.tr("Uninstall - {}").format(igame.title), message, QMessageBox.Close)
|
||||||
self.signals.game_uninstalled.emit(app_name)
|
self.signals.game_uninstalled.emit(app_name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -206,7 +208,7 @@ class GameUtils(QObject):
|
||||||
wine_pfx: str = None,
|
wine_pfx: str = None,
|
||||||
ask_always_sync: bool = False,
|
ask_always_sync: bool = False,
|
||||||
):
|
):
|
||||||
executable = utils.get_rare_executable()
|
executable = misc.get_rare_executable()
|
||||||
executable, args = executable[0], executable[1:]
|
executable, args = executable[0], executable[1:]
|
||||||
args.extend([
|
args.extend([
|
||||||
"start", app_name
|
"start", app_name
|
||||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QFrame, QMessageBox, QAction
|
||||||
from rare.components.tabs.games.game_utils import GameUtils
|
from rare.components.tabs.games.game_utils import GameUtils
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||||
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
||||||
from rare.utils.utils import create_desktop_link
|
from rare.utils.misc import create_desktop_link
|
||||||
from rare.widgets.image_widget import ImageWidget
|
from rare.widgets.image_widget import ImageWidget
|
||||||
|
|
||||||
logger = getLogger("Game")
|
logger = getLogger("Game")
|
||||||
|
|
|
@ -9,7 +9,7 @@ from rare.components.tabs.games.game_widgets.base_installed_widget import (
|
||||||
)
|
)
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.shared.image_manager import ImageSize
|
from rare.shared.image_manager import ImageSize
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
from rare.widgets.elide_label import ElideLabel
|
from rare.widgets.elide_label import ElideLabel
|
||||||
|
|
||||||
logger = getLogger("GameWidgetInstalled")
|
logger = getLogger("GameWidgetInstalled")
|
||||||
|
@ -43,7 +43,7 @@ class InstalledIconWidget(BaseInstalledWidget):
|
||||||
minilayout.setSpacing(0)
|
minilayout.setSpacing(0)
|
||||||
miniwidget.setLayout(minilayout)
|
miniwidget.setLayout(minilayout)
|
||||||
|
|
||||||
self.title_label = ElideLabel(f"<h4>{self.game.app_title}</h4>", parent=miniwidget)
|
self.title_label = ElideLabel(f"<b>{self.game.app_title}</b>", parent=miniwidget)
|
||||||
self.title_label.setAlignment(Qt.AlignTop)
|
self.title_label.setAlignment(Qt.AlignTop)
|
||||||
self.title_label.setObjectName("game_widget")
|
self.title_label.setObjectName("game_widget")
|
||||||
minilayout.addWidget(self.title_label, stretch=2)
|
minilayout.addWidget(self.title_label, stretch=2)
|
||||||
|
|
|
@ -6,8 +6,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||||
from rare.components.tabs.games.game_widgets.base_installed_widget import (
|
from rare.components.tabs.games.game_widgets.base_installed_widget import (
|
||||||
BaseInstalledWidget,
|
BaseInstalledWidget,
|
||||||
)
|
)
|
||||||
from rare.utils.utils import get_size
|
from rare.utils.misc import icon, get_size
|
||||||
from rare.utils.utils import icon
|
|
||||||
|
|
||||||
logger = getLogger("GameWidget")
|
logger = getLogger("GameWidget")
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ class UninstalledIconWidget(BaseUninstalledWidget):
|
||||||
minilayout.setSpacing(0)
|
minilayout.setSpacing(0)
|
||||||
miniwidget.setLayout(minilayout)
|
miniwidget.setLayout(minilayout)
|
||||||
|
|
||||||
self.title_label = ElideLabel(f"<h4>{game.app_title}</h4>", parent=miniwidget)
|
self.title_label = ElideLabel(f"<b>{game.app_title}</b>", parent=miniwidget)
|
||||||
self.title_label.setAlignment(Qt.AlignTop)
|
self.title_label.setAlignment(Qt.AlignTop)
|
||||||
self.title_label.setObjectName("game_widget")
|
self.title_label.setObjectName("game_widget")
|
||||||
minilayout.addWidget(self.title_label, stretch=2)
|
minilayout.addWidget(self.title_label, stretch=2)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from qtawesome import IconWidget
|
||||||
|
|
||||||
from rare.shared import ApiResultsSingleton
|
from rare.shared import ApiResultsSingleton
|
||||||
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
|
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
|
|
||||||
class GameListHeadBar(QWidget):
|
class GameListHeadBar(QWidget):
|
||||||
|
|
|
@ -6,6 +6,7 @@ from typing import Tuple, Iterable, List
|
||||||
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
|
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox
|
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox
|
||||||
|
|
||||||
|
from rare.lgndr.api_exception import LgndrException
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
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_group import Ui_EGLSyncGroup
|
||||||
from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (
|
from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (
|
||||||
|
@ -13,7 +14,7 @@ from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (
|
||||||
)
|
)
|
||||||
from rare.utils.extra_widgets import PathEdit
|
from rare.utils.extra_widgets import PathEdit
|
||||||
from rare.utils.models import PathSpec
|
from rare.utils.models import PathSpec
|
||||||
from rare.utils.utils import WineResolver
|
from rare.utils.misc import WineResolver
|
||||||
|
|
||||||
logger = getLogger("EGLSync")
|
logger = getLogger("EGLSync")
|
||||||
|
|
||||||
|
@ -183,11 +184,18 @@ class EGLSyncListItem(QListWidgetItem):
|
||||||
def is_checked(self) -> bool:
|
def is_checked(self) -> bool:
|
||||||
return self.checkState() == Qt.Checked
|
return self.checkState() == Qt.Checked
|
||||||
|
|
||||||
def action(self) -> None:
|
def action(self) -> str:
|
||||||
|
error = ""
|
||||||
if self.export:
|
if self.export:
|
||||||
error = self.core.egl_export(self.game.app_name)
|
try:
|
||||||
|
self.core.egl_export(self.game.app_name)
|
||||||
|
except LgndrException as ret:
|
||||||
|
error = ret.message
|
||||||
else:
|
else:
|
||||||
error = self.core.egl_import(self.game.app_name)
|
try:
|
||||||
|
self.core.egl_import(self.game.app_name)
|
||||||
|
except LgndrException as ret:
|
||||||
|
error = ret.message
|
||||||
return error
|
return error
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -307,85 +315,3 @@ class EGLSyncWorker(QRunnable):
|
||||||
def run(self):
|
def run(self):
|
||||||
self.import_list.action()
|
self.import_list.action()
|
||||||
self.export_list.action()
|
self.export_list.action()
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QCheckBox, QPushButton, QDialog
|
|
||||||
|
|
||||||
|
|
||||||
class DisableSyncDialog(QDialog):
|
|
||||||
info = 1, False
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super(DisableSyncDialog, self).__init__(parent=parent)
|
|
||||||
self.layout = QVBoxLayout()
|
|
||||||
|
|
||||||
self.question = QLabel(self.tr("Do you really want to disable sync with Epic Games Store"))
|
|
||||||
self.layout.addWidget(self.question)
|
|
||||||
|
|
||||||
self.remove_metadata = QCheckBox(self.tr("Remove metadata from installed games"))
|
|
||||||
self.layout.addWidget(self.remove_metadata)
|
|
||||||
|
|
||||||
self.button_layout = QHBoxLayout()
|
|
||||||
self.button_layout.addStretch(1)
|
|
||||||
|
|
||||||
self.ok_button = QPushButton(self.tr("Ok"))
|
|
||||||
self.cancel_button = QPushButton(self.tr("Cancel"))
|
|
||||||
|
|
||||||
self.ok_button.clicked.connect(self.ok)
|
|
||||||
self.cancel_button.clicked.connect(self.cancel)
|
|
||||||
|
|
||||||
self.button_layout.addWidget(self.ok_button)
|
|
||||||
self.button_layout.addWidget(self.cancel_button)
|
|
||||||
|
|
||||||
self.layout.addStretch(1)
|
|
||||||
self.layout.addLayout(self.button_layout)
|
|
||||||
|
|
||||||
self.setLayout(self.layout)
|
|
||||||
|
|
||||||
def ok(self):
|
|
||||||
self.info = 0, self.remove_metadata.isChecked()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def get_information(self):
|
|
||||||
self.exec_()
|
|
||||||
return self.info
|
|
||||||
|
|
||||||
|
|
||||||
class EGLSyncItemWidget(QGroupBox):
|
|
||||||
def __init__(self, game, export: bool, parent=None):
|
|
||||||
super(EGLSyncItemWidget, self).__init__(parent=parent)
|
|
||||||
self.layout = QHBoxLayout()
|
|
||||||
self.export = export
|
|
||||||
self.game = game
|
|
||||||
if export:
|
|
||||||
self.app_title_label = QLabel(game.title)
|
|
||||||
else:
|
|
||||||
title = self.core.get_game(game.app_name).app_title
|
|
||||||
self.app_title_label = QLabel(title)
|
|
||||||
self.layout.addWidget(self.app_title_label)
|
|
||||||
self.button = QPushButton(self.tr("Export") if export else self.tr("Import"))
|
|
||||||
|
|
||||||
if export:
|
|
||||||
self.button.clicked.connect(self.export_game)
|
|
||||||
else:
|
|
||||||
self.button.clicked.connect(self.import_game)
|
|
||||||
|
|
||||||
self.layout.addWidget(self.button)
|
|
||||||
self.setLayout(self.layout)
|
|
||||||
|
|
||||||
def export_game(self):
|
|
||||||
self.core.egl_export(self.game.app_name)
|
|
||||||
# FIXME: on update_egl_widget this is going to crash because
|
|
||||||
# FIXME: the item is not removed from the list in the python's side
|
|
||||||
self.deleteLater()
|
|
||||||
|
|
||||||
def import_game(self):
|
|
||||||
self.core.egl_import(self.game.app_name)
|
|
||||||
# FIXME: on update_egl_widget this is going to crash because
|
|
||||||
# FIXME: the item is not removed from the list in the python's side
|
|
||||||
self.deleteLater()
|
|
||||||
"""
|
|
||||||
|
|
|
@ -10,10 +10,13 @@ from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThrea
|
||||||
from PyQt5.QtGui import QStandardItemModel
|
from PyQt5.QtGui import QStandardItemModel
|
||||||
from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox
|
from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox
|
||||||
|
|
||||||
|
from rare.lgndr.cli import LegendaryCLI
|
||||||
|
from rare.lgndr.api_arguments import LgndrImportGameArgs
|
||||||
|
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
|
||||||
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
|
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
|
||||||
from rare.utils import legendary_utils
|
|
||||||
from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit
|
from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit
|
||||||
|
from rare.widgets.elide_label import ElideLabel
|
||||||
|
|
||||||
logger = getLogger("Import")
|
logger = getLogger("Import")
|
||||||
|
|
||||||
|
@ -25,8 +28,7 @@ def find_app_name(path: str, core) -> Optional[str]:
|
||||||
with open(os.path.join(path, ".egstore", i)) as file:
|
with open(os.path.join(path, ".egstore", i)) as file:
|
||||||
app_name = json.load(file).get("AppName")
|
app_name = json.load(file).get("AppName")
|
||||||
return app_name
|
return app_name
|
||||||
elif app_name := legendary_utils.resolve_aliases(
|
elif app_name := LegendaryCLI(core).resolve_aliases(os.path.basename(os.path.normpath(path))):
|
||||||
core, os.path.basename(os.path.normpath(path))):
|
|
||||||
# return None if game does not exist (Workaround for overlay)
|
# return None if game does not exist (Workaround for overlay)
|
||||||
if not core.get_game(app_name):
|
if not core.get_game(app_name):
|
||||||
return None
|
return None
|
||||||
|
@ -45,7 +47,9 @@ class ImportResult(IntEnum):
|
||||||
@dataclass
|
@dataclass
|
||||||
class ImportedGame:
|
class ImportedGame:
|
||||||
result: ImportResult
|
result: ImportResult
|
||||||
|
path: Optional[str] = None
|
||||||
app_name: Optional[str] = None
|
app_name: Optional[str] = None
|
||||||
|
app_title: Optional[str] = None
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,14 +58,15 @@ class ImportWorker(QRunnable):
|
||||||
finished = pyqtSignal(list)
|
finished = pyqtSignal(list)
|
||||||
progress = pyqtSignal(int)
|
progress = pyqtSignal(int)
|
||||||
|
|
||||||
def __init__(self, path: str, import_folder: bool = False, app_name: str = None):
|
def __init__(self, path: str, app_name: str = None, import_folder: bool = False, import_dlcs: bool = False):
|
||||||
super(ImportWorker, self).__init__()
|
super(ImportWorker, self).__init__()
|
||||||
self.signals = self.Signals()
|
self.signals = self.Signals()
|
||||||
self.core = LegendaryCoreSingleton()
|
self.core = LegendaryCoreSingleton()
|
||||||
|
|
||||||
self.path = Path(path)
|
self.path = Path(path)
|
||||||
self.import_folder = import_folder
|
|
||||||
self.app_name = app_name
|
self.app_name = app_name
|
||||||
self.tr = lambda message: qApp.translate("ImportThread", message)
|
self.import_folder = import_folder
|
||||||
|
self.import_dlcs = import_dlcs
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
result_list: List = []
|
result_list: List = []
|
||||||
|
@ -80,26 +85,30 @@ class ImportWorker(QRunnable):
|
||||||
self.signals.finished.emit(result_list)
|
self.signals.finished.emit(result_list)
|
||||||
|
|
||||||
def __try_import(self, path: Path, app_name: str = None) -> ImportedGame:
|
def __try_import(self, path: Path, app_name: str = None) -> ImportedGame:
|
||||||
result = ImportedGame(ImportResult.ERROR, None, None)
|
result = ImportedGame(ImportResult.ERROR)
|
||||||
|
result.path = str(path)
|
||||||
if app_name or (app_name := find_app_name(str(path), self.core)):
|
if app_name or (app_name := find_app_name(str(path), self.core)):
|
||||||
result.app_name = app_name
|
result.app_name = app_name
|
||||||
err = self.__import_game(app_name, path)
|
result.app_title = app_title = self.core.get_game(app_name).app_title
|
||||||
if err:
|
success, message = self.__import_game(path, app_name, app_title)
|
||||||
|
if not success:
|
||||||
result.result = ImportResult.FAILED
|
result.result = ImportResult.FAILED
|
||||||
result.message = err
|
result.message = message
|
||||||
else:
|
else:
|
||||||
result.result = ImportResult.SUCCESS
|
result.result = ImportResult.SUCCESS
|
||||||
else:
|
|
||||||
result.message = self.tr("Could not find AppName for {}").format(str(path))
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __import_game(self, app_name: str, path: Path) -> str:
|
def __import_game(self, path: Path, app_name: str, app_title: str):
|
||||||
if not (err := legendary_utils.import_game(self.core, app_name=app_name, path=str(path))):
|
cli = LegendaryCLI(self.core)
|
||||||
igame = self.core.get_installed_game(app_name)
|
status = LgndrIndirectStatus()
|
||||||
logger.info(f"Successfully imported {igame.title}")
|
args = LgndrImportGameArgs(
|
||||||
return ""
|
app_path=str(path),
|
||||||
else:
|
app_name=app_name,
|
||||||
return err
|
indirect_status=status,
|
||||||
|
get_boolean_choice=lambda prompt, default=True: self.import_dlcs
|
||||||
|
)
|
||||||
|
cli.import_game(args)
|
||||||
|
return status.success, status.message
|
||||||
|
|
||||||
|
|
||||||
class AppNameCompleter(QCompleter):
|
class AppNameCompleter(QCompleter):
|
||||||
|
@ -181,17 +190,17 @@ class ImportGroup(QGroupBox):
|
||||||
self.app_name_edit.textChanged.connect(self.app_name_changed)
|
self.app_name_edit.textChanged.connect(self.app_name_changed)
|
||||||
self.ui.app_name_layout.addWidget(self.app_name_edit)
|
self.ui.app_name_layout.addWidget(self.app_name_edit)
|
||||||
|
|
||||||
|
self.ui.import_folder_check.stateChanged.connect(self.import_folder_changed)
|
||||||
|
self.ui.import_dlcs_check.setEnabled(False)
|
||||||
|
|
||||||
self.ui.import_button.setEnabled(False)
|
self.ui.import_button.setEnabled(False)
|
||||||
self.ui.import_button.clicked.connect(
|
self.ui.import_button.clicked.connect(
|
||||||
lambda: self.import_pressed(self.path_edit.text())
|
lambda: self.import_pressed(self.path_edit.text())
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ui.import_folder_check.stateChanged.connect(
|
self.info_label = ElideLabel(text="", parent=self)
|
||||||
lambda s: self.ui.import_button.setEnabled(s or (not s and self.app_name_edit.is_valid))
|
self.ui.button_info_layout.addWidget(self.info_label)
|
||||||
)
|
|
||||||
self.ui.import_folder_check.stateChanged.connect(
|
|
||||||
lambda s: self.app_name_edit.setEnabled(not s)
|
|
||||||
)
|
|
||||||
self.threadpool = QThreadPool.globalInstance()
|
self.threadpool = QThreadPool.globalInstance()
|
||||||
|
|
||||||
def path_edit_cb(self, path) -> Tuple[bool, str, str]:
|
def path_edit_cb(self, path) -> Tuple[bool, str, str]:
|
||||||
|
@ -205,8 +214,8 @@ class ImportGroup(QGroupBox):
|
||||||
return False, path, ""
|
return False, path, ""
|
||||||
|
|
||||||
def path_changed(self, path):
|
def path_changed(self, path):
|
||||||
self.ui.info_label.setText("")
|
self.info_label.setText("")
|
||||||
self.ui.import_folder_check.setChecked(False)
|
self.ui.import_folder_check.setCheckState(Qt.Unchecked)
|
||||||
if self.path_edit.is_valid:
|
if self.path_edit.is_valid:
|
||||||
self.app_name_edit.setText(find_app_name(path, self.core))
|
self.app_name_edit.setText(find_app_name(path, self.core))
|
||||||
else:
|
else:
|
||||||
|
@ -220,17 +229,36 @@ class ImportGroup(QGroupBox):
|
||||||
else:
|
else:
|
||||||
return False, text, IndicatorLineEdit.reasons.game_not_installed
|
return False, text, IndicatorLineEdit.reasons.game_not_installed
|
||||||
|
|
||||||
def app_name_changed(self, text):
|
def app_name_changed(self, app_name: str):
|
||||||
self.ui.info_label.setText("")
|
self.info_label.setText("")
|
||||||
|
self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
|
||||||
if self.app_name_edit.is_valid:
|
if self.app_name_edit.is_valid:
|
||||||
|
self.ui.import_dlcs_check.setEnabled(
|
||||||
|
bool(self.core.get_dlc_for_game(app_name))
|
||||||
|
)
|
||||||
self.ui.import_button.setEnabled(True)
|
self.ui.import_button.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
|
self.ui.import_dlcs_check.setEnabled(False)
|
||||||
self.ui.import_button.setEnabled(False)
|
self.ui.import_button.setEnabled(False)
|
||||||
|
|
||||||
|
def import_folder_changed(self, state):
|
||||||
|
self.app_name_edit.setEnabled(not state)
|
||||||
|
self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
|
||||||
|
self.ui.import_dlcs_check.setEnabled(
|
||||||
|
state
|
||||||
|
or (self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(self.app_name_edit.text())))
|
||||||
|
)
|
||||||
|
self.ui.import_button.setEnabled(state or (not state and self.app_name_edit.is_valid))
|
||||||
|
|
||||||
def import_pressed(self, path=None):
|
def import_pressed(self, path=None):
|
||||||
if not path:
|
if not path:
|
||||||
path = self.path_edit.text()
|
path = self.path_edit.text()
|
||||||
worker = ImportWorker(path, self.ui.import_folder_check.isChecked(), self.app_name_edit.text())
|
worker = ImportWorker(
|
||||||
|
path,
|
||||||
|
self.app_name_edit.text(),
|
||||||
|
self.ui.import_folder_check.isChecked(),
|
||||||
|
self.ui.import_dlcs_check.isChecked(),
|
||||||
|
)
|
||||||
worker.signals.finished.connect(self.import_finished)
|
worker.signals.finished.connect(self.import_finished)
|
||||||
worker.signals.progress.connect(self.import_progress)
|
worker.signals.progress.connect(self.import_progress)
|
||||||
self.threadpool.start(worker)
|
self.threadpool.start(worker)
|
||||||
|
@ -251,16 +279,16 @@ class ImportGroup(QGroupBox):
|
||||||
if len(result) == 1:
|
if len(result) == 1:
|
||||||
res = result[0]
|
res = result[0]
|
||||||
if res.result == ImportResult.SUCCESS:
|
if res.result == ImportResult.SUCCESS:
|
||||||
self.ui.info_label.setText(
|
self.info_label.setText(
|
||||||
self.tr("{} was imported successfully").format(self.core.get_game(res.app_name).app_title)
|
self.tr("Success: <b>{}</b> imported").format(res.app_title)
|
||||||
)
|
)
|
||||||
elif res.result == ImportResult.FAILED:
|
elif res.result == ImportResult.FAILED:
|
||||||
self.ui.info_label.setText(
|
self.info_label.setText(
|
||||||
self.tr("Failed: {}").format(res.message)
|
self.tr("Failed: <b>{}</b> - {}").format(res.app_title, res.message)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.ui.info_label.setText(
|
self.info_label.setText(
|
||||||
self.tr("Error: {}").format(res.message)
|
self.tr("Error: Could not find AppName for <b>{}</b>").format(res.path)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
success = [r for r in result if r.result == ImportResult.SUCCESS]
|
success = [r for r in result if r.result == ImportResult.SUCCESS]
|
||||||
|
@ -280,15 +308,15 @@ class ImportGroup(QGroupBox):
|
||||||
details: List = []
|
details: List = []
|
||||||
for res in success:
|
for res in success:
|
||||||
details.append(
|
details.append(
|
||||||
self.tr("{} was imported successfully").format(self.core.get_game(res.app_name).app_title)
|
self.tr("Success: {} imported").format(res.app_title)
|
||||||
)
|
)
|
||||||
for res in failure:
|
for res in failure:
|
||||||
details.append(
|
details.append(
|
||||||
self.tr("Failed: {}").format(res.message)
|
self.tr("Failed: {} - {}").format(res.app_title, res.message)
|
||||||
)
|
)
|
||||||
for res in errored:
|
for res in errored:
|
||||||
details.append(
|
details.append(
|
||||||
self.tr("Error: {}").format(res.message)
|
self.tr("Error: Could not find AppName for {}").format(res.path)
|
||||||
)
|
)
|
||||||
messagebox.setDetailedText("\n".join(details))
|
messagebox.setDetailedText("\n".join(details))
|
||||||
messagebox.show()
|
messagebox.show()
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import platform
|
from rare.components.tabs.settings.widgets.linux import LinuxSettings
|
||||||
|
|
||||||
from rare.utils.extra_widgets import SideTabWidget
|
from rare.utils.extra_widgets import SideTabWidget
|
||||||
from .about import About
|
from .about import About
|
||||||
from .legendary import LegendarySettings
|
|
||||||
from rare.components.tabs.settings.widgets.linux import LinuxSettings
|
|
||||||
from .rare import RareSettings
|
|
||||||
from .default_game_settings import DefaultGameSettings
|
from .default_game_settings import DefaultGameSettings
|
||||||
|
from .legendary import LegendarySettings
|
||||||
|
from .rare import RareSettings
|
||||||
|
|
||||||
|
|
||||||
class SettingsTab(SideTabWidget):
|
class SettingsTab(SideTabWidget):
|
||||||
|
|
|
@ -11,7 +11,7 @@ from rare.components.tabs.settings.widgets.ubisoft_activation import UbiActivati
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
|
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
|
||||||
from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit
|
from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit
|
||||||
from rare.utils.utils import get_size
|
from rare.utils.misc import get_size
|
||||||
|
|
||||||
logger = getLogger("LegendarySettings")
|
logger = getLogger("LegendarySettings")
|
||||||
|
|
||||||
|
@ -174,10 +174,10 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
||||||
if not keep_manifests:
|
if not keep_manifests:
|
||||||
logger.debug("Removing manifests...")
|
logger.debug("Removing manifests...")
|
||||||
installed = [
|
installed = [
|
||||||
(ig.app_name, ig.version) for ig in self.core.get_installed_list()
|
(ig.app_name, ig.version, ig.platform) for ig in self.core.get_installed_list()
|
||||||
]
|
]
|
||||||
installed.extend(
|
installed.extend(
|
||||||
(ig.app_name, ig.version) for ig in self.core.get_installed_dlc_list()
|
(ig.app_name, ig.version, ig.platform) for ig in self.core.get_installed_dlc_list()
|
||||||
)
|
)
|
||||||
self.core.lgd.clean_manifests(installed)
|
self.core.lgd.clean_manifests(installed)
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,15 @@ from PyQt5.QtWidgets import QWidget, QMessageBox
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.components.tabs.settings.widgets.rpc import RPCSettings
|
from rare.components.tabs.settings.widgets.rpc import RPCSettings
|
||||||
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
|
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
|
||||||
from rare.utils import utils
|
|
||||||
from rare.utils.paths import cache_dir
|
from rare.utils.paths import cache_dir
|
||||||
from rare.utils.utils import (
|
from rare.utils.misc import (
|
||||||
get_translations,
|
get_translations,
|
||||||
get_color_schemes,
|
get_color_schemes,
|
||||||
set_color_pallete,
|
set_color_pallete,
|
||||||
get_style_sheets,
|
get_style_sheets,
|
||||||
set_style_sheet,
|
set_style_sheet,
|
||||||
|
get_size,
|
||||||
|
create_desktop_link,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = getLogger("RareSettings")
|
logger = getLogger("RareSettings")
|
||||||
|
@ -148,7 +149,7 @@ class RareSettings(QWidget, Ui_RareSettings):
|
||||||
for i in os.listdir(logdir):
|
for i in os.listdir(logdir):
|
||||||
size += os.path.getsize(os.path.join(logdir, i))
|
size += os.path.getsize(os.path.join(logdir, i))
|
||||||
|
|
||||||
self.log_dir_size_label.setText(utils.get_size(size))
|
self.log_dir_size_label.setText(get_size(size))
|
||||||
# self.log_dir_clean_button.setVisible(False)
|
# self.log_dir_clean_button.setVisible(False)
|
||||||
# self.log_dir_size_label.setVisible(False)
|
# self.log_dir_size_label.setVisible(False)
|
||||||
|
|
||||||
|
@ -160,7 +161,7 @@ class RareSettings(QWidget, Ui_RareSettings):
|
||||||
def create_start_menu_link(self):
|
def create_start_menu_link(self):
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(self.start_menu_link):
|
if not os.path.exists(self.start_menu_link):
|
||||||
utils.create_desktop_link(type_of_link="start_menu", for_rare=True)
|
create_desktop_link(type_of_link="start_menu", for_rare=True)
|
||||||
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
||||||
else:
|
else:
|
||||||
os.remove(self.start_menu_link)
|
os.remove(self.start_menu_link)
|
||||||
|
@ -169,23 +170,24 @@ class RareSettings(QWidget, Ui_RareSettings):
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"Error",
|
self.tr("Error"),
|
||||||
f"Permission error, cannot remove {self.start_menu_link}",
|
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_desktop_link(self):
|
def create_desktop_link(self):
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(self.desktop_file):
|
if not os.path.exists(self.desktop_file):
|
||||||
utils.create_desktop_link(type_of_link="desktop", for_rare=True)
|
create_desktop_link(type_of_link="desktop", for_rare=True)
|
||||||
self.desktop_link_btn.setText(self.tr("Remove Desktop link"))
|
self.desktop_link_btn.setText(self.tr("Remove Desktop link"))
|
||||||
else:
|
else:
|
||||||
os.remove(self.desktop_file)
|
os.remove(self.desktop_file)
|
||||||
self.desktop_link_btn.setText(self.tr("Create desktop link"))
|
self.desktop_link_btn.setText(self.tr("Create desktop link"))
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
|
logger.error(str(e))
|
||||||
logger.warning(
|
logger.warning(
|
||||||
self,
|
self,
|
||||||
"Error",
|
self.tr("Error"),
|
||||||
f"Permission error, cannot remove {self.desktop_file}",
|
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_color_select_changed(self, color):
|
def on_color_select_changed(self, color):
|
||||||
|
|
|
@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QGroupBox, QTableWidgetItem, QMessageBox, QPushButto
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.ui.components.tabs.settings.widgets.env_vars import Ui_EnvVars
|
from rare.ui.components.tabs.settings.widgets.env_vars import Ui_EnvVars
|
||||||
from rare.utils import config_helper
|
from rare.utils import config_helper
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("EnvVars")
|
logger = getLogger("EnvVars")
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QGroupBox, QMessageBox
|
||||||
from legendary.utils import eos
|
from legendary.utils import eos
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||||
from rare.ui.components.tabs.settings.widgets.eos_widget import Ui_EosWidget
|
from rare.ui.components.tabs.settings.widgets.eos_widget import Ui_EosWidget
|
||||||
from rare.utils.models import InstallOptionsModel
|
from rare.models.install import InstallOptionsModel
|
||||||
|
|
||||||
logger = getLogger("EOS")
|
logger = getLogger("EOS")
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QPushButt
|
||||||
|
|
||||||
from legendary.models.game import Game
|
from legendary.models.game import Game
|
||||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
|
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("Ubisoft")
|
logger = getLogger("Ubisoft")
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QInputDialog, QFra
|
||||||
from rare import shared
|
from rare import shared
|
||||||
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
|
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
|
||||||
from rare.utils import config_helper
|
from rare.utils import config_helper
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("Wrapper Settings")
|
logger = getLogger("Wrapper Settings")
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.components.tabs.shop.shop_models import ShopGame
|
from rare.components.tabs.shop.shop_models import ShopGame
|
||||||
from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info
|
from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info
|
||||||
from rare.utils.extra_widgets import WaitingSpinner, ImageLabel
|
from rare.utils.extra_widgets import WaitingSpinner, ImageLabel
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = logging.getLogger("ShopInfo")
|
logger = logging.getLogger("ShopInfo")
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout
|
||||||
from rare.components.tabs.shop.shop_models import ImageUrlModel
|
from rare.components.tabs.shop.shop_models import ImageUrlModel
|
||||||
from rare.ui.components.tabs.store.wishlist_widget import Ui_WishlistWidget
|
from rare.ui.components.tabs.store.wishlist_widget import Ui_WishlistWidget
|
||||||
from rare.utils.extra_widgets import ImageLabel
|
from rare.utils.extra_widgets import ImageLabel
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = logging.getLogger("GameWidgets")
|
logger = logging.getLogger("GameWidgets")
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from rare.components.tabs.shop import ShopApiCore
|
||||||
from rare.components.tabs.shop.game_widgets import WishlistWidget
|
from rare.components.tabs.shop.game_widgets import WishlistWidget
|
||||||
from rare.ui.components.tabs.store.wishlist import Ui_Wishlist
|
from rare.ui.components.tabs.store.wishlist import Ui_Wishlist
|
||||||
from rare.utils.extra_widgets import WaitingSpinner
|
from rare.utils.extra_widgets import WaitingSpinner
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
|
|
||||||
class Wishlist(QStackedWidget, Ui_Wishlist):
|
class Wishlist(QStackedWidget, Ui_Wishlist):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from PyQt5.QtCore import QSize
|
from PyQt5.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import QTabBar, QToolButton
|
from PyQt5.QtWidgets import QTabBar, QToolButton
|
||||||
|
|
||||||
from rare.utils.utils import icon
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
|
|
||||||
class MainTabBar(QTabBar):
|
class MainTabBar(QTabBar):
|
||||||
|
|
|
@ -69,7 +69,7 @@ class GameProcessApp(RareApp):
|
||||||
self.game_process = QProcess()
|
self.game_process = QProcess()
|
||||||
self.app_name = app_name
|
self.app_name = app_name
|
||||||
self.logger = getLogger(self.app_name)
|
self.logger = getLogger(self.app_name)
|
||||||
self.core = LegendaryCoreSingleton(True)
|
self.core = LegendaryCoreSingleton(init=True)
|
||||||
|
|
||||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||||
self.load_translator(lang)
|
self.load_translator(lang)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 50f71cbd9b2ae0b31615e8f7d8d8595922d3cdf3
|
|
5
rare/lgndr/__init__.py
Normal file
5
rare/lgndr/__init__.py
Normal 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.
|
||||||
|
"""
|
136
rare/lgndr/api_arguments.py
Normal file
136
rare/lgndr/api_arguments.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import Callable, List, Optional, Dict
|
||||||
|
|
||||||
|
from .api_monkeys import (
|
||||||
|
LgndrIndirectStatus,
|
||||||
|
GetBooleanChoiceProtocol,
|
||||||
|
get_boolean_choice,
|
||||||
|
verify_stdout,
|
||||||
|
DLManagerSignals
|
||||||
|
)
|
||||||
|
from .downloading import UIUpdate
|
||||||
|
|
||||||
|
"""
|
||||||
|
@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
|
||||||
|
# Rare: Extra arguments
|
||||||
|
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||||
|
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LgndrUninstallGameArgs:
|
||||||
|
app_name: str
|
||||||
|
keep_files: bool = False
|
||||||
|
yes: bool = False
|
||||||
|
# Rare: Extra arguments
|
||||||
|
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||||
|
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LgndrVerifyGameArgs:
|
||||||
|
app_name: str
|
||||||
|
# Rare: Extra arguments
|
||||||
|
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||||
|
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LgndrInstallGameArgs:
|
||||||
|
app_name: str
|
||||||
|
base_path: str = ""
|
||||||
|
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: Optional[List[str]] = 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
|
||||||
|
# FIXME: move to LgndrInstallGameRealArgs
|
||||||
|
skip_dlcs: bool = False
|
||||||
|
with_dlcs: bool = False
|
||||||
|
# end of FIXME
|
||||||
|
yes: bool = True
|
||||||
|
# Rare: Extra arguments
|
||||||
|
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||||
|
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||||
|
sdl_prompt: Callable[[str, str], List[str]] = lambda app_name, title: [""]
|
||||||
|
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
|
||||||
|
|
||||||
|
# def __post_init__(self):
|
||||||
|
# if self.sdl_prompt is None:
|
||||||
|
# self.sdl_prompt: Callable[[str, str], list] = \
|
||||||
|
# lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LgndrInstallGameRealArgs:
|
||||||
|
app_name: str
|
||||||
|
platform: str = "Windows"
|
||||||
|
repair_mode: bool = False
|
||||||
|
repair_file: str = ""
|
||||||
|
no_install: bool = False
|
||||||
|
save_path: str = ""
|
||||||
|
skip_dlcs: bool = False
|
||||||
|
with_dlcs: bool = False
|
||||||
|
dlm_debug: bool = False
|
||||||
|
yes: bool = False
|
||||||
|
# Rare: Extra arguments
|
||||||
|
install_preqs: bool = False
|
||||||
|
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||||
|
ui_update: Callable[[UIUpdate], None] = lambda ui: None
|
||||||
|
dlm_signals: DLManagerSignals = DLManagerSignals()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LgndrInstallGameRealRet:
|
||||||
|
class ReturnCode(IntEnum):
|
||||||
|
ERROR = 1
|
||||||
|
STOPPED = 2
|
||||||
|
FINISHED = 3
|
||||||
|
|
||||||
|
app_name: str
|
||||||
|
ret_code: ReturnCode = ReturnCode.ERROR
|
||||||
|
message: str = ""
|
||||||
|
dlcs: Optional[List[Dict]] = None
|
||||||
|
sync_saves: bool = False
|
||||||
|
tip_url: str = ""
|
||||||
|
shortcuts: bool = False
|
32
rare/lgndr/api_exception.py
Normal file
32
rare/lgndr/api_exception.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
class LgndrException(RuntimeError):
|
||||||
|
def __init__(self, message="Error in Legendary"):
|
||||||
|
self.message = message
|
||||||
|
super(LgndrException, self).__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class LgndrWarning(RuntimeWarning):
|
||||||
|
def __init__(self, message="Warning in Legendary"):
|
||||||
|
self.message = message
|
||||||
|
super(LgndrWarning, self).__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class LgndrCLILogHandler(logging.Handler):
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
# lk: FATAL is the same as CRITICAL
|
||||||
|
if record.levelno == logging.ERROR or record.levelno == logging.CRITICAL:
|
||||||
|
raise LgndrException(record.getMessage())
|
||||||
|
# if record.levelno < logging.ERROR or record.levelno == logging.WARNING:
|
||||||
|
# warnings.warn(record.getMessage())
|
||||||
|
|
||||||
|
|
||||||
|
class LgndrCoreLogHandler(logging.Handler):
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
# lk: FATAL is the same as CRITICAL
|
||||||
|
if record.levelno == logging.CRITICAL:
|
||||||
|
raise LgndrException(record.getMessage())
|
||||||
|
# if record.levelno < logging.CRITICAL:
|
||||||
|
# warnings.warn(record.getMessage())
|
106
rare/lgndr/api_monkeys.py
Normal file
106
rare/lgndr/api_monkeys.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from typing_extensions import Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class GetBooleanChoiceProtocol(Protocol):
|
||||||
|
def __call__(self, prompt: str, default: bool = ...) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def get_boolean_choice(prompt: str, default: bool = True) -> bool:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def verify_stdout(a0: int, a1: int, a2: float, a3: float) -> None:
|
||||||
|
print(f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r")
|
||||||
|
|
||||||
|
|
||||||
|
class DLManagerSignals:
|
||||||
|
_kill = False
|
||||||
|
_update = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kill(self) -> bool:
|
||||||
|
self._update = False
|
||||||
|
return self._kill
|
||||||
|
|
||||||
|
@kill.setter
|
||||||
|
def kill(self, value: bool) -> None:
|
||||||
|
self._update = True
|
||||||
|
self._kill = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update(self) -> bool:
|
||||||
|
_update = self._update
|
||||||
|
self._update = False
|
||||||
|
return _update
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LgndrIndirectStatus:
|
||||||
|
success: bool = False
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
if self.message:
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.success
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
if item == 0:
|
||||||
|
return self.success
|
||||||
|
elif item == 1:
|
||||||
|
return self.message
|
||||||
|
else:
|
||||||
|
raise IndexError
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter((self.success, self.message))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class LgndrIndirectLogger:
|
||||||
|
def __init__(
|
||||||
|
self, status: LgndrIndirectStatus, logger: logging.Logger = None, level: int = logging.ERROR
|
||||||
|
):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def set_logger(self, logger: logging.Logger):
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def set_level(self, level: int):
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
def _log(self, level: int, msg: str):
|
||||||
|
self.status.success = True if level < self.level else False
|
||||||
|
self.status.message = msg
|
||||||
|
if self.logger:
|
||||||
|
self.logger.log(level, msg)
|
||||||
|
|
||||||
|
def debug(self, msg: str):
|
||||||
|
self._log(logging.DEBUG, msg)
|
||||||
|
|
||||||
|
def info(self, msg: str):
|
||||||
|
self._log(logging.INFO, msg)
|
||||||
|
|
||||||
|
def warning(self, msg: str):
|
||||||
|
self._log(logging.WARNING, msg)
|
||||||
|
|
||||||
|
def error(self, msg: str):
|
||||||
|
self._log(logging.ERROR, msg)
|
||||||
|
|
||||||
|
def critical(self, msg: str):
|
||||||
|
self._log(logging.CRITICAL, msg)
|
||||||
|
|
||||||
|
def fatal(self, msg: str):
|
||||||
|
self.critical(msg)
|
623
rare/lgndr/cli.py
Normal file
623
rare/lgndr/cli.py
Normal file
|
@ -0,0 +1,623 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from typing import Optional, Union, Tuple
|
||||||
|
|
||||||
|
from legendary.cli import LegendaryCLI as LegendaryCLIReal
|
||||||
|
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||||
|
from legendary.models.game import Game, InstalledGame, VerifyResult
|
||||||
|
from legendary.utils.lfs import validate_files
|
||||||
|
from legendary.utils.selective_dl import get_sdl_appname
|
||||||
|
|
||||||
|
from .api_arguments import (
|
||||||
|
LgndrInstallGameArgs,
|
||||||
|
LgndrImportGameArgs,
|
||||||
|
LgndrVerifyGameArgs,
|
||||||
|
LgndrUninstallGameArgs,
|
||||||
|
LgndrInstallGameRealArgs,
|
||||||
|
LgndrInstallGameRealRet,
|
||||||
|
)
|
||||||
|
from .api_monkeys import LgndrIndirectStatus, LgndrIndirectLogger
|
||||||
|
from .core import LegendaryCore
|
||||||
|
from .manager import DLManager
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
class LegendaryCLI(LegendaryCLIReal):
|
||||||
|
|
||||||
|
# noinspection PyMissingConstructor
|
||||||
|
def __init__(self, core: LegendaryCore):
|
||||||
|
self.core = core
|
||||||
|
self.logger = logging.getLogger('cli')
|
||||||
|
self.logging_queue = None
|
||||||
|
self.ql = self.setup_threaded_logging()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.ql.stop()
|
||||||
|
|
||||||
|
def resolve_aliases(self, name):
|
||||||
|
return super(LegendaryCLI, self)._resolve_aliases(name)
|
||||||
|
|
||||||
|
def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult]]:
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||||
|
get_boolean_choice = args.get_boolean_choice
|
||||||
|
sdl_prompt = args.sdl_prompt
|
||||||
|
|
||||||
|
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)
|
||||||
|
args.platform = igame.platform
|
||||||
|
if igame.needs_verification and not args.repair_mode:
|
||||||
|
logger.info('Game needs to be verified before updating, switching to repair mode...')
|
||||||
|
args.repair_mode = True
|
||||||
|
|
||||||
|
repair_file = None
|
||||||
|
# Rare: The 'args.no_install' flags is set externally from the InstallDialog
|
||||||
|
if args.repair_mode:
|
||||||
|
args.repair_mode = True
|
||||||
|
args.no_install = args.repair_and_update is False
|
||||||
|
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
|
||||||
|
|
||||||
|
# Rare: Rare is already logged in
|
||||||
|
|
||||||
|
if args.file_prefix or args.file_exclude_prefix:
|
||||||
|
args.no_install = True
|
||||||
|
|
||||||
|
# Rare: Rare runs updates on already installed games only
|
||||||
|
|
||||||
|
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
|
||||||
|
|
||||||
|
if not game:
|
||||||
|
logger.error(f'Could not find "{args.app_name}" in list of available games, '
|
||||||
|
f'did you type the name correctly?')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Rare: Rare checks this before calling 'install_game'
|
||||||
|
|
||||||
|
if args.platform not in game.asset_infos:
|
||||||
|
if not args.no_install:
|
||||||
|
if self.core.lgd.config.getboolean('Legendary', 'install_platform_fallback', fallback=True):
|
||||||
|
logger.warning(f'App has no asset for platform "{args.platform}", falling back to "Windows".')
|
||||||
|
args.platform = 'Windows'
|
||||||
|
else:
|
||||||
|
logger.error(f'No app asset found for platform "{args.platform}", run '
|
||||||
|
f'"legendary info --platform {args.platform}" and make '
|
||||||
|
f'sure the app is available for the specified platform.')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.warning(f'No asset found for platform "{args.platform}", '
|
||||||
|
f'trying anyway since --no-install is set.')
|
||||||
|
|
||||||
|
if game.is_dlc:
|
||||||
|
logger.info('Install candidate is DLC')
|
||||||
|
app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId']
|
||||||
|
base_game = self.core.get_game(app_name)
|
||||||
|
# check if base_game is actually installed
|
||||||
|
if not self.core.is_installed(app_name):
|
||||||
|
# download mode doesn't care about whether something's installed
|
||||||
|
if not args.no_install:
|
||||||
|
logger.fatal(f'Base game "{app_name}" is not installed!')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
base_game = None
|
||||||
|
|
||||||
|
if args.repair_mode:
|
||||||
|
if not self.core.is_installed(game.app_name):
|
||||||
|
logger.error(f'Game "{game.app_title}" ({game.app_name}) is not installed!')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.exists(repair_file):
|
||||||
|
logger.info('Game has not been verified yet.')
|
||||||
|
# Rare: we do not want to verify while preparing the download in the InstallDialog
|
||||||
|
# Rare: we handle it differently through the GameInfo tab
|
||||||
|
logger.error('Game has not been verified yet.')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info(f'Using existing repair file: {repair_file}')
|
||||||
|
|
||||||
|
# check if SDL should be disabled
|
||||||
|
sdl_enabled = not args.install_tag and not game.is_dlc
|
||||||
|
config_tags = self.core.lgd.config.get(game.app_name, 'install_tags', fallback=None)
|
||||||
|
config_disable_sdl = self.core.lgd.config.getboolean(game.app_name, 'disable_sdl', fallback=False)
|
||||||
|
# remove config flag if SDL is reset
|
||||||
|
if config_disable_sdl and args.reset_sdl and not args.disable_sdl:
|
||||||
|
self.core.lgd.config.remove_option(game.app_name, 'disable_sdl')
|
||||||
|
# if config flag is not yet set, set it and remove previous install tags
|
||||||
|
elif not config_disable_sdl and args.disable_sdl:
|
||||||
|
logger.info('Clearing install tags from config and disabling SDL for title.')
|
||||||
|
if config_tags:
|
||||||
|
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||||
|
config_tags = None
|
||||||
|
self.core.lgd.config.set(game.app_name, 'disable_sdl', True)
|
||||||
|
sdl_enabled = False
|
||||||
|
# just disable SDL, but keep config tags that have been manually specified
|
||||||
|
elif config_disable_sdl or args.disable_sdl:
|
||||||
|
sdl_enabled = False
|
||||||
|
|
||||||
|
if sdl_enabled and ((sdl_name := get_sdl_appname(game.app_name)) is not None):
|
||||||
|
if not self.core.is_installed(game.app_name) or config_tags is None or args.reset_sdl:
|
||||||
|
sdl_data = self.core.get_sdl_data(sdl_name, platform=args.platform)
|
||||||
|
if sdl_data:
|
||||||
|
if args.skip_sdl:
|
||||||
|
args.install_tag = ['']
|
||||||
|
if '__required' in sdl_data:
|
||||||
|
args.install_tag.extend(sdl_data['__required']['tags'])
|
||||||
|
else:
|
||||||
|
args.install_tag = sdl_prompt(sdl_data, game.app_title)
|
||||||
|
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag))
|
||||||
|
else:
|
||||||
|
logger.error(f'Unable to get SDL data for {sdl_name}')
|
||||||
|
else:
|
||||||
|
args.install_tag = config_tags.split(',')
|
||||||
|
elif args.install_tag and not game.is_dlc and not args.no_install:
|
||||||
|
config_tags = ','.join(args.install_tag)
|
||||||
|
logger.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}')
|
||||||
|
self.core.lgd.config.set(game.app_name, 'install_tags', config_tags)
|
||||||
|
elif not game.is_dlc:
|
||||||
|
if config_tags and args.reset_sdl:
|
||||||
|
logger.info('Clearing install tags from config.')
|
||||||
|
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||||
|
elif config_tags:
|
||||||
|
logger.info(f'Using install tags from config: {config_tags}')
|
||||||
|
args.install_tag = config_tags.split(',')
|
||||||
|
|
||||||
|
logger.info(f'Preparing download for "{game.app_title}" ({game.app_name})...')
|
||||||
|
# todo use status queue to print progress from CLI
|
||||||
|
# This has become a little ridiculous hasn't it?
|
||||||
|
dlm, analysis, igame = self.core.prepare_download(game=game, base_game=base_game, base_path=args.base_path,
|
||||||
|
force=args.force, max_shm=args.shared_memory,
|
||||||
|
max_workers=args.max_workers, game_folder=args.game_folder,
|
||||||
|
disable_patching=args.disable_patching,
|
||||||
|
override_manifest=args.override_manifest,
|
||||||
|
override_old_manifest=args.override_old_manifest,
|
||||||
|
override_base_url=args.override_base_url,
|
||||||
|
platform=args.platform,
|
||||||
|
file_prefix_filter=args.file_prefix,
|
||||||
|
file_exclude_filter=args.file_exclude_prefix,
|
||||||
|
file_install_tag=args.install_tag,
|
||||||
|
dl_optimizations=args.order_opt,
|
||||||
|
dl_timeout=args.dl_timeout,
|
||||||
|
repair=args.repair_mode,
|
||||||
|
repair_use_latest=args.repair_and_update,
|
||||||
|
disable_delta=args.disable_delta,
|
||||||
|
override_delta_manifest=args.override_delta_manifest,
|
||||||
|
preferred_cdn=args.preferred_cdn,
|
||||||
|
disable_https=args.disable_https)
|
||||||
|
|
||||||
|
# game is either up-to-date or hasn't changed, so we have nothing to do
|
||||||
|
if not analysis.dl_size:
|
||||||
|
logger.info('Download size is 0, the game is either already up to date or has not changed. Exiting...')
|
||||||
|
self.install_game_cleanup(game, igame, args.repair_mode, repair_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game,
|
||||||
|
updating=self.core.is_installed(args.app_name),
|
||||||
|
ignore_space_req=args.ignore_space)
|
||||||
|
|
||||||
|
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
|
||||||
|
|
||||||
|
# Rare: This is currently handled in DownloadThread, this is a trial
|
||||||
|
def install_game_real(self, args: LgndrInstallGameRealArgs, dlm: DLManager, game: Game, igame: InstalledGame) -> LgndrInstallGameRealRet:
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||||
|
ret = LgndrInstallGameRealRet(game.app_name)
|
||||||
|
|
||||||
|
start_t = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# set up logging stuff (should be moved somewhere else later)
|
||||||
|
dlm.logging_queue = self.logging_queue
|
||||||
|
dlm.proc_debug = args.dlm_debug
|
||||||
|
|
||||||
|
dlm.start()
|
||||||
|
while dlm.is_alive():
|
||||||
|
try:
|
||||||
|
args.ui_update(dlm.status_queue.get(timeout=1.0))
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
if args.dlm_signals.update:
|
||||||
|
try:
|
||||||
|
dlm.signals_queue.put(args.dlm_signals, block=False, timeout=1.0)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
time.sleep(dlm.update_interval / 10)
|
||||||
|
dlm.join()
|
||||||
|
except Exception as e:
|
||||||
|
end_t = time.time()
|
||||||
|
logger.info(f'Installation failed after {end_t - start_t:.02f} seconds.')
|
||||||
|
logger.warning(f'The following exception occurred while waiting for the downloader to finish: {e!r}. '
|
||||||
|
f'Try restarting the process, the resume file will be used to start where it failed. '
|
||||||
|
f'If it continues to fail please open an issue on GitHub.')
|
||||||
|
ret.ret_code = ret.ReturnCode.ERROR
|
||||||
|
ret.message = f"{e!r}"
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
end_t = time.time()
|
||||||
|
if args.dlm_signals.kill is True:
|
||||||
|
logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.")
|
||||||
|
ret.exit_code = ret.ReturnCode.STOPPED
|
||||||
|
return ret
|
||||||
|
logger.info(f"Download finished in {end_t - start_t:.02f} seconds.")
|
||||||
|
if not args.no_install:
|
||||||
|
# Allow setting savegame directory at install time so sync-saves will work immediately
|
||||||
|
if (game.supports_cloud_saves or game.supports_mac_cloud_saves) and args.save_path:
|
||||||
|
igame.save_path = args.save_path
|
||||||
|
|
||||||
|
postinstall = self.core.install_game(igame)
|
||||||
|
if postinstall:
|
||||||
|
self._handle_postinstall(postinstall, igame, yes=args.yes, choice=args.install_preqs)
|
||||||
|
|
||||||
|
dlcs = self.core.get_dlc_for_game(game.app_name)
|
||||||
|
if dlcs and not args.skip_dlcs:
|
||||||
|
for dlc in dlcs:
|
||||||
|
ret.dlcs.append(
|
||||||
|
{
|
||||||
|
"app_name": dlc.app_name,
|
||||||
|
"app_title": dlc.app_title,
|
||||||
|
"app_version": dlc.app_version(args.platform)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rare: We do not install DLCs automatically, we offer to do so through our downloads tab
|
||||||
|
|
||||||
|
if (game.supports_cloud_saves or game.supports_mac_cloud_saves) and not game.is_dlc:
|
||||||
|
# todo option to automatically download saves after the installation
|
||||||
|
# args does not have the required attributes for sync_saves in here,
|
||||||
|
# not sure how to solve that elegantly.
|
||||||
|
logger.info(f'This game supports cloud saves, syncing is handled by the "sync-saves" command. '
|
||||||
|
f'To download saves for this game run "legendary sync-saves {args.app_name}"')
|
||||||
|
ret.sync_saves = True
|
||||||
|
|
||||||
|
# show tip again after installation finishes so users hopefully actually see it
|
||||||
|
if tip_url := self.core.get_game_tip(igame.app_name):
|
||||||
|
ret.tip_url = tip_url
|
||||||
|
|
||||||
|
self.install_game_cleanup(game, igame, args.repair_mode, args.repair_file)
|
||||||
|
|
||||||
|
logger.info(f'Finished installation process in {end_t - start_t:.02f} seconds.')
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bool = False, repair_file: str = '') -> None:
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||||
|
|
||||||
|
old_igame = self.core.get_installed_game(game.app_name)
|
||||||
|
if old_igame and repair_mode and os.path.exists(repair_file):
|
||||||
|
if old_igame.needs_verification:
|
||||||
|
old_igame.needs_verification = False
|
||||||
|
self.core.install_game(old_igame)
|
||||||
|
|
||||||
|
logger.debug('Removing repair file.')
|
||||||
|
os.remove(repair_file)
|
||||||
|
|
||||||
|
# check if install tags have changed, if they did; try deleting files that are no longer required.
|
||||||
|
if old_igame and old_igame.install_tags != igame.install_tags:
|
||||||
|
old_igame.install_tags = igame.install_tags
|
||||||
|
logger.info('Deleting now untagged files.')
|
||||||
|
self.core.uninstall_tag(old_igame)
|
||||||
|
self.core.install_game(old_igame)
|
||||||
|
|
||||||
|
def _handle_postinstall(self, postinstall, igame, yes=False, choice=True):
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||||
|
# noinspection PyShadowingBuiltins
|
||||||
|
def print(x): self.logger.info(x) if x else None
|
||||||
|
# noinspection PyShadowingBuiltins
|
||||||
|
def input(x): return 'y' if choice else 'i'
|
||||||
|
|
||||||
|
print('\nThis game lists the following prequisites to be installed:')
|
||||||
|
print(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
|
||||||
|
print('')
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
if yes:
|
||||||
|
c = 'n' # we don't want to launch anything, just silent install.
|
||||||
|
else:
|
||||||
|
choice = input('Do you wish to install the prerequisites? ([y]es, [n]o, [i]gnore): ')
|
||||||
|
c = choice.lower()[0]
|
||||||
|
print('')
|
||||||
|
|
||||||
|
if c == 'i': # just set it to installed
|
||||||
|
logger.info('Marking prerequisites as installed...')
|
||||||
|
self.core.prereq_installed(igame.app_name)
|
||||||
|
elif c == 'y': # set to installed and launch installation
|
||||||
|
logger.info('Launching prerequisite executable..')
|
||||||
|
self.core.prereq_installed(igame.app_name)
|
||||||
|
req_path, req_exec = os.path.split(postinstall['path'])
|
||||||
|
work_dir = os.path.join(igame.install_path, req_path)
|
||||||
|
fullpath = os.path.join(work_dir, req_exec)
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen([fullpath, postinstall['args']], cwd=work_dir, shell=True)
|
||||||
|
p.wait()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to run prereq executable with: {e!r}')
|
||||||
|
else:
|
||||||
|
logger.info('Automatic installation not available on Linux.')
|
||||||
|
|
||||||
|
def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||||
|
get_boolean_choice = args.get_boolean_choice
|
||||||
|
|
||||||
|
args.app_name = self._resolve_aliases(args.app_name)
|
||||||
|
igame = self.core.get_installed_game(args.app_name)
|
||||||
|
if not igame:
|
||||||
|
logger.error(f'Game {args.app_name} not installed, cannot uninstall!')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not args.yes:
|
||||||
|
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not igame.is_dlc:
|
||||||
|
# Remove DLC first so directory is empty when game uninstall runs
|
||||||
|
dlcs = self.core.get_dlc_for_game(igame.app_name)
|
||||||
|
for dlc in dlcs:
|
||||||
|
if (idlc := self.core.get_installed_game(dlc.app_name)) is not None:
|
||||||
|
logger.info(f'Uninstalling DLC "{dlc.app_name}"...')
|
||||||
|
self.core.uninstall_game(idlc, delete_files=not args.keep_files)
|
||||||
|
|
||||||
|
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
|
||||||
|
self.core.uninstall_game(igame, delete_files=not args.keep_files,
|
||||||
|
delete_root_directory=not igame.is_dlc)
|
||||||
|
logger.info('Game has been uninstalled.')
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
|
||||||
|
return
|
||||||
|
|
||||||
|
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False) -> Optional[Tuple[int, int]]:
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||||
|
|
||||||
|
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')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f'Loading installed manifest for "{args.app_name}"')
|
||||||
|
igame = self.core.get_installed_game(args.app_name)
|
||||||
|
if not os.path.exists(igame.install_path):
|
||||||
|
logger.error(f'Install path "{igame.install_path}" does not exist, make sure all necessary mounts '
|
||||||
|
f'are available. If you previously deleted the game folder without uninstalling, run '
|
||||||
|
f'"legendary uninstall -y {igame.app_name}" and reinstall from scratch.')
|
||||||
|
return
|
||||||
|
|
||||||
|
manifest_data, _ = self.core.get_installed_manifest(args.app_name)
|
||||||
|
if manifest_data is None:
|
||||||
|
if repair_mode:
|
||||||
|
if not repair_online:
|
||||||
|
logger.critical('No manifest could be loaded, the manifest file may be missing!')
|
||||||
|
raise ValueError('Local manifest is missing')
|
||||||
|
|
||||||
|
logger.warning('No manifest could be loaded, the file may be missing. Downloading the latest manifest.')
|
||||||
|
game = self.core.get_game(args.app_name, platform=igame.platform)
|
||||||
|
manifest_data, _ = self.core.get_cdn_manifest(game, igame.platform)
|
||||||
|
# Rare: Save the manifest if we downloaded it because it was missing
|
||||||
|
self.core.lgd.save_manifest(game.app_name, manifest_data,
|
||||||
|
version=self.core.load_manifest(manifest_data).meta.build_version,
|
||||||
|
platform=igame.platform)
|
||||||
|
else:
|
||||||
|
logger.critical(f'Manifest appears to be missing! To repair, run "legendary repair '
|
||||||
|
f'{args.app_name} --repair-and-update", this will however redownload all files '
|
||||||
|
f'that do not match the latest manifest in their entirety.')
|
||||||
|
return
|
||||||
|
|
||||||
|
manifest = self.core.load_manifest(manifest_data)
|
||||||
|
|
||||||
|
files = sorted(manifest.file_manifest_list.elements,
|
||||||
|
key=lambda a: a.filename.lower())
|
||||||
|
|
||||||
|
# build list of hashes
|
||||||
|
if config_tags := self.core.lgd.config.get(args.app_name, 'install_tags', fallback=None):
|
||||||
|
install_tags = set(i.strip() for i in config_tags.split(','))
|
||||||
|
file_list = [
|
||||||
|
(f.filename, f.sha_hash.hex())
|
||||||
|
for f in files
|
||||||
|
if any(it in install_tags for it in f.install_tags) or not f.install_tags
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
|
||||||
|
|
||||||
|
total = len(file_list)
|
||||||
|
total_size = sum(manifest.file_manifest_list.get_file_by_path(fm[0]).file_size
|
||||||
|
for fm in file_list)
|
||||||
|
num = processed = last_processed = 0
|
||||||
|
speed = 0.0
|
||||||
|
percentage = 0.0
|
||||||
|
failed = []
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
last_update = time.time()
|
||||||
|
|
||||||
|
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
|
||||||
|
repair_file = []
|
||||||
|
for result, path, result_hash, bytes_read in validate_files(igame.install_path, file_list):
|
||||||
|
processed += bytes_read
|
||||||
|
percentage = (processed / total_size) * 100.0
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
if (delta := ((current_time := time.time()) - last_update)) > 1 or (not last_processed and delta > 1):
|
||||||
|
last_update = current_time
|
||||||
|
speed = (processed - last_processed) / 1024 / 1024 / delta
|
||||||
|
last_processed = processed
|
||||||
|
|
||||||
|
if args.verify_stdout:
|
||||||
|
args.verify_stdout(num, total, percentage, speed)
|
||||||
|
|
||||||
|
if result == VerifyResult.HASH_MATCH:
|
||||||
|
repair_file.append(f'{result_hash}:{path}')
|
||||||
|
continue
|
||||||
|
elif result == VerifyResult.HASH_MISMATCH:
|
||||||
|
logger.error(f'File does not match hash: "{path}"')
|
||||||
|
repair_file.append(f'{result_hash}:{path}')
|
||||||
|
failed.append(path)
|
||||||
|
elif result == VerifyResult.FILE_MISSING:
|
||||||
|
logger.error(f'File is missing: "{path}"')
|
||||||
|
missing.append(path)
|
||||||
|
else:
|
||||||
|
logger.error(f'Other failure (see log), treating file as missing: "{path}"')
|
||||||
|
missing.append(path)
|
||||||
|
|
||||||
|
if args.verify_stdout:
|
||||||
|
args.verify_stdout(num, total, percentage, speed)
|
||||||
|
|
||||||
|
# always write repair file, even if all match
|
||||||
|
if repair_file:
|
||||||
|
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
|
||||||
|
with open(repair_filename, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('\n'.join(repair_file))
|
||||||
|
logger.debug(f'Written repair file to "{repair_filename}"')
|
||||||
|
|
||||||
|
if not missing and not failed:
|
||||||
|
logger.info('Verification finished successfully.')
|
||||||
|
return 0, 0
|
||||||
|
else:
|
||||||
|
logger.error(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 len(failed), len(missing)
|
||||||
|
|
||||||
|
def import_game(self, args: LgndrImportGameArgs) -> None:
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||||
|
get_boolean_choice = args.get_boolean_choice
|
||||||
|
|
||||||
|
# make sure path is absolute
|
||||||
|
args.app_path = os.path.abspath(args.app_path)
|
||||||
|
args.app_name = self._resolve_aliases(args.app_name)
|
||||||
|
|
||||||
|
if not os.path.exists(args.app_path):
|
||||||
|
logger.error(f'Specified path "{args.app_path}" does not exist!')
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.core.is_installed(args.app_name):
|
||||||
|
logger.error('Game is already installed!')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.core.login():
|
||||||
|
logger.error('Log in failed!')
|
||||||
|
return
|
||||||
|
|
||||||
|
# do some basic checks
|
||||||
|
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
|
||||||
|
if not game:
|
||||||
|
logger.fatal(f'Did not find game "{args.app_name}" on account.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if game.is_dlc:
|
||||||
|
release_info = game.metadata.get('mainGameItem', {}).get('releaseInfo')
|
||||||
|
if release_info:
|
||||||
|
main_game_appname = release_info[0]['appId']
|
||||||
|
main_game_title = game.metadata['mainGameItem']['title']
|
||||||
|
if not self.core.is_installed(main_game_appname):
|
||||||
|
logger.error(f'Import candidate is DLC but base game "{main_game_title}" '
|
||||||
|
f'(App name: "{main_game_appname}") is not installed!')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.fatal(f'Unable to get base game information for DLC, cannot continue.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# get everything needed for import from core, then run additional checks.
|
||||||
|
manifest, igame = self.core.import_game(game, args.app_path, platform=args.platform)
|
||||||
|
exe_path = os.path.join(args.app_path, manifest.meta.launch_exe.lstrip('/'))
|
||||||
|
# check if most files at least exist or if user might have specified the wrong directory
|
||||||
|
total = len(manifest.file_manifest_list.elements)
|
||||||
|
found = sum(os.path.exists(os.path.join(args.app_path, f.filename))
|
||||||
|
for f in manifest.file_manifest_list.elements)
|
||||||
|
ratio = found / total
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
logger.error(f'No files belonging to {"DLC" if game.is_dlc else "Game"} "{game.app_title}" '
|
||||||
|
f'({game.app_name}) found in the specified location, please verify that the path is correct.')
|
||||||
|
if not game.is_dlc:
|
||||||
|
# check if game folder is in path, suggest alternative
|
||||||
|
folder = game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name)
|
||||||
|
if folder and folder not in args.app_path:
|
||||||
|
new_path = os.path.join(args.app_path, folder)
|
||||||
|
logger.info(f'Did you mean "{new_path}"?')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not game.is_dlc and not os.path.exists(exe_path) and not args.disable_check:
|
||||||
|
logger.error(f'Game executable could not be found at "{exe_path}", '
|
||||||
|
f'please verify that the specified path is correct.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if ratio < 0.95:
|
||||||
|
logger.warning('Some files are missing from the game installation, install may not '
|
||||||
|
'match latest Epic Games Store version or might be corrupted.')
|
||||||
|
else:
|
||||||
|
logger.info(f'{"DLC" if game.is_dlc else "Game"} install appears to be complete.')
|
||||||
|
|
||||||
|
self.core.install_game(igame)
|
||||||
|
if igame.needs_verification:
|
||||||
|
logger.info(f'NOTE: The {"DLC" if game.is_dlc else "Game"} installation will have to be '
|
||||||
|
f'verified before it can be updated with legendary.')
|
||||||
|
logger.info(f'Run "legendary repair {args.app_name}" to do so.')
|
||||||
|
else:
|
||||||
|
logger.info(f'Installation had Epic Games Launcher metadata for version "{igame.version}", '
|
||||||
|
f'verification will not be required.')
|
||||||
|
|
||||||
|
# check for importable DLC
|
||||||
|
if not args.skip_dlcs:
|
||||||
|
dlcs = self.core.get_dlc_for_game(game.app_name)
|
||||||
|
if dlcs:
|
||||||
|
logger.info(f'Found {len(dlcs)} items of DLC that could be imported.')
|
||||||
|
import_dlc = True
|
||||||
|
if not args.yes and not args.with_dlcs:
|
||||||
|
if not get_boolean_choice(f'Do you wish to automatically attempt to import all DLCs?'):
|
||||||
|
import_dlc = False
|
||||||
|
|
||||||
|
if import_dlc:
|
||||||
|
for dlc in dlcs:
|
||||||
|
args.app_name = dlc.app_name
|
||||||
|
self.import_game(args)
|
||||||
|
|
||||||
|
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
|
||||||
|
return
|
||||||
|
|
||||||
|
def move(self, args):
|
||||||
|
# Override logger for the local context to use message as part of the indirect return value
|
||||||
|
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||||
|
|
||||||
|
app_name = self._resolve_aliases(args.app_name)
|
||||||
|
igame = self.core.get_installed_game(app_name, skip_sync=True)
|
||||||
|
if not igame:
|
||||||
|
logger.error(f'No installed game found for "{app_name}"')
|
||||||
|
return
|
||||||
|
|
||||||
|
old_base, game_folder = os.path.split(igame.install_path.replace('\\', '/'))
|
||||||
|
new_path = os.path.join(args.new_path, game_folder)
|
||||||
|
logger.info(f'Moving "{game_folder}" from "{old_base}" to "{args.new_path}"')
|
||||||
|
|
||||||
|
if not args.skip_move:
|
||||||
|
try:
|
||||||
|
if not os.path.exists(args.new_path):
|
||||||
|
os.makedirs(args.new_path)
|
||||||
|
|
||||||
|
os.rename(igame.install_path, new_path)
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, OSError) and e.errno == 18:
|
||||||
|
logger.error(f'Moving to a different drive is not supported. Move the folder manually to '
|
||||||
|
f'"{new_path}" and run "legendary move {app_name} "{args.new_path}" --skip-move"')
|
||||||
|
elif isinstance(e, FileExistsError):
|
||||||
|
logger.error(f'The target path already contains a folder called "{game_folder}", '
|
||||||
|
f'please remove or rename it first.')
|
||||||
|
else:
|
||||||
|
logger.error(f'Moving failed with unknown error {e!r}.')
|
||||||
|
logger.info(f'Try moving the folder manually to "{new_path}" and running '
|
||||||
|
f'"legendary move {app_name} "{args.new_path}" --skip-move"')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info(f'Not moving, just rewriting legendary metadata...')
|
||||||
|
|
||||||
|
igame.install_path = new_path
|
||||||
|
self.core.install_game(igame)
|
||||||
|
logger.info('Finished.')
|
||||||
|
|
||||||
|
# fmt: on
|
97
rare/lgndr/core.py
Normal file
97
rare/lgndr/core.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from multiprocessing import Queue
|
||||||
|
|
||||||
|
from legendary.core import LegendaryCore as LegendaryCoreReal
|
||||||
|
from legendary.models.downloading import AnalysisResult
|
||||||
|
from legendary.models.game import Game, InstalledGame
|
||||||
|
from legendary.models.manifest import ManifestMeta
|
||||||
|
|
||||||
|
from .api_exception import LgndrException, LgndrCoreLogHandler
|
||||||
|
from .manager import DLManager
|
||||||
|
|
||||||
|
# import legendary.core
|
||||||
|
# legendary.core.DLManager = DLManager
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
class LegendaryCore(LegendaryCoreReal):
|
||||||
|
|
||||||
|
def __init__(self, override_config=None, timeout=10.0):
|
||||||
|
super(LegendaryCore, self).__init__(override_config=override_config, timeout=timeout)
|
||||||
|
self.handler = LgndrCoreLogHandler()
|
||||||
|
self.log.addHandler(self.handler)
|
||||||
|
|
||||||
|
# skip_sync defaults to false but since Rare is persistent, skip by default
|
||||||
|
# def get_installed_game(self, app_name, skip_sync=True) -> InstalledGame:
|
||||||
|
# return super(LegendaryCore, self).get_installed_game(app_name, skip_sync)
|
||||||
|
|
||||||
|
def prepare_download(self, game: Game, base_game: Game = None, base_path: str = '',
|
||||||
|
status_q: Queue = None, max_shm: 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_filter: list = None,
|
||||||
|
file_exclude_filter: list = None, file_install_tag: list = None,
|
||||||
|
dl_optimizations: bool = False, dl_timeout: int = 10,
|
||||||
|
repair: bool = False, repair_use_latest: bool = False,
|
||||||
|
disable_delta: bool = False, override_delta_manifest: str = '',
|
||||||
|
egl_guid: str = '', preferred_cdn: str = None,
|
||||||
|
disable_https: bool = False) -> (DLManager, AnalysisResult, ManifestMeta):
|
||||||
|
dlm, analysis, igame = super(LegendaryCore, self).prepare_download(
|
||||||
|
game=game, base_game=base_game, base_path=base_path,
|
||||||
|
status_q=status_q, max_shm=max_shm, 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_filter=file_prefix_filter,
|
||||||
|
file_exclude_filter=file_exclude_filter, file_install_tag=file_install_tag,
|
||||||
|
dl_optimizations=dl_optimizations, dl_timeout=dl_timeout,
|
||||||
|
repair=repair, repair_use_latest=repair_use_latest,
|
||||||
|
disable_delta=disable_delta, override_delta_manifest=override_delta_manifest,
|
||||||
|
egl_guid=egl_guid, preferred_cdn=preferred_cdn,
|
||||||
|
disable_https=disable_https
|
||||||
|
)
|
||||||
|
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
|
||||||
|
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
|
||||||
|
# lk: set the queue for reporting statistics back the UI
|
||||||
|
dlm.status_queue = Queue()
|
||||||
|
# lk: set the queue to send control signals to the DLManager
|
||||||
|
# lk: this doesn't exist in the original class, but it is monkeypatched in
|
||||||
|
dlm.signals_queue = Queue()
|
||||||
|
return dlm, analysis, igame
|
||||||
|
|
||||||
|
def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delete_root_directory=False):
|
||||||
|
try:
|
||||||
|
super(LegendaryCore, self).uninstall_game(installed_game, delete_files, delete_root_directory)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def egl_import(self, app_name):
|
||||||
|
try:
|
||||||
|
super(LegendaryCore, self).egl_import(app_name)
|
||||||
|
except LgndrException as ret:
|
||||||
|
raise ret
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def egl_export(self, app_name):
|
||||||
|
try:
|
||||||
|
super(LegendaryCore, self).egl_export(app_name)
|
||||||
|
except LgndrException as ret:
|
||||||
|
raise ret
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def prepare_overlay_install(self, path=None):
|
||||||
|
dlm, analysis_result, igame = super(LegendaryCore, self).prepare_overlay_install(path)
|
||||||
|
# lk: monkeypatch status_q (the queue for download stats)
|
||||||
|
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
|
||||||
|
# lk: set the queue for reporting statistics back the UI
|
||||||
|
dlm.status_queue = Queue()
|
||||||
|
# lk: set the queue to send control signals to the DLManager
|
||||||
|
# lk: this doesn't exist in the original class, but it is monkeypatched in
|
||||||
|
dlm.signals_queue = Queue()
|
||||||
|
return dlm, analysis_result, igame
|
||||||
|
|
||||||
|
# fmt: on
|
25
rare/lgndr/downloading.py
Normal file
25
rare/lgndr/downloading.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UIUpdate:
|
||||||
|
"""
|
||||||
|
Status update object sent from the manager to the CLI/GUI to update status indicators
|
||||||
|
Inheritance doesn't work due to optional arguments in UIUpdate proper
|
||||||
|
"""
|
||||||
|
progress: float
|
||||||
|
download_speed: float
|
||||||
|
write_speed: float
|
||||||
|
read_speed: float
|
||||||
|
memory_usage: float
|
||||||
|
runtime: float
|
||||||
|
estimated_time_left: float
|
||||||
|
processed_chunks: int
|
||||||
|
chunk_tasks: int
|
||||||
|
total_downloaded: float
|
||||||
|
total_written: float
|
||||||
|
cache_usage: float
|
||||||
|
active_tasks: int
|
||||||
|
download_compressed_speed: float
|
||||||
|
current_filename: Optional[str] = None
|
229
rare/lgndr/manager.py
Normal file
229
rare/lgndr/manager.py
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
from multiprocessing import Queue as MPQueue
|
||||||
|
from multiprocessing.shared_memory import SharedMemory
|
||||||
|
from sys import exit
|
||||||
|
from threading import Condition, Thread
|
||||||
|
|
||||||
|
from legendary.downloader.mp.manager import DLManager as DLManagerReal
|
||||||
|
from legendary.downloader.mp.workers import DLWorker, FileWorker
|
||||||
|
from legendary.models.downloading import ChunkTask, SharedMemorySegment, TerminateWorkerTask
|
||||||
|
|
||||||
|
from .downloading import UIUpdate
|
||||||
|
from .api_monkeys import DLManagerSignals
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
class DLManager(DLManagerReal):
|
||||||
|
# Rare: prototype to avoid undefined variable in type checkers
|
||||||
|
signals_queue: MPQueue
|
||||||
|
|
||||||
|
# @staticmethod
|
||||||
|
def run_real(self):
|
||||||
|
self.shared_memory = SharedMemory(create=True, size=self.max_shared_memory)
|
||||||
|
self.log.debug(f'Created shared memory of size: {self.shared_memory.size / 1024 / 1024:.02f} MiB')
|
||||||
|
|
||||||
|
# create the shared memory segments and add them to their respective pools
|
||||||
|
for i in range(int(self.shared_memory.size / self.analysis.biggest_chunk)):
|
||||||
|
_sms = SharedMemorySegment(offset=i * self.analysis.biggest_chunk,
|
||||||
|
end=i * self.analysis.biggest_chunk + self.analysis.biggest_chunk)
|
||||||
|
self.sms.append(_sms)
|
||||||
|
|
||||||
|
self.log.debug(f'Created {len(self.sms)} shared memory segments.')
|
||||||
|
|
||||||
|
# Create queues
|
||||||
|
self.dl_worker_queue = MPQueue(-1)
|
||||||
|
self.writer_queue = MPQueue(-1)
|
||||||
|
self.dl_result_q = MPQueue(-1)
|
||||||
|
self.writer_result_q = MPQueue(-1)
|
||||||
|
|
||||||
|
self.log.info(f'Starting download workers...')
|
||||||
|
for i in range(self.max_workers):
|
||||||
|
w = DLWorker(f'DLWorker {i + 1}', self.dl_worker_queue, self.dl_result_q,
|
||||||
|
self.shared_memory.name, logging_queue=self.logging_queue,
|
||||||
|
dl_timeout=self.dl_timeout)
|
||||||
|
self.children.append(w)
|
||||||
|
w.start()
|
||||||
|
|
||||||
|
self.log.info('Starting file writing worker...')
|
||||||
|
writer_p = FileWorker(self.writer_queue, self.writer_result_q, self.dl_dir,
|
||||||
|
self.shared_memory.name, self.cache_dir, self.logging_queue)
|
||||||
|
self.children.append(writer_p)
|
||||||
|
writer_p.start()
|
||||||
|
|
||||||
|
num_chunk_tasks = sum(isinstance(t, ChunkTask) for t in self.tasks)
|
||||||
|
num_dl_tasks = len(self.chunks_to_dl)
|
||||||
|
num_tasks = len(self.tasks)
|
||||||
|
num_shared_memory_segments = len(self.sms)
|
||||||
|
self.log.debug(f'Chunks to download: {num_dl_tasks}, File tasks: {num_tasks}, Chunk tasks: {num_chunk_tasks}')
|
||||||
|
|
||||||
|
# active downloader tasks
|
||||||
|
self.active_tasks = 0
|
||||||
|
processed_chunks = 0
|
||||||
|
processed_tasks = 0
|
||||||
|
total_dl = 0
|
||||||
|
total_write = 0
|
||||||
|
|
||||||
|
# synchronization conditions
|
||||||
|
shm_cond = Condition()
|
||||||
|
task_cond = Condition()
|
||||||
|
self.conditions = [shm_cond, task_cond]
|
||||||
|
|
||||||
|
# start threads
|
||||||
|
s_time = time.time()
|
||||||
|
self.threads.append(Thread(target=self.download_job_manager, args=(task_cond, shm_cond)))
|
||||||
|
self.threads.append(Thread(target=self.dl_results_handler, args=(task_cond,)))
|
||||||
|
self.threads.append(Thread(target=self.fw_results_handler, args=(shm_cond,)))
|
||||||
|
|
||||||
|
for t in self.threads:
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
last_update = time.time()
|
||||||
|
|
||||||
|
# Rare: kill requested
|
||||||
|
kill_request = False
|
||||||
|
|
||||||
|
while processed_tasks < num_tasks:
|
||||||
|
delta = time.time() - last_update
|
||||||
|
if not delta:
|
||||||
|
time.sleep(self.update_interval)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# update all the things
|
||||||
|
processed_chunks += self.num_processed_since_last
|
||||||
|
processed_tasks += self.num_tasks_processed_since_last
|
||||||
|
|
||||||
|
total_dl += self.bytes_downloaded_since_last
|
||||||
|
total_write += self.bytes_written_since_last
|
||||||
|
|
||||||
|
dl_speed = self.bytes_downloaded_since_last / delta
|
||||||
|
dl_unc_speed = self.bytes_decompressed_since_last / delta
|
||||||
|
w_speed = self.bytes_written_since_last / delta
|
||||||
|
r_speed = self.bytes_read_since_last / delta
|
||||||
|
# c_speed = self.num_processed_since_last / delta
|
||||||
|
|
||||||
|
# set temporary counters to 0
|
||||||
|
self.bytes_read_since_last = self.bytes_written_since_last = 0
|
||||||
|
self.bytes_downloaded_since_last = self.num_processed_since_last = 0
|
||||||
|
self.bytes_decompressed_since_last = self.num_tasks_processed_since_last = 0
|
||||||
|
last_update = time.time()
|
||||||
|
|
||||||
|
perc = (processed_chunks / num_chunk_tasks) * 100
|
||||||
|
runtime = time.time() - s_time
|
||||||
|
total_avail = len(self.sms)
|
||||||
|
total_used = (num_shared_memory_segments - total_avail) * (self.analysis.biggest_chunk / 1024 / 1024)
|
||||||
|
|
||||||
|
if runtime and processed_chunks:
|
||||||
|
average_speed = processed_chunks / runtime
|
||||||
|
estimate = (num_chunk_tasks - processed_chunks) / average_speed
|
||||||
|
hours, estimate = int(estimate // 3600), estimate % 3600
|
||||||
|
minutes, seconds = int(estimate // 60), int(estimate % 60)
|
||||||
|
|
||||||
|
rt_hours, runtime = int(runtime // 3600), runtime % 3600
|
||||||
|
rt_minutes, rt_seconds = int(runtime // 60), int(runtime % 60)
|
||||||
|
else:
|
||||||
|
estimate = 0
|
||||||
|
hours = minutes = seconds = 0
|
||||||
|
rt_hours = rt_minutes = rt_seconds = 0
|
||||||
|
|
||||||
|
# Rare: Disable up to INFO logging level for the segment below
|
||||||
|
log_level = self.log.level
|
||||||
|
self.log.setLevel(logging.ERROR)
|
||||||
|
self.log.info(f'= Progress: {perc:.02f}% ({processed_chunks}/{num_chunk_tasks}), '
|
||||||
|
f'Running for {rt_hours:02d}:{rt_minutes:02d}:{rt_seconds:02d}, '
|
||||||
|
f'ETA: {hours:02d}:{minutes:02d}:{seconds:02d}')
|
||||||
|
self.log.info(f' - Downloaded: {total_dl / 1024 / 1024:.02f} MiB, '
|
||||||
|
f'Written: {total_write / 1024 / 1024:.02f} MiB')
|
||||||
|
self.log.info(f' - Cache usage: {total_used:.02f} MiB, active tasks: {self.active_tasks}')
|
||||||
|
self.log.info(f' + Download\t- {dl_speed / 1024 / 1024:.02f} MiB/s (raw) '
|
||||||
|
f'/ {dl_unc_speed / 1024 / 1024:.02f} MiB/s (decompressed)')
|
||||||
|
self.log.info(f' + Disk\t- {w_speed / 1024 / 1024:.02f} MiB/s (write) / '
|
||||||
|
f'{r_speed / 1024 / 1024:.02f} MiB/s (read)')
|
||||||
|
# Rare: Restore previous logging level
|
||||||
|
self.log.setLevel(log_level)
|
||||||
|
|
||||||
|
# send status update to back to instantiator (if queue exists)
|
||||||
|
if self.status_queue:
|
||||||
|
try:
|
||||||
|
self.status_queue.put(UIUpdate(
|
||||||
|
progress=perc, download_speed=dl_unc_speed, write_speed=w_speed, read_speed=r_speed,
|
||||||
|
runtime=round(runtime),
|
||||||
|
estimated_time_left=round(estimate),
|
||||||
|
processed_chunks=processed_chunks,
|
||||||
|
chunk_tasks=num_chunk_tasks,
|
||||||
|
total_downloaded=total_dl,
|
||||||
|
total_written=total_write,
|
||||||
|
cache_usage=total_used,
|
||||||
|
active_tasks=self.active_tasks,
|
||||||
|
download_compressed_speed=dl_speed,
|
||||||
|
memory_usage=total_used * 1024 * 1024
|
||||||
|
), timeout=1.0)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning(f'Failed to send status update to queue: {e!r}')
|
||||||
|
|
||||||
|
# Rare: queue of control signals
|
||||||
|
try:
|
||||||
|
signals: DLManagerSignals = self.signals_queue.get(timeout=0.5)
|
||||||
|
self.log.warning('Immediate stop requested!')
|
||||||
|
if signals.kill is True:
|
||||||
|
# lk: graceful but not what legendary does
|
||||||
|
self.running = False
|
||||||
|
# send conditions to unlock threads if they aren't already
|
||||||
|
for cond in self.conditions:
|
||||||
|
with cond:
|
||||||
|
cond.notify()
|
||||||
|
kill_request = True
|
||||||
|
break
|
||||||
|
# # lk: alternative way, but doesn't clean shm
|
||||||
|
# for i in range(self.max_workers):
|
||||||
|
# self.dl_worker_queue.put_nowait(TerminateWorkerTask())
|
||||||
|
#
|
||||||
|
# self.log.info('Waiting for installation to finish...')
|
||||||
|
# self.writer_queue.put_nowait(TerminateWorkerTask())
|
||||||
|
# raise KeyboardInterrupt
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(self.update_interval)
|
||||||
|
|
||||||
|
for i in range(self.max_workers):
|
||||||
|
self.dl_worker_queue.put_nowait(TerminateWorkerTask())
|
||||||
|
|
||||||
|
self.log.info('Waiting for installation to finish...')
|
||||||
|
self.writer_queue.put_nowait(TerminateWorkerTask())
|
||||||
|
|
||||||
|
writer_p.join(timeout=10.0)
|
||||||
|
if writer_p.exitcode is None:
|
||||||
|
self.log.warning(f'Terminating writer process, no exit code!')
|
||||||
|
writer_p.terminate()
|
||||||
|
|
||||||
|
# forcibly kill DL workers that are not actually dead yet
|
||||||
|
for child in self.children:
|
||||||
|
if child.exitcode is None:
|
||||||
|
child.terminate()
|
||||||
|
|
||||||
|
# make sure all the threads are dead.
|
||||||
|
for t in self.threads:
|
||||||
|
t.join(timeout=5.0)
|
||||||
|
if t.is_alive():
|
||||||
|
self.log.warning(f'Thread did not terminate! {repr(t)}')
|
||||||
|
|
||||||
|
# clean up resume file
|
||||||
|
if self.resume_file and not kill_request:
|
||||||
|
try:
|
||||||
|
os.remove(self.resume_file)
|
||||||
|
except OSError as e:
|
||||||
|
self.log.warning(f'Failed to remove resume file: {e!r}')
|
||||||
|
|
||||||
|
# close up shared memory
|
||||||
|
self.shared_memory.close()
|
||||||
|
self.shared_memory.unlink()
|
||||||
|
self.shared_memory = None
|
||||||
|
|
||||||
|
self.log.info('All done! Download manager quitting...')
|
||||||
|
# finally, exit the process.
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
# fmt: on
|
65
rare/models/install.py
Normal file
65
rare/models/install.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import os
|
||||||
|
import platform as pf
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional, Callable, Dict
|
||||||
|
|
||||||
|
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||||
|
from legendary.models.game import Game, InstalledGame
|
||||||
|
|
||||||
|
from rare.lgndr.manager import DLManager
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InstallOptionsModel:
|
||||||
|
app_name: str
|
||||||
|
base_path: str = ""
|
||||||
|
shared_memory: int = 1024
|
||||||
|
max_workers: int = os.cpu_count() * 2
|
||||||
|
force: bool = False
|
||||||
|
platform: str = "Windows"
|
||||||
|
install_tag: Optional[List[str]] = None
|
||||||
|
order_opt: bool = False
|
||||||
|
repair_mode: bool = False
|
||||||
|
repair_and_update: bool = False
|
||||||
|
no_install: bool = False
|
||||||
|
ignore_space: bool = False
|
||||||
|
skip_dlcs: bool = False
|
||||||
|
with_dlcs: bool = False
|
||||||
|
# Rare's internal arguments
|
||||||
|
# FIXME: Do we really need all of these?
|
||||||
|
create_shortcut: bool = True
|
||||||
|
overlay: bool = False
|
||||||
|
update: bool = False
|
||||||
|
silent: bool = False
|
||||||
|
install_preqs: bool = pf.system() == "Windows"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.sdl_prompt: Callable[[str, str], list] = \
|
||||||
|
lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
|
||||||
|
|
||||||
|
def as_install_kwargs(self) -> Dict:
|
||||||
|
return {
|
||||||
|
k: getattr(self, k)
|
||||||
|
for k in self.__dict__
|
||||||
|
if k not in ["update", "silent", "create_shortcut", "overlay", "install_preqs"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InstallDownloadModel:
|
||||||
|
dlm: DLManager
|
||||||
|
analysis: AnalysisResult
|
||||||
|
igame: InstalledGame
|
||||||
|
game: Game
|
||||||
|
repair: bool
|
||||||
|
repair_file: str
|
||||||
|
res: ConditionCheckResult
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InstallQueueItemModel:
|
||||||
|
download: Optional[InstallDownloadModel] = None
|
||||||
|
options: Optional[InstallOptionsModel] = None
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return (self.download is not None) and (self.options is not None)
|
|
@ -1,6 +1,6 @@
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
from rare.utils.models import InstallOptionsModel
|
from .install import InstallOptionsModel
|
||||||
|
|
||||||
|
|
||||||
class GlobalSignals(QObject):
|
class GlobalSignals(QObject):
|
||||||
|
|
|
@ -8,7 +8,7 @@ and only ONCE!
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from legendary.core import LegendaryCore
|
from rare.lgndr.core import LegendaryCore
|
||||||
|
|
||||||
from rare.models.apiresults import ApiResults
|
from rare.models.apiresults import ApiResults
|
||||||
from rare.models.signals import GlobalSignals
|
from rare.models.signals import GlobalSignals
|
||||||
|
|
|
@ -14,10 +14,11 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
class Ui_ImportGroup(object):
|
class Ui_ImportGroup(object):
|
||||||
def setupUi(self, ImportGroup):
|
def setupUi(self, ImportGroup):
|
||||||
ImportGroup.setObjectName("ImportGroup")
|
ImportGroup.setObjectName("ImportGroup")
|
||||||
ImportGroup.resize(501, 136)
|
ImportGroup.resize(501, 162)
|
||||||
ImportGroup.setWindowTitle("ImportGroup")
|
ImportGroup.setWindowTitle("ImportGroup")
|
||||||
ImportGroup.setWindowFilePath("")
|
ImportGroup.setWindowFilePath("")
|
||||||
self.formLayout = QtWidgets.QFormLayout(ImportGroup)
|
self.formLayout = QtWidgets.QFormLayout(ImportGroup)
|
||||||
|
self.formLayout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
self.formLayout.setObjectName("formLayout")
|
self.formLayout.setObjectName("formLayout")
|
||||||
self.path_edit_label = QtWidgets.QLabel(ImportGroup)
|
self.path_edit_label = QtWidgets.QLabel(ImportGroup)
|
||||||
self.path_edit_label.setObjectName("path_edit_label")
|
self.path_edit_label.setObjectName("path_edit_label")
|
||||||
|
@ -40,6 +41,15 @@ class Ui_ImportGroup(object):
|
||||||
self.import_folder_check.setFont(font)
|
self.import_folder_check.setFont(font)
|
||||||
self.import_folder_check.setObjectName("import_folder_check")
|
self.import_folder_check.setObjectName("import_folder_check")
|
||||||
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.import_folder_check)
|
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.import_folder_check)
|
||||||
|
self.import_dlcs_label = QtWidgets.QLabel(ImportGroup)
|
||||||
|
self.import_dlcs_label.setObjectName("import_dlcs_label")
|
||||||
|
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.import_dlcs_label)
|
||||||
|
self.import_dlcs_check = QtWidgets.QCheckBox(ImportGroup)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setItalic(True)
|
||||||
|
self.import_dlcs_check.setFont(font)
|
||||||
|
self.import_dlcs_check.setObjectName("import_dlcs_check")
|
||||||
|
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.import_dlcs_check)
|
||||||
self.button_info_layout = QtWidgets.QHBoxLayout()
|
self.button_info_layout = QtWidgets.QHBoxLayout()
|
||||||
self.button_info_layout.setObjectName("button_info_layout")
|
self.button_info_layout.setObjectName("button_info_layout")
|
||||||
self.import_button = QtWidgets.QPushButton(ImportGroup)
|
self.import_button = QtWidgets.QPushButton(ImportGroup)
|
||||||
|
@ -50,11 +60,7 @@ class Ui_ImportGroup(object):
|
||||||
self.import_button.setSizePolicy(sizePolicy)
|
self.import_button.setSizePolicy(sizePolicy)
|
||||||
self.import_button.setObjectName("import_button")
|
self.import_button.setObjectName("import_button")
|
||||||
self.button_info_layout.addWidget(self.import_button)
|
self.button_info_layout.addWidget(self.import_button)
|
||||||
self.info_label = QtWidgets.QLabel(ImportGroup)
|
self.formLayout.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.button_info_layout)
|
||||||
self.info_label.setText("")
|
|
||||||
self.info_label.setObjectName("info_label")
|
|
||||||
self.button_info_layout.addWidget(self.info_label)
|
|
||||||
self.formLayout.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.button_info_layout)
|
|
||||||
|
|
||||||
self.retranslateUi(ImportGroup)
|
self.retranslateUi(ImportGroup)
|
||||||
QtCore.QMetaObject.connectSlotsByName(ImportGroup)
|
QtCore.QMetaObject.connectSlotsByName(ImportGroup)
|
||||||
|
@ -66,6 +72,8 @@ class Ui_ImportGroup(object):
|
||||||
self.app_name_label.setText(_translate("ImportGroup", "Override app name"))
|
self.app_name_label.setText(_translate("ImportGroup", "Override app name"))
|
||||||
self.import_folder_label.setText(_translate("ImportGroup", "Import all folders"))
|
self.import_folder_label.setText(_translate("ImportGroup", "Import all folders"))
|
||||||
self.import_folder_check.setText(_translate("ImportGroup", "Scan the installation path for game folders and import them"))
|
self.import_folder_check.setText(_translate("ImportGroup", "Scan the installation path for game folders and import them"))
|
||||||
|
self.import_dlcs_label.setText(_translate("ImportGroup", "Import DLCs"))
|
||||||
|
self.import_dlcs_check.setText(_translate("ImportGroup", "If a game has DLCs, try to import them too"))
|
||||||
self.import_button.setText(_translate("ImportGroup", "Import Game"))
|
self.import_button.setText(_translate("ImportGroup", "Import Game"))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>501</width>
|
<width>501</width>
|
||||||
<height>136</height>
|
<height>162</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
@ -20,6 +20,9 @@
|
||||||
<string>Import EGL game from a directory</string>
|
<string>Import EGL game from a directory</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QFormLayout" name="formLayout">
|
<layout class="QFormLayout" name="formLayout">
|
||||||
|
<property name="labelAlignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="path_edit_label">
|
<widget class="QLabel" name="path_edit_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -59,7 +62,26 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="import_dlcs_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Import DLCs</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="3" column="1">
|
||||||
|
<widget class="QCheckBox" name="import_dlcs_check">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<italic>true</italic>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>If a game has DLCs, try to import them too</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
<layout class="QHBoxLayout" name="button_info_layout">
|
<layout class="QHBoxLayout" name="button_info_layout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="import_button">
|
<widget class="QPushButton" name="import_button">
|
||||||
|
@ -74,13 +96,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="info_label">
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true"/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
|
||||||
|
|
||||||
from rare.utils.paths import tmp_dir
|
from rare.utils.paths import tmp_dir
|
||||||
from rare.utils.qt_requests import QtRequestManager
|
from rare.utils.qt_requests import QtRequestManager
|
||||||
from rare.utils.utils import icon as qta_icon
|
from rare.utils.misc import icon as qta_icon
|
||||||
|
|
||||||
logger = getLogger("ExtraWidgets")
|
logger = getLogger("ExtraWidgets")
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,19 @@ import os
|
||||||
import platform
|
import platform
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths
|
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable, QStandardPaths
|
||||||
from legendary.core import LegendaryCore
|
from legendary.core import LegendaryCore
|
||||||
from legendary.models.game import VerifyResult
|
|
||||||
from legendary.utils.lfs import validate_files
|
|
||||||
|
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.lgndr.api_arguments import LgndrVerifyGameArgs, LgndrUninstallGameArgs
|
||||||
|
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||||
|
from rare.lgndr.cli import LegendaryCLI
|
||||||
|
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
|
||||||
from rare.utils import config_helper
|
from rare.utils import config_helper
|
||||||
|
|
||||||
logger = getLogger("Legendary Utils")
|
logger = getLogger("Legendary Utils")
|
||||||
|
|
||||||
|
|
||||||
def uninstall(app_name: str, core: LegendaryCore, options=None):
|
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
|
||||||
if not options:
|
|
||||||
options = {"keep_files": False}
|
|
||||||
igame = core.get_installed_game(app_name)
|
igame = core.get_installed_game(app_name)
|
||||||
|
|
||||||
# remove shortcuts link
|
# remove shortcuts link
|
||||||
|
@ -40,31 +39,24 @@ def uninstall(app_name: str, core: LegendaryCore, options=None):
|
||||||
if os.path.exists(start_menu_shortcut):
|
if os.path.exists(start_menu_shortcut):
|
||||||
os.remove(start_menu_shortcut)
|
os.remove(start_menu_shortcut)
|
||||||
|
|
||||||
try:
|
status = LgndrIndirectStatus()
|
||||||
# Remove DLC first so directory is empty when game uninstall runs
|
LegendaryCLI(core).uninstall_game(
|
||||||
dlcs = core.get_dlc_for_game(app_name)
|
LgndrUninstallGameArgs(
|
||||||
for dlc in dlcs:
|
app_name=app_name,
|
||||||
if (idlc := core.get_installed_game(dlc.app_name)) is not None:
|
keep_files=keep_files,
|
||||||
logger.info(f'Uninstalling DLC "{dlc.app_name}"...')
|
indirect_status=status,
|
||||||
core.uninstall_game(idlc, delete_files=not options["keep_files"])
|
yes=True,
|
||||||
|
|
||||||
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
|
|
||||||
core.uninstall_game(
|
|
||||||
igame, delete_files=not options["keep_files"], delete_root_directory=True
|
|
||||||
)
|
)
|
||||||
logger.info("Game has been uninstalled.")
|
)
|
||||||
|
if not keep_config:
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Removing game failed: {e!r}, please remove {igame.install_path} manually."
|
|
||||||
)
|
|
||||||
if not options["keep_files"]:
|
|
||||||
logger.info("Removing sections in config file")
|
logger.info("Removing sections in config file")
|
||||||
config_helper.remove_section(app_name)
|
config_helper.remove_section(app_name)
|
||||||
config_helper.remove_section(f"{app_name}.env")
|
config_helper.remove_section(f"{app_name}.env")
|
||||||
|
|
||||||
config_helper.save_config()
|
config_helper.save_config()
|
||||||
|
|
||||||
|
return status.success, status.message
|
||||||
|
|
||||||
|
|
||||||
def update_manifest(app_name: str, core: LegendaryCore):
|
def update_manifest(app_name: str, core: LegendaryCore):
|
||||||
game = core.get_game(app_name)
|
game = core.get_game(app_name)
|
||||||
|
@ -78,146 +70,65 @@ def update_manifest(app_name: str, core: LegendaryCore):
|
||||||
new_manifest = core.load_manifest(new_manifest_data)
|
new_manifest = core.load_manifest(new_manifest_data)
|
||||||
logger.debug(f"Base urls: {base_urls}")
|
logger.debug(f"Base urls: {base_urls}")
|
||||||
# save manifest with version name as well for testing/downgrading/etc.
|
# save manifest with version name as well for testing/downgrading/etc.
|
||||||
core.lgd.save_manifest(
|
core.lgd.save_manifest(game.app_name, new_manifest_data, version=new_manifest.meta.build_version)
|
||||||
game.app_name, new_manifest_data, version=new_manifest.meta.build_version
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifySignals(QObject):
|
|
||||||
status = pyqtSignal(int, int, str)
|
|
||||||
summary = pyqtSignal(int, int, str)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyWorker(QRunnable):
|
class VerifyWorker(QRunnable):
|
||||||
|
class Signals(QObject):
|
||||||
|
status = pyqtSignal(str, int, int, float, float)
|
||||||
|
result = pyqtSignal(str, bool, int, int)
|
||||||
|
error = pyqtSignal(str, str)
|
||||||
|
|
||||||
num: int = 0
|
num: int = 0
|
||||||
total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized
|
total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized
|
||||||
|
|
||||||
def __init__(self, app_name):
|
def __init__(self, app_name):
|
||||||
super(VerifyWorker, self).__init__()
|
super(VerifyWorker, self).__init__()
|
||||||
self.signals = VerifySignals()
|
self.signals = VerifyWorker.Signals()
|
||||||
self.setAutoDelete(True)
|
self.setAutoDelete(True)
|
||||||
self.core = LegendaryCoreSingleton()
|
self.core = LegendaryCoreSingleton()
|
||||||
|
self.args = ArgumentsSingleton()
|
||||||
self.app_name = app_name
|
self.app_name = app_name
|
||||||
|
|
||||||
|
def status_callback(self, num: int, total: int, percentage: float, speed: float):
|
||||||
|
self.signals.status.emit(self.app_name, num, total, percentage, speed)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if not self.core.is_installed(self.app_name):
|
cli = LegendaryCLI(self.core)
|
||||||
logger.error(f'Game "{self.app_name}" is not installed')
|
status = LgndrIndirectStatus()
|
||||||
return
|
args = LgndrVerifyGameArgs(
|
||||||
|
app_name=self.app_name, indirect_status=status, verify_stdout=self.status_callback
|
||||||
logger.info(f'Loading installed manifest for "{self.app_name}"')
|
|
||||||
igame = self.core.get_installed_game(self.app_name)
|
|
||||||
manifest_data, _ = self.core.get_installed_manifest(self.app_name)
|
|
||||||
manifest = self.core.load_manifest(manifest_data)
|
|
||||||
|
|
||||||
files = sorted(manifest.file_manifest_list.elements,
|
|
||||||
key=lambda a: a.filename.lower())
|
|
||||||
|
|
||||||
# build list of hashes
|
|
||||||
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
|
|
||||||
total = len(file_list)
|
|
||||||
num = 0
|
|
||||||
failed = []
|
|
||||||
missing = []
|
|
||||||
|
|
||||||
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
|
|
||||||
repair_file = []
|
|
||||||
for result, path, result_hash, _ in validate_files(igame.install_path, file_list):
|
|
||||||
num += 1
|
|
||||||
self.signals.status.emit(num, total, self.app_name)
|
|
||||||
|
|
||||||
if result == VerifyResult.HASH_MATCH:
|
|
||||||
repair_file.append(f"{result_hash}:{path}")
|
|
||||||
continue
|
|
||||||
elif result == VerifyResult.HASH_MISMATCH:
|
|
||||||
logger.error(f'File does not match hash: "{path}"')
|
|
||||||
repair_file.append(f"{result_hash}:{path}")
|
|
||||||
failed.append(path)
|
|
||||||
elif result == VerifyResult.FILE_MISSING:
|
|
||||||
logger.error(f'File is missing: "{path}"')
|
|
||||||
missing.append(path)
|
|
||||||
else:
|
|
||||||
logger.error(f'Other failure (see log), treating file as missing: "{path}"')
|
|
||||||
missing.append(path)
|
|
||||||
|
|
||||||
# always write repair file, even if all match
|
|
||||||
if repair_file:
|
|
||||||
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{self.app_name}.repair')
|
|
||||||
with open(repair_filename, 'w') as f:
|
|
||||||
f.write('\n'.join(repair_file))
|
|
||||||
logger.debug(f'Written repair file to "{repair_filename}"')
|
|
||||||
|
|
||||||
if not missing and not failed:
|
|
||||||
logger.info('Verification finished successfully.')
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
|
|
||||||
self.signals.summary.emit(len(failed), len(missing), self.app_name)
|
|
||||||
|
|
||||||
|
|
||||||
# FIXME: lk: ah ef me sideways, we can't even import this thing properly
|
|
||||||
# FIXME: lk: so copy it here
|
|
||||||
def resolve_aliases(core: LegendaryCore, name):
|
|
||||||
# make sure aliases exist if not yet created
|
|
||||||
core.update_aliases(force=False)
|
|
||||||
name = name.strip()
|
|
||||||
# resolve alias (if any) to real app name
|
|
||||||
return core.lgd.config.get(
|
|
||||||
section='Legendary.aliases', option=name,
|
|
||||||
fallback=core.lgd.aliases.get(name.lower(), name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def import_game(core: LegendaryCore, app_name: str, path: str) -> str:
|
|
||||||
_tr = QCoreApplication.translate
|
|
||||||
logger.info(f"Import {app_name}")
|
|
||||||
game = core.get_game(app_name, update_meta=False)
|
|
||||||
if not game:
|
|
||||||
return _tr("LgdUtils", "Could not get game for {}").format(app_name)
|
|
||||||
|
|
||||||
if core.is_installed(app_name):
|
|
||||||
logger.error(f"{game.app_title} is already installed")
|
|
||||||
return _tr("LgdUtils", "{} is already installed").format(game.app_title)
|
|
||||||
|
|
||||||
if not os.path.exists(path):
|
|
||||||
logger.error("Path does not exist")
|
|
||||||
return _tr("LgdUtils", "Path does not exist")
|
|
||||||
|
|
||||||
manifest, igame = core.import_game(game, path)
|
|
||||||
exe_path = os.path.join(path, manifest.meta.launch_exe.lstrip("/"))
|
|
||||||
|
|
||||||
if not os.path.exists(exe_path):
|
|
||||||
logger.error(f"Launch Executable of {game.app_title} does not exist")
|
|
||||||
return _tr("LgdUtils", "Launch executable of {} does not exist").format(
|
|
||||||
game.app_title
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if game.is_dlc:
|
# lk: first pass, verify with the current manifest
|
||||||
release_info = game.metadata.get("mainGameItem", {}).get("releaseInfo")
|
repair_mode = False
|
||||||
if release_info:
|
result = cli.verify_game(
|
||||||
main_game_appname = release_info[0]["appId"]
|
args, print_command=False, repair_mode=repair_mode, repair_online=not self.args.offline
|
||||||
main_game_title = game.metadata["mainGameItem"]["title"]
|
)
|
||||||
if not core.is_installed(main_game_appname):
|
if result is None:
|
||||||
return _tr("LgdUtils", "Game is a DLC, but {} is not installed").format(
|
# lk: second pass with downloading the latest manifest
|
||||||
main_game_title
|
# lk: this happens if the manifest was not found and repair_mode was not requested
|
||||||
|
# lk: we already have checked if the directory exists before starting the worker
|
||||||
|
try:
|
||||||
|
# lk: this try-except block handles the exception caused by a missing manifest
|
||||||
|
# lk: and is raised only in the case we are offline
|
||||||
|
repair_mode = True
|
||||||
|
result = cli.verify_game(
|
||||||
|
args, print_command=False, repair_mode=repair_mode, repair_online=not self.args.offline
|
||||||
)
|
)
|
||||||
else:
|
if result is None:
|
||||||
return _tr("LgdUtils", "Unable to get base game information for DLC")
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
self.signals.error.emit(self.app_name, status.message)
|
||||||
|
return
|
||||||
|
|
||||||
total = len(manifest.file_manifest_list.elements)
|
success = result is not None and not any(result)
|
||||||
found = sum(
|
if success:
|
||||||
os.path.exists(os.path.join(path, f.filename))
|
# lk: if verification was successful we delete the repair file and run the clean procedure
|
||||||
for f in manifest.file_manifest_list.elements
|
# lk: this could probably be cut down to what is relevant for this use-case and skip the `cli` call
|
||||||
)
|
igame = self.core.get_installed_game(self.app_name)
|
||||||
ratio = found / total
|
game = self.core.get_game(self.app_name, platform=igame.platform)
|
||||||
|
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.app_name}.repair")
|
||||||
|
cli.install_game_cleanup(game=game, igame=igame, repair_mode=True, repair_file=repair_file)
|
||||||
|
|
||||||
if ratio < 0.9:
|
self.signals.result.emit(self.app_name, success, *result)
|
||||||
logger.warning(
|
|
||||||
"Game files are missing. It may be not the latest version or it is corrupt"
|
|
||||||
)
|
|
||||||
# return False
|
|
||||||
core.install_game(igame)
|
|
||||||
if igame.needs_verification:
|
|
||||||
logger.info(f"{igame.title} needs verification")
|
|
||||||
|
|
||||||
logger.info(f"Successfully imported Game: {game.app_title}")
|
|
||||||
return ""
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
import qtawesome
|
import qtawesome
|
||||||
import requests
|
import requests
|
||||||
|
@ -157,7 +157,7 @@ def get_latest_version():
|
||||||
return "0.0.0"
|
return "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
def get_size(b: int) -> str:
|
def get_size(b: Union[int, float]) -> str:
|
||||||
for i in ["", "K", "M", "G", "T", "P", "E"]:
|
for i in ["", "K", "M", "G", "T", "P", "E"]:
|
||||||
if b < 1024:
|
if b < 1024:
|
||||||
return f"{b:.2f}{i}B"
|
return f"{b:.2f}{i}B"
|
||||||
|
@ -165,7 +165,10 @@ def get_size(b: int) -> str:
|
||||||
|
|
||||||
|
|
||||||
def get_rare_executable() -> List[str]:
|
def get_rare_executable() -> List[str]:
|
||||||
if platform.system() == "Linux" or platform.system() == "Darwin":
|
# lk: detech if nuitka
|
||||||
|
if "__compiled__" in globals():
|
||||||
|
executable = [sys.executable]
|
||||||
|
elif platform.system() == "Linux" or platform.system() == "Darwin":
|
||||||
# TODO flatpak
|
# TODO flatpak
|
||||||
if p := os.environ.get("APPIMAGE"):
|
if p := os.environ.get("APPIMAGE"):
|
||||||
executable = [p]
|
executable = [p]
|
|
@ -1,61 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import platform as pf
|
from typing import Union, List
|
||||||
from dataclasses import field, dataclass
|
|
||||||
from multiprocessing import Queue
|
|
||||||
from typing import Union, List, Optional
|
|
||||||
|
|
||||||
from legendary.core import LegendaryCore
|
from legendary.core import LegendaryCore
|
||||||
from legendary.downloader.mp.manager import DLManager
|
|
||||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
|
||||||
from legendary.models.game import Game, InstalledGame
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class InstallOptionsModel:
|
|
||||||
app_name: str
|
|
||||||
base_path: str = ""
|
|
||||||
max_shm: int = 1024
|
|
||||||
max_workers: int = os.cpu_count() * 2
|
|
||||||
repair: bool = False
|
|
||||||
no_install: bool = False
|
|
||||||
ignore_space_req: bool = False
|
|
||||||
force: bool = False
|
|
||||||
sdl_list: list = field(default_factory=lambda: [""])
|
|
||||||
update: bool = False
|
|
||||||
silent: bool = False
|
|
||||||
platform: str = ""
|
|
||||||
dl_optimizations: bool = False
|
|
||||||
overlay: bool = False
|
|
||||||
create_shortcut: bool = True
|
|
||||||
install_preqs: bool = pf.system() == "Windows"
|
|
||||||
|
|
||||||
def set_no_install(self, enabled: bool) -> None:
|
|
||||||
self.no_install = enabled
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class InstallDownloadModel:
|
|
||||||
dlmanager: DLManager
|
|
||||||
analysis: AnalysisResult
|
|
||||||
game: Game
|
|
||||||
igame: InstalledGame
|
|
||||||
repair: bool
|
|
||||||
repair_file: str
|
|
||||||
res: ConditionCheckResult
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class InstallQueueItemModel:
|
|
||||||
status_q: Optional[Queue] = None
|
|
||||||
download: Optional[InstallDownloadModel] = None
|
|
||||||
options: Optional[InstallOptionsModel] = None
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return (
|
|
||||||
(self.status_q is not None)
|
|
||||||
and (self.download is not None)
|
|
||||||
and (self.options is not None)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PathSpec:
|
class PathSpec:
|
||||||
|
@ -80,9 +26,7 @@ class PathSpec:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wine_egl_programdata(self):
|
def wine_egl_programdata(self):
|
||||||
return self.egl_programdata.replace("\\", "/").replace(
|
return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
|
||||||
"%PROGRAMDATA%", self.wine_programdata
|
|
||||||
)
|
|
||||||
|
|
||||||
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
|
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
|
||||||
possible_prefixes = [
|
possible_prefixes = [
|
||||||
|
|
|
@ -11,7 +11,7 @@ from legendary.core import LegendaryCore
|
||||||
|
|
||||||
import rare.resources.resources
|
import rare.resources.resources
|
||||||
from rare.utils.paths import resources_path
|
from rare.utils.paths import resources_path
|
||||||
from rare.utils.utils import set_color_pallete, set_style_sheet
|
from rare.utils.misc import set_color_pallete, set_style_sheet
|
||||||
|
|
||||||
|
|
||||||
class RareApp(QApplication):
|
class RareApp(QApplication):
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
typing_extensions
|
||||||
requests
|
requests
|
||||||
PyQt5
|
PyQt5
|
||||||
QtAwesome
|
QtAwesome
|
||||||
psutil
|
psutil
|
||||||
pypresence
|
pypresence
|
||||||
|
setuptools
|
||||||
|
legendary-gl
|
||||||
pywin32; platform_system == "Windows"
|
pywin32; platform_system == "Windows"
|
||||||
|
|
Loading…
Reference in a new issue