background tasks prototype

This commit is contained in:
DYefremov
2022-08-12 09:14:13 +03:00
parent 858b2ae2d6
commit a69127f0cc
6 changed files with 168 additions and 4 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -4328,6 +4328,37 @@ Author: Dmitriy Yefremov
<property name="position">0</property>
</packing>
</child>
<child type="center">
<object class="GtkBox" id="task_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="spacing">5</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkImage" id="http_status_image">
<property name="can_focus">False</property>

View File

@@ -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

View File

@@ -14,6 +14,10 @@
margin: 1px;
}
#task-button {
padding: 0;
}
#stack-switch-button {
padding-top: 0;
padding-bottom: 0;

87
app/ui/tasks.py Normal file
View File

@@ -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