1
0
Fork 0
mirror of synced 2024-05-20 04:22:58 +12:00

Merge pull request #294 from loathingKernel/develop

Fixes for DLCs and downloads
This commit is contained in:
Dummerle 2023-06-11 15:50:50 +02:00 committed by GitHub
commit c586305927
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 129 additions and 123 deletions

View file

@ -89,7 +89,7 @@ class DownloadsTab(QWidget):
@pyqtSlot()
@pyqtSlot(int)
def update_queues_count(self):
count = self.updates_group.count() + self.queue_group.count() + (1 if self.is_download_active else 0)
count = self.updates_group.count() + self.queue_group.count() + (1 if self.is_download_active else 0)
self.update_title.emit(count)
@property
@ -131,10 +131,11 @@ class DownloadsTab(QWidget):
:param app_name:
:return:
"""
rgame = self.rcore.get_game(app_name)
rgame.state = RareGame.State.IDLE
if self.updates_group.contains(app_name):
self.updates_group.set_widget_enabled(app_name, True)
else:
rgame = self.rcore.get_game(app_name)
if rgame.is_installed and rgame.has_update:
self.__add_update(app_name)
@ -180,15 +181,17 @@ class DownloadsTab(QWidget):
def __start_download(self, item: InstallQueueItemModel):
rgame = self.rcore.get_game(item.options.app_name)
if not rgame.is_idle:
logger.error(f"Can't start download {item.options.app_name} due to non-idle state {rgame.state}")
if not rgame.state == RareGame.State.DOWNLOADING:
logger.error(
f"Can't start download {item.options.app_name}"
f"due to incompatible state {RareGame.State(rgame.state).name}"
)
# lk: invalidate the queue item in case the game was uninstalled
self.__requeue_download(InstallQueueItemModel(options=item.options))
return
if item.expired:
self.__refresh_download(item)
return
rgame.state = RareGame.State.DOWNLOADING
thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug)
thread.result.connect(self.__on_download_result)
thread.progress.connect(self.__on_download_progress)
@ -215,6 +218,7 @@ class DownloadsTab(QWidget):
def __requeue_download(self, item: InstallQueueItemModel):
rgame = self.rcore.get_game(item.options.app_name)
rgame.state = RareGame.State.DOWNLOADING
self.queue_group.push_front(item, rgame.igame)
logger.info(f"Re-queued download for {rgame.app_name} ({rgame.app_title})")
@ -278,8 +282,10 @@ class DownloadsTab(QWidget):
@pyqtSlot(InstallOptionsModel)
def __get_install_options(self, options: InstallOptionsModel):
rgame = self.rcore.get_game(options.app_name)
rgame.state = RareGame.State.DOWNLOADING
install_dialog = InstallDialog(
self.rcore.get_game(options.app_name),
rgame,
options=options,
parent=self,
)
@ -288,8 +294,10 @@ class DownloadsTab(QWidget):
@pyqtSlot(InstallQueueItemModel)
def __on_install_dialog_closed(self, item: InstallQueueItemModel):
rgame = self.rcore.get_game(item.options.app_name)
if item and not item.download.game.is_dlc and not item.download.analysis.dl_size:
self.rcore.get_game(item.download.game.app_name).set_installed(True)
rgame.set_installed(True)
rgame.state = RareGame.State.IDLE
return
if item:
# lk: start update only if there is no other active thread and there is no queue
@ -310,11 +318,14 @@ class DownloadsTab(QWidget):
else:
if self.updates_group.contains(item.options.app_name):
self.updates_group.set_widget_enabled(item.options.app_name, True)
rgame.state = RareGame.State.IDLE
@pyqtSlot(UninstallOptionsModel)
def __get_uninstall_options(self, options: UninstallOptionsModel):
rgame = self.rcore.get_game(options.app_name)
rgame.state = RareGame.State.UNINSTALLING
uninstall_dialog = UninstallDialog(
self.rcore.get_game(options.app_name),
rgame,
options=options,
parent=self,
)
@ -323,14 +334,17 @@ class DownloadsTab(QWidget):
@pyqtSlot(UninstallOptionsModel)
def __on_uninstall_dialog_closed(self, options: UninstallOptionsModel):
rgame = self.rcore.get_game(options.app_name)
if options and options.accepted:
rgame = self.rcore.get_game(options.app_name)
rgame.set_installed(False)
worker = UninstallWorker(self.core, rgame, options)
worker.signals.result.connect(self.__on_uninstall_worker_result)
QThreadPool.globalInstance().start(worker)
else:
rgame.state = RareGame.State.IDLE
@pyqtSlot(RareGame, bool, str)
def __on_uninstall_worker_result(self, rgame: RareGame, success: bool, message: str):
if not success:
QMessageBox.warning(None, self.tr("Uninstall - {}").format(rgame.title), message, QMessageBox.Close)
rgame.state = RareGame.State.IDLE

View file

@ -31,6 +31,13 @@ class GameDlcWidget(QFrame):
self.image.setPixmap(rdlc.pixmap)
self.__update()
rdlc.signals.widget.update.connect(self.__update)
@pyqtSlot()
def __update(self):
self.ui.action_button.setEnabled(self.rdlc.is_idle)
class InstalledGameDlcWidget(GameDlcWidget):
uninstalled = pyqtSignal(RareGame)
@ -42,7 +49,11 @@ class InstalledGameDlcWidget(GameDlcWidget):
self.ui.action_button.clicked.connect(self.uninstall_dlc)
self.ui.action_button.setText(self.tr("Uninstall DLC"))
# lk: don't reference `self.rdlc` here because the object has been deleted
rdlc.signals.game.uninstalled.connect(lambda: self.uninstalled.emit(rdlc))
rdlc.signals.game.uninstalled.connect(self.__uninstalled)
@pyqtSlot()
def __uninstalled(self):
self.uninstalled.emit(self.rdlc)
def uninstall_dlc(self):
self.rdlc.uninstall()
@ -58,7 +69,11 @@ class AvailableGameDlcWidget(GameDlcWidget):
self.ui.action_button.clicked.connect(self.install_dlc)
self.ui.action_button.setText(self.tr("Install DLC"))
# lk: don't reference `self.rdlc` here because the object has been deleted
rdlc.signals.game.installed.connect(lambda: self.installed.emit(rdlc))
rdlc.signals.game.installed.connect(self.__installed)
@pyqtSlot()
def __installed(self):
self.installed.emit(self.rdlc)
def install_dlc(self):
if not self.rgame.is_installed:

View file

@ -5,7 +5,7 @@ from dataclasses import dataclass, field
from datetime import datetime
from logging import getLogger
from threading import Lock
from typing import List, Optional, Dict
from typing import List, Optional, Dict, Set
from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool
from PyQt5.QtGui import QPixmap
@ -77,7 +77,7 @@ class RareGame(RareGameSlim):
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata()
self.owned_dlcs: List[RareGame] = []
self.owned_dlcs: Set[RareGame] = set()
if self.has_update:
logger.info(f"Update available for game: {self.app_name} ({self.app_title})")
@ -530,12 +530,15 @@ class RareGame(RareGameSlim):
self.signals.game.uninstalled.emit(self.app_name)
self.set_pixmap()
def repair(self, repair_and_update):
def repair(self, repair_and_update) -> bool:
if not self.is_idle:
return False
self.signals.game.install.emit(
InstallOptionsModel(
app_name=self.app_name, repair_mode=True, repair_and_update=repair_and_update, update=repair_and_update
)
)
return True
def uninstall(self) -> bool:
if not self.is_idle:

View file

@ -120,4 +120,4 @@ class UninstallOptionsModel:
"""
self.accepted = values[0]
self.keep_files = values[1]
self.keep_config = values[2]
self.keep_config = values[2]

