1
0
Fork 0
mirror of synced 2024-06-26 10:11:19 +12:00

InstallQueueItemModel: Refactor to add an expiration date to the download

While not sure if it is required, add an expiration date to the prepared
download 30 minutes after it was prepared. If a download has been in the
queue for more than 30 minutes, the download will be prepared again silently
before starting.

Return only the `InstallOptionsModel` in the result of the download thread
to avoid the potential mistake of re-using it. This required for the tray
notification signal to operate on the `app_name` instead of the `app_title`.
As a result, the notification slot was moved into the TrayIcon class for
better encapsulation.
This commit is contained in:
loathingKernel 2023-01-27 22:05:39 +02:00
parent 90021e34f2
commit 2ef70b8eb4
7 changed files with 105 additions and 69 deletions

View file

@ -15,7 +15,7 @@ from PyQt5.QtWidgets import (
from rare.components.tabs import TabWidget
from rare.components.tray_icon import TrayIcon
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared import RareCore
from rare.utils.paths import lock_file
logger = getLogger("MainWindow")
@ -31,10 +31,9 @@ class MainWindow(QMainWindow):
self._window_launched = False
super(MainWindow, self).__init__(parent=parent)
self.setAttribute(Qt.WA_DeleteOnClose, True)
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton()
self.core = RareCore.instance().core()
self.signals = RareCore.instance().signals()
self.args = RareCore.instance().args()
self.settings = QSettings()
self.setWindowTitle("Rare - GUI for legendary")
@ -68,17 +67,6 @@ class MainWindow(QMainWindow):
self.tray_icon.show_app.connect(self.show)
self.tray_icon.activated.connect(lambda r: self.toggle() if r == self.tray_icon.DoubleClick else None)
self.signals.application.notify.connect(
lambda title: self.tray_icon.showMessage(
self.tr("Download finished"),
self.tr("Download finished. {} is playable now").format(title),
self.tray_icon.Information,
4000,
)
if self.settings.value("notification", True, bool)
else None
)
# enable kinetic scrolling
for scroll_area in self.findChildren(QScrollArea):
if not scroll_area.property("no_kinetic_scroll"):

View file

@ -13,8 +13,9 @@ from rare.components.dialogs.install_dialog import InstallDialog
from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel, InstallDownloadModel
from rare.shared import RareCore
from rare.shared.workers.install_info import InstallInfoWorker
from rare.shared.workers.uninstall import UninstallWorker
from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab
from rare.utils.misc import get_size, create_desktop_link
@ -127,7 +128,7 @@ class DownloadsTab(QWidget):
self.stop_download()
self.__forced_item = item
else:
self.__start_installation(item)
self.__start_download(item)
def stop_download(self, omit_queue=False):
"""
@ -148,7 +149,23 @@ class DownloadsTab(QWidget):
if omit_queue:
self.thread.wait()
def __start_installation(self, item: InstallQueueItemModel):
def __refresh_download(self, item: InstallQueueItemModel):
worker = InstallInfoWorker(self.core, item.options)
worker.signals.result.connect(
lambda d: self.__start_download(InstallQueueItemModel(options=item.options, download=d))
)
worker.signals.failed.connect(
lambda m: logger.error(f"Failed to refresh download for {item.options.app_name} with error: {m}")
)
worker.signals.finished.connect(
lambda: logger.info(f"Download refresh worker finished for {item.options.app_name}")
)
self.threadpool.start(worker)
def __start_download(self, item: InstallQueueItemModel):
if item.expired:
self.__refresh_download(item)
return
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)
@ -179,42 +196,42 @@ class DownloadsTab(QWidget):
def __on_download_result(self, result: DlResultModel):
if result.code == DlResultCode.FINISHED:
if result.shortcuts:
if not create_desktop_link(result.item.options.app_name, self.core, "desktop"):
if not create_desktop_link(result.options.app_name, self.core, "desktop"):
# maybe add it to download summary, to show in finished downloads
pass
else:
logger.info("Desktop shortcut written")
logger.info(
f"Download finished: {result.item.download.game.app_name} ({result.item.download.game.app_title})"
f"Download finished: {result.options.app_name}"
)
if result.item.options.overlay:
if result.options.overlay:
self.signals.application.overlay_installed.emit()
else:
self.signals.application.notify.emit(result.item.download.game.app_title)
self.signals.application.notify.emit(result.options.app_name)
if self.updates_group.contains(result.item.options.app_name):
self.updates_group.set_widget_enabled(result.item.options.app_name, True)
if self.updates_group.contains(result.options.app_name):
self.updates_group.set_widget_enabled(result.options.app_name, True)
elif result.code == DlResultCode.ERROR:
QMessageBox.warning(self, self.tr("Error"), self.tr("Download error: {}").format(result.message))
logger.error(f"Download error: {result.message}")
logger.error(f"Download error: {result.options.app_name} ({result.message})")
elif result.code == DlResultCode.STOPPED:
logger.info(f"Download stopped: {result.item.download.game.app_title}")
logger.info(f"Download stopped: {result.options.app_name}")
if not self.__omit_requeue:
self.__requeue_download(InstallQueueItemModel(options=result.item.options))
self.__requeue_download(InstallQueueItemModel(options=result.options))
else:
return
# lk: if we finished a repair, and we have a disabled update, re-enable it
if self.updates_group.contains(result.item.options.app_name):
self.updates_group.set_widget_enabled(result.item.options.app_name, True)
if self.updates_group.contains(result.options.app_name):
self.updates_group.set_widget_enabled(result.options.app_name, True)
if result.code == DlResultCode.FINISHED and self.queue_group.count():
self.__start_installation(self.queue_group.pop_front())
self.__start_download(self.queue_group.pop_front())
elif result.code == DlResultCode.STOPPED and self.__forced_item:
self.__start_installation(self.__forced_item)
self.__start_download(self.__forced_item)
self.__forced_item = None
else:
self.__reset_download()
@ -244,7 +261,7 @@ class DownloadsTab(QWidget):
if item:
# lk: start update only if there is no other active thread and there is no queue
if self.thread is None and not self.queue_group.count():
self.__start_installation(item)
self.__start_download(item)
else:
rgame = self.rcore.get_game(item.options.app_name)
self.queue_group.push_back(item, rgame.igame)
@ -273,7 +290,7 @@ class DownloadsTab(QWidget):
@pyqtSlot(UninstallOptionsModel)
def __on_uninstall_dialog_closed(self, options: UninstallOptionsModel):
if options and options.uninstall:
if options and options.accepted:
rgame = self.rcore.get_game(options.app_name)
rgame.set_installed(False)
worker = UninstallWorker(self.core, rgame, options)

View file

@ -15,7 +15,7 @@ from rare.lgndr.core import LegendaryCore
from rare.lgndr.glue.monkeys import DLManagerSignals
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.install import InstallQueueItemModel
from rare.models.install import InstallQueueItemModel, InstallOptionsModel
logger = getLogger("DownloadThread")
@ -25,9 +25,10 @@ class DlResultCode(IntEnum):
STOPPED = 2
FINISHED = 3
@dataclass
class DlResultModel:
item: InstallQueueItemModel
options: InstallOptionsModel
code: DlResultCode = DlResultCode.ERROR
message: str = ""
dlcs: Optional[List[Dict]] = None
@ -35,6 +36,7 @@ class DlResultModel:
tip_url: str = ""
shortcuts: bool = False
class DlThread(QThread):
result = pyqtSignal(DlResultModel)
progress = pyqtSignal(UIUpdate, c_ulonglong)
@ -59,7 +61,7 @@ class DlThread(QThread):
cli = LegendaryCLI(self.core)
self.item.download.dlm.logging_queue = cli.logging_queue
self.item.download.dlm.proc_debug = self.debug
result = DlResultModel(self.item)
result = DlResultModel(self.item.options)
start_t = time.time()
try:
self.item.download.dlm.start()

View file

@ -105,7 +105,6 @@ class QueueWidget(QFrame):
# InstallQueueItemModel
force = pyqtSignal(InstallQueueItemModel)
def __init__(self, item: InstallQueueItemModel, old_igame: InstalledGame, parent=None):
super(QueueWidget, self).__init__(parent=parent)
self.ui = Ui_DownloadWidget()
@ -113,15 +112,18 @@ class QueueWidget(QFrame):
# lk: setObjectName has to be after `setupUi` because it is also set in that function
self.setObjectName(widget_object_name(self, item.options.app_name))
self.threadpool = QThreadPool.globalInstance()
if not item:
self.ui.queue_buttons.setEnabled(False)
worker = InstallInfoWorker(RareCore.instance().core(), item.options)
worker.signals.result.connect(self.add_info_widget)
worker.signals.failed.connect(self.__on_info_worker_failed)
worker.signals.finished.connect(self.__on_info_worker_finished)
self.threadpool.start(worker)
worker.signals.result.connect(self.__update_info)
worker.signals.failed.connect(
lambda m: logger.error(f"Failed to requeue download for {item.options.app_name} with error: {m}")
)
worker.signals.failed.connect(lambda m: self.remove.emit(item.options.app_name))
worker.signals.finished.connect(
lambda: logger.info(f"Download requeue worker finished for {item.options.app_name}")
)
QThreadPool.globalInstance().start(worker)
self.info_widget = InfoWidget(None, None, None, old_igame, parent=self)
else:
self.info_widget = InfoWidget(
@ -146,17 +148,8 @@ class QueueWidget(QFrame):
self.ui.remove_button.clicked.connect(lambda: self.remove.emit(self.item.options.app_name))
self.ui.force_button.clicked.connect(lambda: self.force.emit(self.item))
@pyqtSlot(str)
def __on_info_worker_failed(self, message: str):
logger.error(f"Failed to requeue download for {self.item.options.app_name} with error: {message}")
self.remove.emit(self.item.options.app_name)
@pyqtSlot()
def __on_info_worker_finished(self):
logger.info(f"Download requeue worker finished for {self.item.options.app_name}")
@pyqtSlot(InstallDownloadModel)
def add_info_widget(self, download: InstallDownloadModel):
def __update_info(self, download: InstallDownloadModel):
self.item.download = download
if self.item:
self.info_widget.update_information(download.game, download.igame, download.analysis, self.old_igame)

View file

@ -1,11 +1,11 @@
from logging import getLogger
from typing import List
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication
from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.shared import RareCore
logger = getLogger("TrayIcon")
@ -20,7 +20,8 @@ class TrayIcon(QSystemTrayIcon):
super(TrayIcon, self).__init__(parent=parent)
self.__parent = parent
self.rcore = RareCore.instance()
self.core = LegendaryCoreSingleton()
self.core = RareCore.instance().core()
self.settings = QSettings()
self.setIcon(QIcon(":/images/Rare.png"))
self.setVisible(True)
@ -48,8 +49,9 @@ class TrayIcon(QSystemTrayIcon):
self.setContextMenu(self.menu)
self.signals = GlobalSignalsSingleton()
self.signals = RareCore.instance().signals()
self.signals.game.uninstalled.connect(self.remove_button)
self.signals.application.notify.connect(self.notify)
self.signals.application.update_tray.connect(self.update_actions)
def last_played(self) -> List:
@ -57,6 +59,18 @@ class TrayIcon(QSystemTrayIcon):
last_played.sort(key=lambda g: g.metadata.last_played, reverse=True)
return last_played[0:5]
@pyqtSlot(str)
def notify(self, app_name: str):
if self.settings.value("notification", True, bool):
self.showMessage(
self.tr("Download finished"),
self.tr("Download finished. {} is playable now").format(
self.rcore.get_game(app_name).app_title
),
self.Information,
4000,
)
@pyqtSlot()
def update_actions(self):
for action in self.game_actions:

View file

@ -1,6 +1,7 @@
import os
import platform as pf
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import List, Optional, Callable, Dict, Tuple
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
@ -56,26 +57,44 @@ class InstallDownloadModel:
res: ConditionCheckResult
@dataclass
class InstallQueueItemModel:
options: Optional[InstallOptionsModel] = None
download: Optional[InstallDownloadModel] = None
def __init__(self, options: InstallOptionsModel, download: InstallDownloadModel = None):
self.options: Optional[InstallOptionsModel] = options
# lk: internal attribute holders
self.__download: Optional[InstallDownloadModel] = None
self.__date: Optional[datetime] = None
self.download = download
@property
def download(self) -> Optional[InstallDownloadModel]:
return self.__download
@download.setter
def download(self, download: Optional[InstallDownloadModel]):
self.__download = download
if download is not None:
self.__date = datetime.now()
@property
def expired(self) -> bool:
return datetime.now() > (self.__date + timedelta(minutes=30))
def __bool__(self):
return (self.download is not None) and (self.options is not None)
return (self.download is not None) and (self.options is not None) and (not self.expired)
@dataclass
class UninstallOptionsModel:
app_name: str
uninstall: bool = None
accepted: bool = None
keep_files: bool = None
keep_config: bool = None
def __bool__(self):
return (
bool(self.app_name)
and (self.uninstall is not None)
and (self.accepted is not None)
and (self.keep_files is not None)
and (self.keep_config is not None)
)
@ -86,9 +105,9 @@ class UninstallOptionsModel:
This model's options
:return:
Tuple of `uninstall` `keep_files` `keep_config`
Tuple of `accepted` `keep_files` `keep_config`
"""
return self.uninstall, self.keep_config, self.keep_files
return self.accepted, self.keep_config, self.keep_files
@values.setter
def values(self, values: Tuple[bool, bool, bool]):
@ -96,9 +115,9 @@ class UninstallOptionsModel:
Set this model's options
:param values:
Tuple of `uninstall` `keep_files` `keep_config`
Tuple of `accepted` `keep_files` `keep_config`
:return:
"""
self.uninstall = values[0]
self.accepted = values[0]
self.keep_files = values[1]
self.keep_config = values[2]

View file

@ -1,5 +1,6 @@
import os
import sys
from logging import getLogger
from PyQt5.QtCore import QObject, QRunnable, pyqtSignal, pyqtSlot
from legendary.lfs.eos import EOSOverlayApp
@ -12,6 +13,8 @@ from rare.lgndr.glue.exception import LgndrException
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.install import InstallDownloadModel, InstallOptionsModel
logger = getLogger("InstallInfoWorker")
class InstallInfoWorker(QRunnable):
class Signals(QObject):
@ -66,4 +69,4 @@ class InstallInfoWorker(QRunnable):
self.signals.failed.emit(ret.message)
except Exception as e:
self.signals.failed.emit(str(e))
self.signals.finished.emit()
self.signals.finished.emit()