import os import time from gettext import gettext as _ from gi.repository import GObject, Gtk, Pango from lutris.gui.dialogs import display_error from lutris.util.downloader import Downloader from lutris.util.jobs import schedule_repeating_at_idle from lutris.util.log import logger from lutris.util.strings import gtk_safe, human_size # Same reason as Downloader get_time = time.monotonic class DownloadCollectionProgressBox(Gtk.Box): """Progress bar used to monitor a collection of files download.""" max_retries = 3 __gsignals__ = { "complete": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), "cancel": (GObject.SignalFlags.RUN_LAST, None, ()), "error": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), } def __init__(self, file_collection, cancelable=True, downloader=None): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.downloader = downloader self.is_complete = False self._file_queue = file_collection.files_list.copy() self._file_download = None # file being downloaded self.title = file_collection.human_url self.num_files_downloaded = 0 self.num_files_to_download = file_collection.num_files self.num_retries = 0 self.full_size = file_collection.full_size self.current_size = 0 self.time_left = "00:00:00" self.time_left_check_time = 0 self.last_size = 0 self.avg_speed = 0 self.speed_list = [] top_box = Gtk.Box() self.main_label = Gtk.Label(self.title) self.main_label.set_alignment(0, 0) self.main_label.set_property("wrap", True) self.main_label.set_margin_bottom(10) self.main_label.set_selectable(True) self.main_label.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE) top_box.pack_start(self.main_label, False, False, 0) self.cancel_button = Gtk.Button.new_with_mnemonic(_("_Cancel")) self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_cancel_clicked) if not cancelable: self.cancel_button.set_sensitive(False) top_box.pack_end(self.cancel_button, False, False, 0) self.pack_start(top_box, True, True, 0) self.file_name_label = Gtk.Label() self.file_name_label.set_alignment(0, 0) self.file_name_label.set_property("wrap", True) self.file_name_label.set_margin_bottom(10) self.file_name_label.set_selectable(True) self.file_name_label.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE) self.pack_start(self.file_name_label, True, True, 0) progress_box = Gtk.Box() self.progressbar = Gtk.ProgressBar() self.progressbar.set_margin_top(5) self.progressbar.set_margin_bottom(5) self.progressbar.set_margin_right(10) progress_box.pack_start(self.progressbar, True, True, 0) self.pack_start(progress_box, False, False, 0) self.progress_label = Gtk.Label() self.progress_label.set_alignment(0, 0) self.pack_start(self.progress_label, True, True, 0) self.show_all() self.cancel_button.hide() def update_download_file_label(self, file_name): """Update file label to file being downloaded""" self.file_name_label.set_text(file_name) def get_next_file_from_queue(self): """Returns the file to download; if there isn't one this will pull the next one from the queue and return that. If there are none there, this returns None.""" if not self._file_download: if not self._file_queue: return None self._file_download = self._file_queue.pop() self.num_retries = 0 return self._file_download def start(self) -> None: """Start downloading a file.""" file = self.get_next_file_from_queue() if not file: self.cancel_button.set_sensitive(False) self.is_complete = True self.emit("complete", {}) return # Check if the file already exists in the cache and skip this download then if os.path.exists(file.dest_file): logger.info("File exists, skipping download: '%s'", file.dest_file) self.num_files_downloaded += 1 self.current_size += os.path.getsize(file.dest_file) self._file_download = None self.start() return # Use a temporary file to avoid problems with partially downloaded files tmp_path = file.dest_file + ".tmp" file.tmp_file = tmp_path if os.path.exists(tmp_path): os.remove(tmp_path) self.update_download_file_label(file.filename) if not self.downloader: try: self.downloader = Downloader(file.url, file.tmp_file, referer=file.referer, overwrite=True) except RuntimeError as ex: display_error(ex, parent=self.get_toplevel()) self.emit("cancel") return schedule_repeating_at_idle(self._progress, interval_seconds=0.5) self.cancel_button.show() self.cancel_button.set_sensitive(True) if not self.downloader.state == self.downloader.DOWNLOADING: self.downloader.start() def set_retry_button(self): """Transform the cancel button into a retry button""" self.cancel_button.set_label(_("Retry")) self.cancel_button.disconnect(self.cancel_cb_id) self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_retry_clicked) self.cancel_button.set_sensitive(True) def on_retry_clicked(self, button): """Retry current download.""" logger.debug("Retrying download") button.set_label(_("Cancel")) button.disconnect(self.cancel_cb_id) self.cancel_cb_id = button.connect("clicked", self.on_cancel_clicked) self.downloader.reset() self.start() def on_cancel_clicked(self, _widget=None): """Cancel the current download.""" logger.debug("Download cancel requested") if self.downloader: self.downloader.cancel() self.cancel_button.set_sensitive(False) self.emit("cancel") def _progress(self) -> bool: """Show download progress of current file.""" if self.downloader.state in [self.downloader.CANCELLED, self.downloader.ERROR]: self.progressbar.set_fraction(0) if self.downloader.state == self.downloader.CANCELLED: self._set_text(_("Download interrupted")) self.emit("cancel") else: if self.num_retries > self.max_retries: self._set_text(str(self.downloader.error)[:80]) self.emit("error", self.downloader.error) return False self.num_retries += 1 if self.downloader: self.downloader.reset() self.start() return False downloaded_size = self.current_size + self.downloader.downloaded_size progress = 0 if self.full_size > 0: progress = min(downloaded_size / self.full_size, 1) self.progressbar.set_fraction(progress) self.update_speed_and_time() megabytes = 1024 * 1024 progress_text = _("{downloaded} / {size} ({speed:0.2f}MB/s), {time} remaining").format( downloaded=human_size(downloaded_size), size=human_size(self.full_size), speed=float(self.avg_speed) / megabytes, time=self.time_left, ) self._set_text(progress_text) if self.downloader.state == self.downloader.COMPLETED: self.num_files_downloaded += 1 self.current_size += self.downloader.downloaded_size os.rename(self._file_download.tmp_file, self._file_download.dest_file) # set file to None to get next one self._file_download = None self.downloader = None # start the downloader to a new file or finish self.start() return False return True def update_speed_and_time(self): """Update time left and average speed.""" elapsed_time = get_time() - self.time_left_check_time if elapsed_time < 1: # Minimum delay return if not self.downloader: self.time_left = "???" return downloaded_size = self.current_size + self.downloader.downloaded_size elapsed_size = downloaded_size - self.last_size self.last_size = downloaded_size speed = elapsed_size / elapsed_time # last 20 speeds if len(self.speed_list) >= 20: self.speed_list.pop(0) self.speed_list.append(speed) self.avg_speed = sum(self.speed_list) / len(self.speed_list) if self.avg_speed == 0: self.time_left = "???" return average_time_left = (self.full_size - downloaded_size) / self.avg_speed minutes, seconds = divmod(average_time_left, 60) hours, minutes = divmod(minutes, 60) self.time_left_check_time = get_time() self.time_left = "%d:%02d:%02d" % (hours, minutes, seconds) def _set_text(self, text): markup = "{}".format(gtk_safe(text)) self.progress_label.set_markup(markup)