View file

@ -5,7 +5,7 @@ import zlib
# from concurrent import futures
from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Set
from typing import Tuple, Dict, Union, Type, List, Callable
import requests
@ -34,7 +34,7 @@ from legendary.models.game import Game
from rare.lgndr.core import LegendaryCore
from rare.models.image import ImageSize
from rare.models.signals import GlobalSignals
from rare.utils.paths import image_dir, resources_path, desktop_icon_suffix, desktop_links_supported
from rare.utils.paths import image_dir, resources_path, desktop_icon_suffix
# from requests_futures.sessions import FuturesSession
@ -61,7 +61,7 @@ class ImageManager(QObject):
def run(self):
self.func(self.updates, self.json_data, self.game)
logger.debug(f" Emitting singal for game {self.game.app_name} - {self.game.app_title}")
logger.debug(f" Emitting singal for {self.game.app_name} ({self.game.app_title})")
self.signals.completed.emit(self.game)
def __init__(self, signals: GlobalSignals, core: LegendaryCore):
@ -69,7 +69,7 @@ class ImageManager(QObject):
# self.__img_types: Tuple = ("DieselGameBoxTall", "Thumbnail", "DieselGameBoxLogo", "DieselGameBox", "OfferImageTall")
self.__img_types: Tuple = ("DieselGameBoxTall", "Thumbnail", "DieselGameBoxLogo", "OfferImageTall")
self.__dl_retries = 1
self.__worker_app_names: List[str] = []
self.__worker_app_names: Set[str] = set()
super(QObject, self).__init__()
self.signals = signals
self.core = core
@ -116,9 +116,17 @@ class ImageManager(QObject):
else:
json_data = json.load(open(self.__img_json(game.app_name), "r"))
# lk: fast path for games without images, convert Rare's logo
if not game.metadata.get("keyImages", False):
if not self.__img_color(game.app_name).is_file() or not self.__img_gray(game.app_name).is_file():
# lk: Find updates or initialize if images are missing.
# lk: `updates` will be empty for games without images
# lk: so everything below it is skipped
updates = []
if not (
self.__img_color(game.app_name).is_file()
and self.__img_gray(game.app_name).is_file()
and self.__img_desktop_icon(game.app_name).is_file()
):
# lk: fast path for games without images, convert Rare's logo
if not game.metadata.get("keyImages", []):
cache_data: Dict = dict(zip(self.__img_types, [None] * len(self.__img_types)))
cache_data["DieselGameBoxTall"] = open(
resources_path.joinpath("images", "cover.png"), "rb"
@ -130,19 +138,10 @@ class ImageManager(QObject):
json_data["scale"] = ImageSize.Image.pixel_ratio
json_data["size"] = ImageSize.Image.size.__str__()
json.dump(json_data, open(self.__img_json(game.app_name), "w"))
# lk: Find updates or initialize if images are missing.
# lk: `updates` will be empty for games without images
# lk: so everything below it is skipped
if not (
self.__img_color(game.app_name).is_file()
and self.__img_gray(game.app_name).is_file()
and self.__img_desktop_icon(game.app_name).is_file()
):
updates = [image for image in game.metadata["keyImages"] if image["type"] in self.__img_types]
else:
updates = [image for image in game.metadata["keyImages"] if image["type"] in self.__img_types]
else:
updates = []
for image in game.metadata["keyImages"]:
for image in game.metadata.get("keyImages", []):
if image["type"] in self.__img_types:
if image["type"] not in json_data.keys() or json_data[image["type"]] != image["md5"]:
updates.append(image)
@ -180,9 +179,10 @@ class ImageManager(QObject):
# cache_data[req.image_type] = req.result().content
# else:
for image in updates:
logger.info(f"Downloading {image['type']} for {game.app_title}")
logger.info(f"Downloading {image['type']} for {game.app_name} ({game.app_title})")
json_data[image["type"]] = image["md5"]
payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
# cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=2).content
cache_data[image["type"]] = requests.get(image["url"], params=payload).content
self.__convert(game, cache_data)
@ -281,9 +281,9 @@ class ImageManager(QObject):
painter.end()
cover = cover.scaled(ImageSize.Image.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
if desktop_links_supported():
icon = self.__convert_icon(cover)
icon.save(str(self.__img_desktop_icon(game.app_name)), format=desktop_icon_suffix().upper())
icon = self.__convert_icon(cover)
icon.save(str(self.__img_desktop_icon(game.app_name)), format=desktop_icon_suffix().upper())
# this is not required if we ever want to re-apply the alpha channel
# cover = cover.convertToFormat(QImage.Format_Indexed8)
@ -318,16 +318,17 @@ class ImageManager(QObject):
def download_image(
self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False
) -> None:
if game.app_name in self.__worker_app_names:
return
self.__worker_app_names.add(game.app_name)
updates, json_data = self.__prepare_download(game, force)
if not updates:
self.__worker_app_names.remove(game.app_name)
load_callback()
return
if updates and game.app_name not in self.__worker_app_names:
else:
image_worker = ImageManager.Worker(self.__download, updates, json_data, game)
self.__worker_app_names.append(game.app_name)
image_worker.signals.completed.connect(load_callback)
image_worker.signals.completed.connect(lambda g: self.__worker_app_names.remove(g.app_name))
image_worker.signals.completed.connect(load_callback)
self.threadpool.start(image_worker, priority)
def download_image_blocking(self, game: Game, force: bool = False) -> None:

View file

@ -4,7 +4,7 @@ import time
from argparse import Namespace
from itertools import chain
from logging import getLogger
from typing import Dict, Iterator, Callable, Optional, List, Union, Iterable
from typing import Dict, Iterator, Callable, Optional, List, Union, Iterable, Tuple
from PyQt5.QtCore import QObject, pyqtSignal, QSettings, pyqtSlot, QThreadPool, QRunnable, QTimer
from legendary.lfs.eos import EOSOverlayApp
@ -21,8 +21,6 @@ from .workers import (
VerifyWorker,
MoveWorker,
FetchWorker,
GamesWorker,
NonAssetWorker,
OriginWineWorker,
)
from .workers.uninstall import uninstall_game
@ -52,11 +50,6 @@ class RareCore(QObject):
self.__image_manager: Optional[ImageManager] = None
self.__start_time = time.time()
self.__fetched_games: List = []
self.__fetched_dlcs: Dict = {}
self.__games_fetched: bool = False
self.__non_asset_fetched: bool = False
self.args(args)
self.signals(init=True)
@ -144,19 +137,18 @@ class RareCore(QObject):
with open(os.path.join(path, "config.ini"), "w") as config_file:
config_file.write("[Legendary]")
self.__core = LegendaryCore()
if "Legendary" not in self.__core.lgd.config.sections():
self.__core.lgd.config.add_section("Legendary")
self.__core.lgd.save_config()
for section in ["Legendary", "default", "default.env"]:
if section not in self.__core.lgd.config.sections():
self.__core.lgd.config.add_section(section)
# workaround if egl sync enabled, but no programdata_path
# programdata_path might be unset if logging in through the browser
if self.__core.egl_sync_enabled:
if self.__core.egl.programdata_path is None:
self.__core.lgd.config.remove_option("Legendary", "egl_sync")
self.__core.lgd.save_config()
else:
if not os.path.exists(self.__core.egl.programdata_path):
self.__core.lgd.config.remove_option("Legendary", "egl_sync")
self.__core.lgd.save_config()
self.__core.lgd.save_config()
return self.__core
def image_manager(self, init: bool = False) -> ImageManager:
@ -264,60 +256,27 @@ class RareCore(QObject):
rdlc.signals.progress.start.connect(rgame.signals.progress.start)
rdlc.signals.progress.update.connect(rgame.signals.progress.update)
rdlc.signals.progress.finish.connect(rgame.signals.progress.finish)
rgame.owned_dlcs.append(rdlc)
rgame.owned_dlcs.add(rdlc)
self.__add_game(rdlc)
self.__add_game(rgame)
self.progress.emit(int(idx/length * 80) + 20, self.tr("Loaded <b>{}</b>").format(rgame.app_title))
@pyqtSlot(object, int)
def handle_result(self, result: object, res_type: int):
status = ""
if res_type == FetchWorker.Result.GAMES:
games, dlc_dict = result
self.__fetched_games += games
self.__fetched_dlcs.update(dlc_dict)
self.fetch_non_asset()
self.__games_fetched = True
status = self.tr("Prepared games")
if res_type == FetchWorker.Result.NON_ASSET:
games, dlc_dict = result
self.__fetched_games += games
for catalog_id, dlcs in dlc_dict.items():
if catalog_id in self.__fetched_dlcs.keys():
self.__fetched_dlcs[catalog_id] += dlcs
else:
self.__fetched_dlcs[catalog_id] = dlcs
self.__non_asset_fetched = True
status = self.tr("Prepared games without assets")
def __on_fetch_result(self, result: Tuple[List, Dict], res_type: int):
logger.info(f"Got API results for {FetchWorker.Result(res_type).name}")
fetched = [
self.__games_fetched,
self.__non_asset_fetched,
]
self.progress.emit(sum(fetched) * 10, status)
if all(fetched):
self.__add_games_and_dlcs(self.__fetched_games, self.__fetched_dlcs)
self.progress.emit(100, self.tr("Launching Rare"))
logger.debug(f"Fetch time {time.time() - self.__start_time} seconds")
QTimer.singleShot(100, self.__post_init)
self.completed.emit()
self.progress.emit(15, self.tr("Preparing library"))
self.__add_games_and_dlcs(*result)
self.progress.emit(100, self.tr("Launching Rare"))
logger.debug(f"Fetch time {time.time() - self.__start_time} seconds")
QTimer.singleShot(100, self.__post_init)
self.completed.emit()
def fetch(self):
self.__games_fetched: bool = False
self.__non_asset_fetched: bool = False
self.__start_time = time.time()
games_worker = GamesWorker(self.__core, self.__args)
games_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(games_worker)
def fetch_non_asset(self):
non_asset_worker = NonAssetWorker(self.__core, self.__args)
non_asset_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(non_asset_worker)
fetch_worker = FetchWorker(self.__core, self.__args)
fetch_worker.signals.progress.connect(self.progress)
fetch_worker.signals.result.connect(self.__on_fetch_result)
QThreadPool.globalInstance().start(fetch_worker)
def fetch_saves(self):
def __fetch() -> None:

View file

@ -1,4 +1,4 @@
from .fetch import FetchWorker, GamesWorker, NonAssetWorker
from .fetch import FetchWorker
from .install_info import InstallInfoWorker
from .move import MoveWorker
from .uninstall import UninstallWorker

View file

@ -16,8 +16,10 @@ class FetchWorker(Worker):
class Result(IntEnum):
GAMES = 1
NON_ASSET = 2
COMBINED = 3
class Signals(QObject):
progress = pyqtSignal(int, str)
result = pyqtSignal(object, int)
def __init__(self, core: LegendaryCore, args: Namespace):
@ -26,33 +28,43 @@ class FetchWorker(Worker):
self.core = core
self.args = args
class GamesWorker(FetchWorker):
def run_real(self):
# Fetch regular EGL games with assets
self.signals.progress.emit(0, self.signals.tr("Updating game metadata"))
start_time = time.time()
result = self.core.get_game_and_dlc_list(update_assets=not self.args.offline, platform="Windows", skip_ue=False)
self.signals.result.emit(result, FetchWorker.Result.GAMES)
logger.debug(f"Games: {len(result[0])}, DLCs {len(result[1])}")
logger.debug(f"Request Games: {time.time() - start_time} seconds")
games, dlc_dict = self.core.get_game_and_dlc_list(
update_assets=not self.args.offline, platform="Windows", skip_ue=False
)
logger.debug(f"Games {len(games)}, games with DLCs {len(dlc_dict)}")
logger.debug(f"Request games: {time.time() - start_time} seconds")
class NonAssetWorker(FetchWorker):
def run_real(self):
# Fetch non-asset games
self.signals.progress.emit(10, self.signals.tr("Updating non-asset metadata"))
start_time = time.time()
try:
result = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False)
na_games, na_dlc_dict = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False)
except (HTTPError, ConnectionError) as e:
logger.warning(f"Exception while fetching non asset games from EGS: {e}")
result = ([], {})
na_games, na_dlc_dict = ([], {})
# FIXME:
# This is here because of broken appIds from Epic:
# https://discord.com/channels/826881530310819914/884510635642216499/1111321692703305729
# There is a tab character in the appId of Fallout New Vegas: Honest Hearts DLC, this breaks metadata storage
# on Windows as they can't handle tabs at the end of the filename (?)
# Legendary and Heroic are also affected, but it completed breaks Rare, so dodge it for now pending a fix.
# Legendary and Heroic are also affected, but it completely breaks Rare, so dodge it for now pending a fix.
except Exception as e:
logger.error(f"Exception while fetching non asset games from EGS: {e}")
result = ([], {})
self.signals.result.emit(result, FetchWorker.Result.NON_ASSET)
logger.debug(f"Non asset: {len(result[0])}, DLCs {len(result[1])}")
logger.debug(f"Request Non Asset: {time.time() - start_time} seconds")
na_games, na_dlc_dict = ([], {})
logger.debug(f"Non-asset {len(na_games)}, games with non-asset DLCs {len(na_dlc_dict)}")
logger.debug(f"Request non-asset: {time.time() - start_time} seconds")
# Combine the two games lists and the two dlc dictionaries between regular and non-asset results
games += na_games
for catalog_id, dlcs in na_dlc_dict.items():
if catalog_id in dlc_dict.keys():
dlc_dict[catalog_id] += dlcs
else:
dlc_dict[catalog_id] = dlcs
logger.debug(f"Games {len(games)}, games with DLCs {len(dlc_dict)}")
self.signals.result.emit((games, dlc_dict), FetchWorker.Result.COMBINED)

View file

@ -60,6 +60,7 @@ def create_dirs() -> None:
for path in (data_dir(), cache_dir(), image_dir(), log_dir(), tmp_dir()):
if not path.exists():
path.mkdir(parents=True)
logger.info(f"Created directory at {path}")
def home_dir() -> Path:
@ -83,14 +84,15 @@ __link_suffix = {
"link": "desktop",
"icon": "png",
},
# "Darwin": {
# "link": "",
# "icon": "icns",
# },
"Darwin": {
"link": "",
"icon": "icns",
},
}
def desktop_links_supported() -> bool:
return platform.system() in __link_suffix.keys()
supported_systems = [k for (k, v) in __link_suffix.items() if v["link"]]
return platform.system() in supported_systems
def desktop_icon_suffix() -> str:
return __link_suffix[platform.system()]["icon"]