diff --git a/app/connections.py b/app/connections.py index f54d06f0..4cfce30b 100644 --- a/app/connections.py +++ b/app/connections.py @@ -596,7 +596,8 @@ def telnet(host, port=23, user="", password="", timeout=5): # ***************** HTTP API ******************* # class HttpAPI: - __MAX_WORKERS = 4 + _MAX_WORKERS = 4 + _TIMEOUT = 10 class Request(str, Enum): ZAP = "zap?sRef=" @@ -623,6 +624,7 @@ class HttpAPI: # EPG EPG = "epgservice?sRef=" EPG_NOW = "epgnow?bRef=" + EPG_MULTI = "epgmulti?bRef=" # Timer TIMER = "" TIMER_LIST = "timerlist" @@ -667,6 +669,7 @@ class HttpAPI: Request.VOL, Request.EPG, Request.EPG_NOW, + Request.EPG_MULTI, Request.TIMER, Request.RECORDINGS, Request.N_ZAP} @@ -678,7 +681,7 @@ class HttpAPI: def __init__(self, settings): from concurrent.futures import ThreadPoolExecutor as PoolExecutor - self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS) + self._executor = PoolExecutor(max_workers=self._MAX_WORKERS) self._settings = settings self._shutdown = False @@ -690,7 +693,7 @@ class HttpAPI: self._s_type = SettingsType.ENIGMA_2 self.init() - def send(self, req_type, ref, callback=print, ref_prefix=""): + def send(self, req_type, ref, callback=print, ref_prefix="", timeout=_TIMEOUT): if self._shutdown: return @@ -710,7 +713,7 @@ class HttpAPI: def done_callback(f): callback(f.result()) - future = self._executor.submit(self.get_response, req_type, url, data, self._s_type) + future = self._executor.submit(self.get_response, req_type, url, data, self._s_type, timeout) future.add_done_callback(done_callback) @run_task @@ -747,7 +750,7 @@ class HttpAPI: self._executor.shutdown() @staticmethod - def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2, timeout=10): + def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2, timeout=_TIMEOUT): try: with urlopen(Request(url, data=data), timeout=timeout) as f: if s_type is SettingsType.ENIGMA_2: @@ -780,7 +783,7 @@ class HttpAPI: elif req_type is HttpAPI.Request.PLAYER_LIST: return [{el.tag: el.text for el in el.iter()} for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2file")] - elif req_type is HttpAPI.Request.EPG or req_type is HttpAPI.Request.EPG_NOW: + elif req_type in (HttpAPI.Request.EPG, HttpAPI.Request.EPG_NOW, HttpAPI.Request.EPG_MULTI): return {"event_list": [{el.tag: el.text for el in el.iter()} for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]} elif req_type is HttpAPI.Request.TIMER_LIST: diff --git a/app/tools/epg.py b/app/tools/epg.py index 97fe2d66..748db96c 100644 --- a/app/tools/epg.py +++ b/app/tools/epg.py @@ -54,8 +54,8 @@ except ModuleNotFoundError: else: DETECT_ENCODING = True -EpgEvent = namedtuple("EpgEvent", ["title", "time", "desc", "event_data"]) -EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", None) # For Python3 < 3.7 +EpgEvent = namedtuple("EpgEvent", ["service_name", "title", "time", "desc", "event_data"]) +EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", "N/A", None) # For Python3 < 3.7 class Reader(metaclass=abc.ABCMeta): @@ -305,7 +305,7 @@ class XmlTvReader(Reader): start = datetime.fromtimestamp(ev.start) + offset end_time = datetime.fromtimestamp(ev.duration) + offset tm = f"{start.strftime('%H:%M')} - {end_time.strftime('%H:%M')}" - events[srv.name] = EpgEvent(ev.title, tm, ev.desc, ev) + events[srv.name] = EpgEvent(srv.name, ev.title, tm, ev.desc, ev) return events diff --git a/app/ui/epg/epg.py b/app/ui/epg/epg.py index cf64e3af..9b865174 100644 --- a/app/ui/epg/epg.py +++ b/app/ui/epg/epg.py @@ -47,7 +47,7 @@ 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.timers import TimerTool -from ..main_helper import on_popup_menu, update_entry_data +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 @@ -185,14 +185,17 @@ class EpgTool(Gtk.Box): def __init__(self, app, *args, **kwargs): super().__init__(*args, **kwargs) + self._current_bq = None self._app = app self._app.connect("fav-changed", self.on_service_changed) + self._app.connect("bouquet-changed", self.on_bouquet_changed) handlers = {"on_epg_press": self.on_epg_press, "on_timer_add": self.on_timer_add, "on_epg_filter_changed": self.on_epg_filter_changed, "on_epg_filter_toggled": self.on_epg_filter_toggled, - "on_view_query_tooltip": self.on_view_query_tooltip} + "on_view_query_tooltip": self.on_view_query_tooltip, + "on_multi_epg_toggled": self.on_multi_epg_toggled} builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}tab.glade", handlers) @@ -201,9 +204,10 @@ class EpgTool(Gtk.Box): self._filter_model = builder.get_object("epg_filter_model") self._filter_model.set_visible_func(self.epg_filter_function) self._filter_entry = builder.get_object("epg_filter_entry") + self._multi_epg_button = builder.get_object("multi_epg_button") self.pack_start(builder.get_object("epg_frame"), True, True, 0) # Custom sort function. - self._view.get_model().set_sort_func(1, self.time_sort_func, 1) + self._view.get_model().set_sort_func(2, self.time_sort_func, 2) self.show() @@ -229,7 +233,7 @@ class EpgTool(Gtk.Box): def add_timers_list(self, paths): ref_str = "timeraddbyeventid?sRef={}&eventid={}&justplay=0" - refs = [ref_str.format(ev.get("e2eventservicereference", ""), ev.get("e2eventid", "")) for ev in paths] + refs = [ref_str.format(quote(ev.get("e2eventservicereference", "")), ev.get("e2eventid", "")) for ev in paths] gen = self.write_timers_list(refs) GLib.idle_add(lambda: next(gen, False)) @@ -251,18 +255,26 @@ class EpgTool(Gtk.Box): self.on_timer_add() def on_service_changed(self, app, ref): - self._app.wait_dialog.show() - self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data) + if app.page is Page.EPG: + if self._multi_epg_button.get_active(): + ref += ":" + path = next((r.path for r in self._model if r[-1].get("e2eventservicereference", None) == ref), None) + scroll_to(path, self._view) if path else None + else: + self._app.wait_dialog.show() + self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data) @run_idle def update_epg_data(self, epg): self._model.clear() - list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", [])))) + list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", []) + if e.get("e2eventid", "").isdigit()))) self._app.wait_dialog.hide() @staticmethod def get_event(event, show_day=True): t_str = f"{'%a, ' if show_day else ''}%x, %H:%M" + s_name = event.get("e2eventservicename", "") title = event.get("e2eventtitle", "") or "" desc = event.get("e2eventdescription", "") or "" desc = desc.strip() @@ -272,7 +284,7 @@ class EpgTool(Gtk.Box): end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0"))) ev_time = f"{start_time.strftime(t_str)} - {end_time.strftime('%H:%M')}" - return EpgEvent(title, ev_time, desc, event) + return EpgEvent(s_name, title, ev_time, desc, event) def on_epg_filter_changed(self, entry): self._filter_model.refilter() @@ -283,12 +295,12 @@ class EpgTool(Gtk.Box): def epg_filter_function(self, model, itr, data): txt = self._filter_entry.get_text().upper() - return next((s for s in model.get(itr, 0, 1, 2) if txt in s.upper()), False) + return next((s for s in model.get(itr, 0, 1, 2, 3) if txt in s.upper()), False) def time_sort_func(self, model, iter1, iter2, column): """ Custom sort function for time column. """ - event1 = model.get_value(iter1, 3) - event2 = model.get_value(iter2, 3) + event1 = model.get_value(iter1, 4) + event2 = model.get_value(iter2, 4) return int(event1.get("e2eventstart", "0")) - int(event2.get("e2eventstart", "0")) @@ -308,6 +320,29 @@ class EpgTool(Gtk.Box): return True + def on_multi_epg_toggled(self, button): + self._model.clear() + if button.get_active(): + self.get_multi_epg() + + def on_bouquet_changed(self, app, bq): + self._current_bq = bq + if app.page is Page.EPG and self._multi_epg_button.get_active(): + self.get_multi_epg() + + def get_multi_epg(self): + if not self._current_bq: + return + + self._app.wait_dialog.show() + bq = self._app.current_bouquet_files.get(self._current_bq, None) + api = self._app.http_api + + if bq and api: + tm = datetime.now().timestamp() + req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"&time={tm}') + api.send(HttpAPI.Request.EPG_MULTI, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_epg_data, timeout=15) + class EpgDialog: diff --git a/app/ui/epg/settings.glade b/app/ui/epg/settings.glade index 7e13bba1..90d1fefe 100644 --- a/app/ui/epg/settings.glade +++ b/app/ui/epg/settings.glade @@ -75,7 +75,7 @@ Author: Dmitriy Yefremov expand - WebIf + Receiver True True False diff --git a/app/ui/epg/tab.glade b/app/ui/epg/tab.glade index cd56a8c0..bfefedfd 100644 --- a/app/ui/epg/tab.glade +++ b/app/ui/epg/tab.glade @@ -35,6 +35,8 @@ Author: Dmitriy Yefremov + + @@ -74,6 +76,23 @@ Author: Dmitriy Yefremov 5 5 5 + + + True + False + EPG source + 0 + 0 + + Receiver + + + + False + True + 0 + + True @@ -93,7 +112,7 @@ Author: Dmitriy Yefremov False True - 0 + 1 @@ -116,7 +135,21 @@ Author: Dmitriy Yefremov False True - 1 + 2 + + + + + Multi EPG + True + False + True + + + + False + True + 3 @@ -225,81 +258,113 @@ Author: Dmitriy Yefremov - + + stack True - True - in + False - + True True - epg_sort_model - True - True - both - 3 - - - - - multiple - - + in - - True - 170 - 50 - Title - 0.49000000953674316 - 0 - - - 5 + + True + True + epg_sort_model + True + True + both + 3 + + + + + multiple - - 0 - - - - - - True - 210 - 50 - Time - 0.49000000953674316 - 1 - - 5 - 0.49000000953674316 + + False + True + 100 + 40 + Service + 0.49000000953674316 + 0 + + + + 5 + + + 0 + + - - 1 - - - - - - 100 - 50 - Description - True - 0.49000000953674316 - - end + + True + 170 + 50 + Title + 0.49000000953674316 + 1 + + + 5 + + + 1 + + + + + + + True + 210 + 50 + Time + 0.49000000953674316 + 2 + + + 5 + 0.49000000953674316 + + + 2 + + + + + + + 100 + 50 + Description + True + 0.49000000953674316 + 3 + + + end + + + 3 + + - - 2 - + + epg + EPG +