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:
parent
90021e34f2
commit
2ef70b8eb4
|
@ -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"):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue