From 11d185255807d15b87e0377532e3f9f1562c543a Mon Sep 17 00:00:00 2001 From: egon984 Date: Sun, 7 Sep 2025 12:34:39 +0200 Subject: [PATCH 01/22] EXPERIMENTAL: EPG support first commit --- usr/lib/hypnotix/hypnotix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 4885ac5..df7621f 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1272,6 +1272,8 @@ def set_provider_type(self, type_id): visible_widgets.append(self.path_entry) visible_widgets.append(self.path_label) visible_widgets.append(self.browse_button) + visible_widgets.append(self.epg_entry) + visible_widgets.append(self.epg_label) elif type_id == PROVIDER_TYPE_XTREAM: visible_widgets.append(self.url_entry) visible_widgets.append(self.url_label) From 09bdf084ce4d673695c1f1ce5a58d65a75fba7eb Mon Sep 17 00:00:00 2001 From: egon984 Date: Sun, 7 Sep 2025 18:35:46 +0200 Subject: [PATCH 02/22] EXPERIMENTAL: implemented support for EPG press G to show info about currently playing TV show --- usr/lib/hypnotix/hypnotix.py | 42 ++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index df7621f..fee9f8b 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -8,8 +8,13 @@ import traceback import warnings import subprocess +import tempfile +import requests +import gzip +import xml.etree.ElementTree as xmlET from functools import partial from pathlib import Path +from datetime import datetime, date, timedelta, timezone # Force X11 on a Wayland session if "WAYLAND_DISPLAY" in os.environ: @@ -452,6 +457,7 @@ def add_badge(self, word, box, added_words): print(e) def show_groups(self, widget, content_type): + self.load_epg() self.content_type = content_type self.navigate_to("categories_page") for child in self.categories_flowbox.get_children(): @@ -1449,6 +1455,18 @@ def close(w, res): def on_menu_quit(self, widget): self.application.quit() + + def load_epg(self): + #self.status_label.show() + #self.status_label.set_text("Loading EPG...") + if (self.active_provider.epg != ""): + response = requests.get(self.active_provider.epg) + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(response.content) + temp_file_path = tmp_file.name + with gzip.open(temp_file_path, 'rb') as f: + ungzip = f.read().decode('utf-8') + self.epg = xmlET.fromstring(ungzip) def on_key_press_event(self, widget, event): # Get any active, but not pressed modifiers, like CapsLock and NumLock @@ -1459,8 +1477,28 @@ def on_key_press_event(self, widget, event): # Bool of Control or Shift modifier states ctrl = modifier == Gdk.ModifierType.CONTROL_MASK shift = modifier == Gdk.ModifierType.SHIFT_MASK - - if ctrl and event.keyval == Gdk.KEY_r: + + def chan_match(chan1, chan2): + chan1 = chan1.lower().replace(" ","") + chan1 = ''.join(filter(str.isalnum, chan1)) + chan2 = chan2.lower().replace(" ","") + chan2 = ''.join(filter(str.isalnum, chan2)) + return (chan1 in chan2 or chan2 in chan1) + + if event.keyval == Gdk.KEY_g: + dateFormat = "%Y%m%d%H%M%S" + timeFormat = "%H:%M" + hoursOffset = 0 - int(datetime.now().astimezone().utcoffset().total_seconds() / 3600) + targetDatetime = datetime.now() + timedelta(hours=hoursOffset) + channelEPG = [p for p in self.epg.findall("programme") if chan_match(p.attrib["channel"], self.active_channel.name)] + onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"> + try: + onairTime = (datetime.strptime(onair.attrib["start"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) + " - " +> + onairText = onair.find("title").text + "\n" + onairTime + except: + onairText = "(no info)" + self.mpv.command("show-text", onairText, 3000) + elif ctrl and event.keyval == Gdk.KEY_r: self.reload(page=None, refresh=True) elif ctrl and event.keyval == Gdk.KEY_f: if self.search_button.get_active(): From 5c676205f93cdbd00955457f45a9895cc9377a25 Mon Sep 17 00:00:00 2001 From: egon984 Date: Mon, 8 Sep 2025 11:32:07 +0200 Subject: [PATCH 03/22] bugfix: don't treat G as a shortcut when a text field is focused --- usr/lib/hypnotix/hypnotix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index fee9f8b..787140b 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1485,7 +1485,7 @@ def chan_match(chan1, chan2): chan2 = ''.join(filter(str.isalnum, chan2)) return (chan1 in chan2 or chan2 in chan1) - if event.keyval == Gdk.KEY_g: + if event.keyval == Gdk.KEY_g and not isinstance(widget.get_focus(), Gtk.Entry): dateFormat = "%Y%m%d%H%M%S" timeFormat = "%H:%M" hoursOffset = 0 - int(datetime.now().astimezone().utcoffset().total_seconds() / 3600) From b4e19ec17dd495243d389b2bf8ecf386055a5948 Mon Sep 17 00:00:00 2001 From: egon984 Date: Mon, 8 Sep 2025 12:53:30 +0200 Subject: [PATCH 04/22] improved matching channel name against EPG --- usr/lib/hypnotix/hypnotix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 787140b..2cd71d9 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1483,6 +1483,8 @@ def chan_match(chan1, chan2): chan1 = ''.join(filter(str.isalnum, chan1)) chan2 = chan2.lower().replace(" ","") chan2 = ''.join(filter(str.isalnum, chan2)) + chan1 = re.sub(r'^\d+', '', chan1) + chan2 = re.sub(r'^\d+', '', chan2) return (chan1 in chan2 or chan2 in chan1) if event.keyval == Gdk.KEY_g and not isinstance(widget.get_focus(), Gtk.Entry): From 13d20b84366fc8f56bf93108518413349711e7c5 Mon Sep 17 00:00:00 2001 From: egon984 Date: Mon, 8 Sep 2025 14:10:57 +0200 Subject: [PATCH 05/22] added support for multiple EPG urls (space separated) increased chances of finding channels, support for EPGs from multiple countries at the same time, and so on --- usr/lib/hypnotix/hypnotix.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 2cd71d9..395fec2 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1459,14 +1459,21 @@ def on_menu_quit(self, widget): def load_epg(self): #self.status_label.show() #self.status_label.set_text("Loading EPG...") + self.epg = None if (self.active_provider.epg != ""): - response = requests.get(self.active_provider.epg) - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - tmp_file.write(response.content) - temp_file_path = tmp_file.name - with gzip.open(temp_file_path, 'rb') as f: - ungzip = f.read().decode('utf-8') - self.epg = xmlET.fromstring(ungzip) + for e in self.active_provider.epg.split(): + response = requests.get(e) + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(response.content) + temp_file_path = tmp_file.name + with gzip.open(temp_file_path, 'rb') as f: + ungzip = f.read().decode('utf-8') + epg = xmlET.fromstring(ungzip) + if self.epg is None: + self.epg = epg + else: + for item in epg: + self.epg.append(item) def on_key_press_event(self, widget, event): # Get any active, but not pressed modifiers, like CapsLock and NumLock From 1d067b6728c1a1d9c221370fa69503c4ad4b5f81 Mon Sep 17 00:00:00 2001 From: egon984 Date: Mon, 8 Sep 2025 14:13:35 +0200 Subject: [PATCH 06/22] bugfix: imported re --- usr/lib/hypnotix/hypnotix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 395fec2..4dfd55c 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -11,6 +11,7 @@ import tempfile import requests import gzip +import re import xml.etree.ElementTree as xmlET from functools import partial from pathlib import Path From dce50761df2f9ce7b026e76905d4498b52f82522 Mon Sep 17 00:00:00 2001 From: egon984 Date: Mon, 8 Sep 2025 16:01:15 +0200 Subject: [PATCH 07/22] minor tweaks try/except for expired EPG urls or malformed gzip/xml files; cleanup mpv OSD when changing channel --- usr/lib/hypnotix/hypnotix.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 4dfd55c..1ea1033 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -882,6 +882,7 @@ def on_next_channel(self): @async_function def play_async(self, channel): if self.mpv is not None: + self.mpv.command("show-text", "", 1) self.mpv.stop() print("CHANNEL: '%s' (%s)" % (channel.name, channel.url)) if channel is not None and channel.url is not None: @@ -1463,18 +1464,21 @@ def load_epg(self): self.epg = None if (self.active_provider.epg != ""): for e in self.active_provider.epg.split(): - response = requests.get(e) - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - tmp_file.write(response.content) - temp_file_path = tmp_file.name - with gzip.open(temp_file_path, 'rb') as f: - ungzip = f.read().decode('utf-8') - epg = xmlET.fromstring(ungzip) - if self.epg is None: - self.epg = epg - else: - for item in epg: - self.epg.append(item) + try: + response = requests.get(e) + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(response.content) + temp_file_path = tmp_file.name + with gzip.open(temp_file_path, 'rb') as f: + ungzip = f.read().decode('utf-8') + epg = xmlET.fromstring(ungzip) + if self.epg is None: + self.epg = epg + else: + for item in epg: + self.epg.append(item) + except: + pass def on_key_press_event(self, widget, event): # Get any active, but not pressed modifiers, like CapsLock and NumLock From 1809e4582432af51fe937ba334cb6d04b75c28f5 Mon Sep 17 00:00:00 2001 From: egon984 Date: Mon, 8 Sep 2025 16:15:33 +0200 Subject: [PATCH 08/22] bugfix fixed a couple of typos --- usr/lib/hypnotix/hypnotix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 1ea1033..8f652e3 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1504,10 +1504,10 @@ def chan_match(chan1, chan2): timeFormat = "%H:%M" hoursOffset = 0 - int(datetime.now().astimezone().utcoffset().total_seconds() / 3600) targetDatetime = datetime.now() + timedelta(hours=hoursOffset) - channelEPG = [p for p in self.epg.findall("programme") if chan_match(p.attrib["channel"], self.active_channel.name)] - onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"> try: - onairTime = (datetime.strptime(onair.attrib["start"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) + " - " +> + channelEPG = [p for p in self.epg.findall("programme") if chan_match(p.attrib["channel"], self.active_channel.name)] + onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"].split()[0], dateFormat) >= targetDatetime][0] + onairTime = (datetime.strptime(onair.attrib["start"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) + " - " + (datetime.strptime(onair.attrib["stop"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) onairText = onair.find("title").text + "\n" + onairTime except: onairText = "(no info)" From 9eb9fa40e03fd4f5480d29ec2b9e4419d12493f6 Mon Sep 17 00:00:00 2001 From: egon984 Date: Tue, 9 Sep 2025 11:45:44 +0200 Subject: [PATCH 09/22] added caching the cache is refreshed at least once a day and dramatically speeds up loading the EPG --- usr/lib/hypnotix/hypnotix.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 8f652e3..ea4d3a3 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -12,6 +12,8 @@ import requests import gzip import re +import base64 +import pickle import xml.etree.ElementTree as xmlET from functools import partial from pathlib import Path @@ -1462,7 +1464,12 @@ def load_epg(self): #self.status_label.show() #self.status_label.set_text("Loading EPG...") self.epg = None - if (self.active_provider.epg != ""): + cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + self.active_provider.epg).encode()).decode() + cached_epg_path = os.path.join(tempfile.gettempdir(), cached_epg_name) + if os.path.exists(cached_epg_path): + with open(cached_epg_path, 'rb') as f: + self.epg = pickle.load(f) + elif (self.active_provider.epg != ""): for e in self.active_provider.epg.split(): try: response = requests.get(e) @@ -1479,7 +1486,9 @@ def load_epg(self): self.epg.append(item) except: pass - + with open(cached_epg_path, 'wb') as f: + pickle.dump(self.epg, f) + def on_key_press_event(self, widget, event): # Get any active, but not pressed modifiers, like CapsLock and NumLock persistant_modifiers = Gtk.accelerator_get_default_mod_mask() From 0a6b1b1b802a83039b5064b42c42c9d1d3276e8f Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:13:01 +0200 Subject: [PATCH 10/22] increased EPG timeout --- usr/lib/hypnotix/hypnotix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index ea4d3a3..387e7ec 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1520,7 +1520,7 @@ def chan_match(chan1, chan2): onairText = onair.find("title").text + "\n" + onairTime except: onairText = "(no info)" - self.mpv.command("show-text", onairText, 3000) + self.mpv.command("show-text", onairText, 4500) elif ctrl and event.keyval == Gdk.KEY_r: self.reload(page=None, refresh=True) elif ctrl and event.keyval == Gdk.KEY_f: From 7bab95adcd862e3ca0c1aa51289eb14612f8d48f Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:05:08 +0200 Subject: [PATCH 11/22] EPG for favorites when a channel is added to favorites, its provider's EPG urls are appended to an internal list (favorites' EPG); this list is loaded when the favorites button is clicked --- usr/lib/hypnotix/hypnotix.py | 13 ++++++++----- .../glib-2.0/schemas/org.x.hypnotix.gschema.xml | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 387e7ec..41f78a8 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -460,7 +460,7 @@ def add_badge(self, word, box, added_words): print(e) def show_groups(self, widget, content_type): - self.load_epg() + self.load_epg(self.active_provider.epg) self.content_type = content_type self.navigate_to("categories_page") for child in self.categories_flowbox.get_children(): @@ -523,6 +523,7 @@ def on_category_button_clicked(self, widget, group): self.show_vod(self.active_provider.series) def show_favorites(self, widget=None): + self.load_epg(self.settings.get_string("favorites-epg")) self.content_type = TV_GROUP channels = [] for line in self.favorite_data: @@ -861,6 +862,8 @@ def on_favorite_button_toggled(self, widget): if widget.get_active() and data not in self.favorite_data: print (f"Adding {name} to favorites") self.favorite_data.append(data) + current_epg = self.active_provider.epg + self.settings.set_string("favorites-epg", (self.settings.get_string("favorites-epg").replace(current_epg, "") + " " + current_epg)) elif widget.get_active() == False and data in self.favorite_data: print (f"Removing {name} from favorites") self.favorite_data.remove(data) @@ -1460,17 +1463,17 @@ def close(w, res): def on_menu_quit(self, widget): self.application.quit() - def load_epg(self): + def load_epg(self, epg_urls): #self.status_label.show() #self.status_label.set_text("Loading EPG...") self.epg = None - cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + self.active_provider.epg).encode()).decode() + cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + epg_urls).encode()).decode() cached_epg_path = os.path.join(tempfile.gettempdir(), cached_epg_name) if os.path.exists(cached_epg_path): with open(cached_epg_path, 'rb') as f: self.epg = pickle.load(f) - elif (self.active_provider.epg != ""): - for e in self.active_provider.epg.split(): + elif (epg_urls != ""): + for e in epg_urls.split(): try: response = requests.get(e) with tempfile.NamedTemporaryFile(delete=False) as tmp_file: diff --git a/usr/share/glib-2.0/schemas/org.x.hypnotix.gschema.xml b/usr/share/glib-2.0/schemas/org.x.hypnotix.gschema.xml index 0ffd7b3..5e67bef 100644 --- a/usr/share/glib-2.0/schemas/org.x.hypnotix.gschema.xml +++ b/usr/share/glib-2.0/schemas/org.x.hypnotix.gschema.xml @@ -21,6 +21,11 @@ Provider selected by default + + "" + EPG urls for favorites + + ['Free-TV:::url:::https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8:::::::::'] Format: name:::type:::url(or path):::username:::password:::epg From 334f755716d671488683800e5e80fb82cd4d9e3f Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:13:43 +0200 Subject: [PATCH 12/22] support for multiple entries (details in description) if you set multiple EPG urls you can easily get multiple entries for the current TV show. Press G multiple times to show them all (in the order you choose when setting EPG urls). TV shows with abnormal duration are automatically skipped. --- usr/lib/hypnotix/hypnotix.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 41f78a8..fb254eb 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -552,6 +552,7 @@ def show_channels(self, channels, favorites=False): self.download_channel_logos(logos_to_refresh) else: self.sidebar.hide() + self.epg_counter = {"channel": "", "idx": 0} def show_vod(self, items): logos_to_refresh = [] @@ -1516,14 +1517,21 @@ def chan_match(chan1, chan2): timeFormat = "%H:%M" hoursOffset = 0 - int(datetime.now().astimezone().utcoffset().total_seconds() / 3600) targetDatetime = datetime.now() + timedelta(hours=hoursOffset) + if self.active_channel.name != self.epg_counter["channel"]: + self.epg_counter = {"channel": self.active_channel.name, "idx": 0 } try: channelEPG = [p for p in self.epg.findall("programme") if chan_match(p.attrib["channel"], self.active_channel.name)] - onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"].split()[0], dateFormat) >= targetDatetime][0] + onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"].split()[0], dateFormat) >= targetDatetime and (int(p.attrib["stop"][8:10]) - int(p.attrib["start"][8:10]) < 5)] + osd_counter = "" + if len(onair) > 1: + osd_counter = " [" + str(self.epg_counter["idx"] + 1) + "/" + str(len(onair)) + "]" + self.epg_counter["idx"] = (self.epg_counter["idx"] + 1) % len(onair) + onair = onair[::-1][self.epg_counter["idx"]] onairTime = (datetime.strptime(onair.attrib["start"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) + " - " + (datetime.strptime(onair.attrib["stop"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) - onairText = onair.find("title").text + "\n" + onairTime + onairText = onair.attrib["channel"] + osd_counter + "\n" + onair.find("title").text + "\n" + onairTime except: onairText = "(no info)" - self.mpv.command("show-text", onairText, 4500) + self.mpv.command("show-text", onairText, 6000) elif ctrl and event.keyval == Gdk.KEY_r: self.reload(page=None, refresh=True) elif ctrl and event.keyval == Gdk.KEY_f: From eccd4872c2ffcf422c0330da14462f2299bfc554 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:57:51 +0200 Subject: [PATCH 13/22] bugfix --- usr/lib/hypnotix/hypnotix.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index fb254eb..fe37645 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -552,7 +552,7 @@ def show_channels(self, channels, favorites=False): self.download_channel_logos(logos_to_refresh) else: self.sidebar.hide() - self.epg_counter = {"channel": "", "idx": 0} + self.epg_counter = {"channel": "", "idx": -1} def show_vod(self, items): logos_to_refresh = [] @@ -888,6 +888,7 @@ def on_next_channel(self): @async_function def play_async(self, channel): if self.mpv is not None: + self.epg_counter["idx"] = -1 self.mpv.command("show-text", "", 1) self.mpv.stop() print("CHANNEL: '%s' (%s)" % (channel.name, channel.url)) @@ -1518,15 +1519,15 @@ def chan_match(chan1, chan2): hoursOffset = 0 - int(datetime.now().astimezone().utcoffset().total_seconds() / 3600) targetDatetime = datetime.now() + timedelta(hours=hoursOffset) if self.active_channel.name != self.epg_counter["channel"]: - self.epg_counter = {"channel": self.active_channel.name, "idx": 0 } + self.epg_counter = {"channel": self.active_channel.name, "idx": -1 } try: channelEPG = [p for p in self.epg.findall("programme") if chan_match(p.attrib["channel"], self.active_channel.name)] onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"].split()[0], dateFormat) >= targetDatetime and (int(p.attrib["stop"][8:10]) - int(p.attrib["start"][8:10]) < 5)] osd_counter = "" if len(onair) > 1: - osd_counter = " [" + str(self.epg_counter["idx"] + 1) + "/" + str(len(onair)) + "]" self.epg_counter["idx"] = (self.epg_counter["idx"] + 1) % len(onair) - onair = onair[::-1][self.epg_counter["idx"]] + osd_counter = " [" + str(self.epg_counter["idx"] + 1) + "/" + str(len(onair)) + "]" + onair = onair[self.epg_counter["idx"]] onairTime = (datetime.strptime(onair.attrib["start"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) + " - " + (datetime.strptime(onair.attrib["stop"].split()[0], dateFormat) + timedelta(hours=(0 - hoursOffset))).strftime(timeFormat) onairText = onair.attrib["channel"] + osd_counter + "\n" + onair.find("title").text + "\n" + onairTime except: From 22de4f3039794ea6b0d983c82cf1774a162b4a57 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:37:44 +0200 Subject: [PATCH 14/22] little improvements here and there --- usr/lib/hypnotix/hypnotix.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index fe37645..c7ab37e 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1469,12 +1469,15 @@ def load_epg(self, epg_urls): #self.status_label.show() #self.status_label.set_text("Loading EPG...") self.epg = None - cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + epg_urls).encode()).decode() - cached_epg_path = os.path.join(tempfile.gettempdir(), cached_epg_name) + def get_cached_epg_path(urls): + cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + urls).encode()).decode() + return os.path.join(tempfile.gettempdir(), cached_epg_name) + cached_epg_path = get_cached_epg_path(epg_urls) if os.path.exists(cached_epg_path): with open(cached_epg_path, 'rb') as f: self.epg = pickle.load(f) elif (epg_urls != ""): + urls = "" for e in epg_urls.split(): try: response = requests.get(e) @@ -1489,9 +1492,10 @@ def load_epg(self, epg_urls): else: for item in epg: self.epg.append(item) + urls += (" " + e) except: pass - with open(cached_epg_path, 'wb') as f: + with open(get_cached_epg_path(urls), 'wb') as f: pickle.dump(self.epg, f) def on_key_press_event(self, widget, event): @@ -1505,12 +1509,18 @@ def on_key_press_event(self, widget, event): shift = modifier == Gdk.ModifierType.SHIFT_MASK def chan_match(chan1, chan2): + # discard digits at the beginning + chan1 = re.sub(r'^\d+', '', chan1) + chan2 = re.sub(r'^\d+', '', chan2) + # discard useless words + regex = r"\b(4K|HD)\b" + chan1 = re.sub(regex, "", chan1, flags=re.IGNORECASE) + chan2 = re.sub(regex, "", chan2, flags=re.IGNORECASE) + # normalize chan1 = chan1.lower().replace(" ","") chan1 = ''.join(filter(str.isalnum, chan1)) chan2 = chan2.lower().replace(" ","") chan2 = ''.join(filter(str.isalnum, chan2)) - chan1 = re.sub(r'^\d+', '', chan1) - chan2 = re.sub(r'^\d+', '', chan2) return (chan1 in chan2 or chan2 in chan1) if event.keyval == Gdk.KEY_g and not isinstance(widget.get_focus(), Gtk.Entry): From 6e1295187e1bcb39c412e294dc7d7f902858dd32 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:02:54 +0200 Subject: [PATCH 15/22] better way of calculating timestamps --- usr/lib/hypnotix/hypnotix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index c7ab37e..9d10b6f 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1527,12 +1527,12 @@ def chan_match(chan1, chan2): dateFormat = "%Y%m%d%H%M%S" timeFormat = "%H:%M" hoursOffset = 0 - int(datetime.now().astimezone().utcoffset().total_seconds() / 3600) - targetDatetime = datetime.now() + timedelta(hours=hoursOffset) + targetDatetime = (datetime.now() + timedelta(hours=hoursOffset)).timestamp() if self.active_channel.name != self.epg_counter["channel"]: self.epg_counter = {"channel": self.active_channel.name, "idx": -1 } try: channelEPG = [p for p in self.epg.findall("programme") if chan_match(p.attrib["channel"], self.active_channel.name)] - onair = [p for p in channelEPG if datetime.strptime(p.attrib["start"].split()[0], dateFormat) <= targetDatetime and datetime.strptime(p.attrib["stop"].split()[0], dateFormat) >= targetDatetime and (int(p.attrib["stop"][8:10]) - int(p.attrib["start"][8:10]) < 5)] + onair = [p for p in channelEPG if (tstart := datetime.strptime(p.attrib["start"].split()[0], dateFormat).timestamp()) <= targetDatetime and (tstop := datetime.strptime(p.attrib["stop"].split()[0], dateFormat).timestamp()) >= targetDatetime and (tstop - tstart) < (3600 * 5)] osd_counter = "" if len(onair) > 1: self.epg_counter["idx"] = (self.epg_counter["idx"] + 1) % len(onair) From ccaea7509ca6a2577c4644d5fdc3071f791b5f69 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Sat, 13 Sep 2025 10:08:21 +0200 Subject: [PATCH 16/22] bugfix --- usr/lib/hypnotix/hypnotix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 9d10b6f..153e458 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1470,7 +1470,7 @@ def load_epg(self, epg_urls): #self.status_label.set_text("Loading EPG...") self.epg = None def get_cached_epg_path(urls): - cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + urls).encode()).decode() + cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + urls.replace(" ","")).encode()).decode() return os.path.join(tempfile.gettempdir(), cached_epg_name) cached_epg_path = get_cached_epg_path(epg_urls) if os.path.exists(cached_epg_path): @@ -1492,7 +1492,7 @@ def get_cached_epg_path(urls): else: for item in epg: self.epg.append(item) - urls += (" " + e) + urls += e except: pass with open(get_cached_epg_path(urls), 'wb') as f: From 77588150939fe5b1a12a840184bcb5d6bb69c11c Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:46:38 +0200 Subject: [PATCH 17/22] hide EPG pressing ESC --- usr/lib/hypnotix/hypnotix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 153e458..36fdc0a 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1558,6 +1558,7 @@ def chan_match(chan1, chan2): elif event.keyval == Gdk.KEY_F7: self.borderless_mode() elif event.keyval == Gdk.KEY_Escape: + self.mpv.command("show-text", "", 1) self.normal_mode() elif event.keyval == Gdk.KEY_BackSpace and not ctrl and type(widget.get_focus()) != gi.repository.Gtk.SearchEntry: self.normal_mode() From a457923b684b9936867b95eb15e61127c4413434 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:03:08 +0200 Subject: [PATCH 18/22] improved ESC keypress --- usr/lib/hypnotix/hypnotix.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 36fdc0a..053dd0c 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -553,6 +553,7 @@ def show_channels(self, channels, favorites=False): else: self.sidebar.hide() self.epg_counter = {"channel": "", "idx": -1} + self.epg_timestamp = 0 def show_vod(self, items): logos_to_refresh = [] @@ -1543,6 +1544,7 @@ def chan_match(chan1, chan2): except: onairText = "(no info)" self.mpv.command("show-text", onairText, 6000) + self.epg_timestamp = datetime.now().timestamp() elif ctrl and event.keyval == Gdk.KEY_r: self.reload(page=None, refresh=True) elif ctrl and event.keyval == Gdk.KEY_f: @@ -1558,8 +1560,11 @@ def chan_match(chan1, chan2): elif event.keyval == Gdk.KEY_F7: self.borderless_mode() elif event.keyval == Gdk.KEY_Escape: - self.mpv.command("show-text", "", 1) - self.normal_mode() + if ((datetime.now().timestamp() - self.epg_timestamp) <= 6): + self.mpv.command("show-text", "", 1) + self.epg_timestamp = 0 + else: + self.normal_mode() elif event.keyval == Gdk.KEY_BackSpace and not ctrl and type(widget.get_focus()) != gi.repository.Gtk.SearchEntry: self.normal_mode() self.on_go_back_button() From ba50f0864229906b975eb49ca0833b0480c74d63 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:30:44 +0200 Subject: [PATCH 19/22] G and ESC use the same variable epg_duration --- usr/lib/hypnotix/hypnotix.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 053dd0c..e276f1a 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1509,6 +1509,8 @@ def on_key_press_event(self, widget, event): ctrl = modifier == Gdk.ModifierType.CONTROL_MASK shift = modifier == Gdk.ModifierType.SHIFT_MASK + epg_duration = 6 # seconds + def chan_match(chan1, chan2): # discard digits at the beginning chan1 = re.sub(r'^\d+', '', chan1) @@ -1543,7 +1545,7 @@ def chan_match(chan1, chan2): onairText = onair.attrib["channel"] + osd_counter + "\n" + onair.find("title").text + "\n" + onairTime except: onairText = "(no info)" - self.mpv.command("show-text", onairText, 6000) + self.mpv.command("show-text", onairText, (epg_duration * 1000)) self.epg_timestamp = datetime.now().timestamp() elif ctrl and event.keyval == Gdk.KEY_r: self.reload(page=None, refresh=True) @@ -1560,7 +1562,7 @@ def chan_match(chan1, chan2): elif event.keyval == Gdk.KEY_F7: self.borderless_mode() elif event.keyval == Gdk.KEY_Escape: - if ((datetime.now().timestamp() - self.epg_timestamp) <= 6): + if ((datetime.now().timestamp() - self.epg_timestamp) <= epg_duration): self.mpv.command("show-text", "", 1) self.epg_timestamp = 0 else: From 1f7ad9e49e45188b37c0136c3c30d81e2f446f8a Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Sat, 20 Sep 2025 08:40:55 +0200 Subject: [PATCH 20/22] async EPG loading cache not needed anymore --- usr/lib/hypnotix/hypnotix.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index e276f1a..884c5d2 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1466,10 +1466,12 @@ def close(w, res): def on_menu_quit(self, widget): self.application.quit() + @async_function def load_epg(self, epg_urls): - #self.status_label.show() - #self.status_label.set_text("Loading EPG...") + self.status_label.set_text("Loading EPG...") + self.status_label.show() self.epg = None + """ def get_cached_epg_path(urls): cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + urls.replace(" ","")).encode()).decode() return os.path.join(tempfile.gettempdir(), cached_epg_name) @@ -1478,6 +1480,8 @@ def get_cached_epg_path(urls): with open(cached_epg_path, 'rb') as f: self.epg = pickle.load(f) elif (epg_urls != ""): + """ + if (epg_urls != ""): urls = "" for e in epg_urls.split(): try: @@ -1496,8 +1500,9 @@ def get_cached_epg_path(urls): urls += e except: pass - with open(get_cached_epg_path(urls), 'wb') as f: - pickle.dump(self.epg, f) + #with open(get_cached_epg_path(urls), 'wb') as f: + # pickle.dump(self.epg, f) + self.status_label.hide() def on_key_press_event(self, widget, event): # Get any active, but not pressed modifiers, like CapsLock and NumLock From 623ba9b49ba29ed7965a5a93b3d3164f2f4a8447 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Sun, 21 Sep 2025 09:09:36 +0200 Subject: [PATCH 21/22] caching re-enabled unfortunately some EPG services allow a limited number of downloads per day, so caching is necessary --- usr/lib/hypnotix/hypnotix.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 884c5d2..8579e15 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1471,7 +1471,6 @@ def load_epg(self, epg_urls): self.status_label.set_text("Loading EPG...") self.status_label.show() self.epg = None - """ def get_cached_epg_path(urls): cached_epg_name = base64.urlsafe_b64encode((date.today().isoformat() + urls.replace(" ","")).encode()).decode() return os.path.join(tempfile.gettempdir(), cached_epg_name) @@ -1480,8 +1479,6 @@ def get_cached_epg_path(urls): with open(cached_epg_path, 'rb') as f: self.epg = pickle.load(f) elif (epg_urls != ""): - """ - if (epg_urls != ""): urls = "" for e in epg_urls.split(): try: @@ -1500,8 +1497,8 @@ def get_cached_epg_path(urls): urls += e except: pass - #with open(get_cached_epg_path(urls), 'wb') as f: - # pickle.dump(self.epg, f) + with open(get_cached_epg_path(urls), 'wb') as f: + pickle.dump(self.epg, f) self.status_label.hide() def on_key_press_event(self, widget, event): From 7f605c07bfadb297fa4f447506763f44ca2ab539 Mon Sep 17 00:00:00 2001 From: Frederic Bontemps <35567681+egon984@users.noreply.github.com> Date: Sun, 21 Sep 2025 09:49:36 +0200 Subject: [PATCH 22/22] gzipped cache huge EPGs load dramatically faster --- usr/lib/hypnotix/hypnotix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 8579e15..4dbfb0c 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1476,7 +1476,7 @@ def get_cached_epg_path(urls): return os.path.join(tempfile.gettempdir(), cached_epg_name) cached_epg_path = get_cached_epg_path(epg_urls) if os.path.exists(cached_epg_path): - with open(cached_epg_path, 'rb') as f: + with gzip.open(cached_epg_path, 'rb') as f: self.epg = pickle.load(f) elif (epg_urls != ""): urls = "" @@ -1497,7 +1497,7 @@ def get_cached_epg_path(urls): urls += e except: pass - with open(get_cached_epg_path(urls), 'wb') as f: + with gzip.open(get_cached_epg_path(urls), 'wb') as f: pickle.dump(self.epg, f) self.status_label.hide()