From a69127f0ccf6b6bbeca74a07a44548b55ca5ca25 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Fri, 12 Aug 2022 09:14:13 +0300 Subject: [PATCH] background tasks prototype --- app/tools/epg.py | 1 - app/ui/epg/epg.py | 17 +++++++-- app/ui/main.glade | 31 +++++++++++++++++ app/ui/main.py | 32 ++++++++++++++++- app/ui/style.css | 4 +++ app/ui/tasks.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 app/ui/tasks.py diff --git a/app/tools/epg.py b/app/tools/epg.py index 748db96c..15d0a114 100644 --- a/app/tools/epg.py +++ b/app/tools/epg.py @@ -236,7 +236,6 @@ class XmlTvReader(Reader): self._url = url self._ids = {} - @run_task def download(self, clb=None): """ Downloads an XMLTV file. """ res = urlparse(self._url) diff --git a/app/ui/epg/epg.py b/app/ui/epg/epg.py index d8dbd400..63411fda 100644 --- a/app/ui/epg/epg.py +++ b/app/ui/epg/epg.py @@ -46,6 +46,7 @@ from app.eparser.ecommons import BouquetService, BqServiceType from app.settings import SEP, EpgSource from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder +from app.ui.tasks import BGTaskWidget from app.ui.timers import TimerTool from ..main_helper import on_popup_menu, update_entry_data, scroll_to from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page @@ -61,11 +62,14 @@ class EpgCache(dict): super().__init__() self._current_bq = None self._reader = None + self._canceled = False + self._settings = app.app_settings self._src = self._settings.epg_source self._app = app self._app.connect("bouquet-changed", self.on_bouquet_changed) self._app.connect("profile-changed", self.on_profile_changed) + self._app.connect("task-canceled", self.on_xml_load_cancel) self.init() @@ -84,9 +88,15 @@ class EpgCache(dict): # Difference calculation between the current time and file modification. dif = datetime.now() - datetime.fromtimestamp(os.path.getmtime(gz_file)) # We will update daily. -> Temporarily!!! - self._reader.download(process_data) if dif.days > 0 else process_data() + if dif.days > 0 and not self._canceled: + task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data,) + self._app.emit("add-background-task", task) + else: + process_data() else: - self._reader.download(process_data) + if not self._canceled: + task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, ) + self._app.emit("add-background-task", task) elif self._src is EpgSource.DAT: self._reader = EPG.DatReader(f"{self._settings.profile_data_path}epg{os.sep}epg.dat") self._reader.download() @@ -99,6 +109,9 @@ class EpgCache(dict): def on_profile_changed(self, app, p): self.clear() + def on_xml_load_cancel(self, app, widget): + self._canceled = True + def update_epg_data(self): if self._src is EpgSource.HTTP: api = self._app.http_api diff --git a/app/ui/main.glade b/app/ui/main.glade index b2ff86e0..ca556bbe 100644 --- a/app/ui/main.glade +++ b/app/ui/main.glade @@ -4328,6 +4328,37 @@ Author: Dmitriy Yefremov 0 + + + True + False + center + 5 + + + + + + + + + + + + + + + + + + + False + True + 3 + + False diff --git a/app/ui/main.py b/app/ui/main.py index 4838015a..72c3960c 100644 --- a/app/ui/main.py +++ b/app/ui/main.py @@ -88,6 +88,8 @@ class Application(Gtk.Application): _TV_TYPES = ("TV", "TV (HD)", "TV (UHD)", "TV (H264)") + BG_TASK_LIMIT = 5 + # Dynamically active elements depending on the selected view _SERVICE_ELEMENTS = ("services_to_fav_end_move_popup_item", "services_to_fav_move_popup_item", "services_create_bouquet_popup_item", "services_copy_popup_item", "services_edit_popup_item", @@ -270,7 +272,7 @@ class Application(Gtk.Application): # Current page. self._page = Page.INFO self._fav_pages = {Page.SERVICES, Page.PICONS, Page.EPG, Page.TIMERS} - self._download_pages = {Page.INFO, Page.SERVICES, Page.SATELLITE, Page.PICONS} + self._download_pages = {Page.INFO, Page.SERVICES, Page.SATELLITE, Page.PICONS, Page.RECORDINGS} # Signals. GObject.signal_new("profile-changed", self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) @@ -314,6 +316,14 @@ class Application(Gtk.Application): GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) GObject.signal_new("data-save-as", self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("add-background-task", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("task-done", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("task-cancel", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("task-canceled", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) builder = get_builder(UI_RESOURCES_PATH + "main.glade", handlers) self._main_window = builder.get_object("main_window") @@ -365,6 +375,7 @@ class Application(Gtk.Application): self._signal_level_bar.bind_property("visible", builder.get_object("record_button"), "visible") self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4) self._receiver_info_box.bind_property("visible", self._signal_box, "visible") + self._task_box = builder.get_object("task_box") # Alternatives self._alt_view = builder.get_object("alt_tree_view") self._alt_model = builder.get_object("alt_list_store") @@ -472,6 +483,10 @@ class Application(Gtk.Application): # Data save. self.connect("data-save", self.on_data_save) self.connect("data-save-as", self.on_data_save_as) + # Background tasks. + self.connect("add-background-task", self.on_bg_task_add) + self.connect("task-done", self.on_task_done) + self.connect("task-cancel", self.on_task_cancel) # Header bar. profile_box = builder.get_object("profile_combo_box") toolbar_box = builder.get_object("toolbar_main_box") @@ -1965,6 +1980,21 @@ class Application(Gtk.Application): if page is Page.SERVICES or page is Page.INFO: self.on_upload_data() + def on_bg_task_add(self, app, task): + if len(self._task_box) <= self.BG_TASK_LIMIT: + self._task_box.add(task) + else: + self.show_error_message("Task limit (> 5) exceeded!") + + def on_task_done(self, app, task): + self._task_box.remove(task) + task.destroy() + + def on_task_cancel(self, app, task): + if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.OK: + task.cancel() + self.on_task_done(app, task) + @run_task def on_download_data(self, download_type=DownloadType.ALL): backup, backup_src, data_path = self._settings.backup_before_downloading, None, None diff --git a/app/ui/style.css b/app/ui/style.css index 182d0404..078e1410 100644 --- a/app/ui/style.css +++ b/app/ui/style.css @@ -14,6 +14,10 @@ margin: 1px; } +#task-button { + padding: 0; +} + #stack-switch-button { padding-top: 0; padding-bottom: 0; diff --git a/app/ui/tasks.py b/app/ui/tasks.py new file mode 100644 index 00000000..65105ae6 --- /dev/null +++ b/app/ui/tasks.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018-2022 Dmitriy Yefremov +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Author: Dmitriy Yefremov +# + + +from .uicommons import Gtk, GLib + + +class BGTaskWidget(Gtk.Box): + """ Widget for displaying and running background tasks. """ + + TASK_LIMIT = 1 + + def __init__(self, app, text, target, *args): + super().__init__(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.CENTER) + self._app = app + + self._label = Gtk.Label(text) + self.pack_start(self._label, False, False, 0) + + self._spinner = Gtk.Spinner(active=True) + self.pack_start(self._spinner, False, False, 0) + + close_button = Gtk.Button.new_from_icon_name("gtk-close", Gtk.IconSize.MENU) + close_button.set_relief(Gtk.ReliefStyle.NONE) + close_button.set_valign(Gtk.Align.CENTER) + close_button.set_tooltip_text("Cancel") + close_button.set_name("task-button") + close_button.connect("clicked", lambda b: self._app.emit("task-cancel", self)) + self.pack_start(close_button, False, False, 0) + + self.show_all() + + # Just prototype. -> It may not work properly! + # TODO: Different options need to be tested. Possibly with normal threads. + from concurrent.futures import ThreadPoolExecutor + + self._executor = ThreadPoolExecutor(max_workers=self.TASK_LIMIT) + future = self._executor.submit(target, *args) + future.add_done_callback(lambda f: GLib.idle_add(self._app.emit, "task-done", self)) + + @property + def text(self): + return self._label.get_text() + + @text.setter + def text(self, value): + self._label.set_text(value) + + @property + def tooltip(self): + return self.get_tooltip_text() + + @tooltip.setter + def tooltip(self, value): + self.set_tooltip_text(value) + + def cancel(self): + self._executor.shutdown(wait=False) + self._app.emit("task-canceled", None) + + +if __name__ == '__main__': + pass