From 0460c67b4afe1e3ed00f3a2453ac61364ddc3aa8 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:42:32 +0000 Subject: [PATCH 01/18] Fix version check and py3 decode bugs in package helpers - check_version: the changelog branch was nested inside the version branch and never ran; versions were compared as strings so 2.9.x would never see a 2.10.x update; an empty response returned a bare None instead of a 3-tuple. Add version_tuple() and compare numerically. - checkGZIP: gzip-encoded responses crashed on Python 3 because StringIO was fed bytes; use BytesIO. - b64decoder: the padding fixup appended bytes to a str (TypeError on Python 3). - fetch_url: PY3 was undefined when running on Python 2. --- .gitignore | 3 ++ .../Extensions/LinuxsatPanel/__init__.py | 44 ++++++++++++------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 3c03de0..10bd174 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/icons/link-.png usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/icons2/link-.png usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin~.py +__pycache__/ +*.pyc +*.pyo diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py index 0914e2d..2d2b152 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py @@ -117,6 +117,17 @@ def setup_timer(callback_method): return timer +def version_tuple(version): + """'2.10.1' -> (2, 10, 1) for a numeric comparison""" + parts = [] + for part in str(version).strip().split("."): + try: + parts.append(int(part)) + except (TypeError, ValueError): + parts.append(0) + return tuple(parts) or (0,) + + def check_version(currversion, installer_url, AgentRequest): """Controllo versione con gestione avanzata formato numerico""" print("[Version Check] Starting...") @@ -148,24 +159,26 @@ def check_version(currversion, installer_url, AgentRequest): remote_changelog = "No changelog available" for line in lines: + line = line.strip() if line.startswith("version"): parts = line.split("=") if len(parts) > 1: - remote_version = parts[1].strip().strip("'") - if line.startswith("changelog"): - parts = line.split("=") - if len(parts) > 1: - try: - remote_changelog = parts[1].strip().strip( - "'") - except BaseException: - remote_changelog = "No changelog available" - break + remote_version = parts[1].strip().strip( + "'").strip('"') + elif line.startswith("changelog"): + parts = line.split("=") + if len(parts) > 1: + remote_changelog = parts[1].strip().strip( + "'").strip('"') + break new_version = remote_version or "Unknown" new_changelog = remote_changelog or "No changelog available" - return new_version, new_changelog, currversion < remote_version + return new_version, new_changelog, version_tuple( + currversion) < version_tuple(remote_version) + + return None, None, False except Exception as e: print("Error while checking version:", e) @@ -294,6 +307,7 @@ def fetch_url(url, retries=3, initial_timeout=5): else: from urllib2 import (urlopen) from urllib2 import URLError + PY3 = False timeout = initial_timeout for i in range(retries): try: @@ -322,7 +336,7 @@ def fetch_url(url, retries=3, initial_timeout=5): def checkGZIP(url): url = url - from io import StringIO + from io import BytesIO import gzip import requests import sys @@ -337,7 +351,7 @@ def checkGZIP(url): try: response = urlopen(request, timeout=10) if response.info().get('Content-Encoding') == 'gzip': - buffer = StringIO(response.read()) + buffer = BytesIO(response.read()) deflatedContent = gzip.GzipFile(fileobj=buffer) if sys.version_info[0] == 3: return deflatedContent.read().decode('utf-8') @@ -371,9 +385,9 @@ def b64decoder(s): print('Invalid base64 string: {}'.format(s)) return "" elif padding == 2: - s += b'==' + s += '==' elif padding == 3: - s += b'=' + s += '=' else: return "" output = base64.b64decode(s) From dfd8b9d665bf622c748f1eed1401f460ee5a93af Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:43:08 +0000 Subject: [PATCH 02/18] Fix confirmation dialogs running the action on No The Lcn, LcnXX and Checkskin prompts used 'if answer is None / else', so answering No fell into the else branch and ran the scan or check anyway. Guard with 'elif answer:'. LcnXX also reopened its prompt with self.Lcn as callback, so the answer ran the wrong handler. Checkskin additionally connected its eTimer to the return value of check_module_skin() - the function was already executed and returns None, so the timer fired into a TypeError; run the check directly and drop the timer. --- .../Extensions/LinuxsatPanel/plugin.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 3755f1c..416d965 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -3340,7 +3340,7 @@ def Lcn(self, answer=None): MessageBox, _("Do you want to Order LCN Bouquet?"), MessageBox.TYPE_YESNO) - else: + elif answer: print("Starting LCN scan...") try: from .LCNScanner.Terrestrial import PluginSetup @@ -3351,11 +3351,11 @@ def Lcn(self, answer=None): def LcnXX(self, answer=None): if answer is None: self.session.openWithCallback( - self.Lcn, + self.LcnXX, MessageBox, _("Do you want to Order LCN Bouquet?"), MessageBox.TYPE_YESNO) - else: + elif answer: print("Starting LCN scan...") try: lcn_scanner_instance = LCNScanner() @@ -3389,15 +3389,9 @@ def Checkskin(self, answer=None): MessageBox, _("[Checkskin] This operation checks if the skin has its components (is not sure)..\nDo you really want to continue?"), MessageBox.TYPE_YESNO) - else: + elif answer: from .addons import checkskin - check = checkskin.check_module_skin() - self.timer = eTimer() - try: - self.timer_conn = self.timer.timeout.connect(check) - except BaseException: - self.timer.callback.append(check) - self.timer.start(100, True) + checkskin.check_module_skin() self.session.openWithCallback( self._view_log, MessageBox, @@ -4282,7 +4276,7 @@ def Lcn(self, answer=None): _("Do you want to Order LCN Bouquet?"), MessageBox.TYPE_YESNO ) - else: + elif answer: print("Starting LCN scan...") try: from .LCNScanner.Terrestrial import PluginSetup @@ -4293,11 +4287,11 @@ def Lcn(self, answer=None): def LcnXX(self, answer=None): if answer is None: self.session.openWithCallback( - self.Lcn, + self.LcnXX, MessageBox, _("Do you want to Order LCN Bouquet?"), MessageBox.TYPE_YESNO) - else: + elif answer: print("Starting LCN scan...") try: lcn_scanner_instance = LCNScanner() From 7aa13e0bd7708566fb6bf145c70b60acd8b106a6 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:43:28 +0000 Subject: [PATCH 03/18] Fix free C-line fetcher (getcl) - A missing comma in the generic pattern list concatenated two regexes into one broken pattern that could never match. - A dead server made make_request return None and '"x" in None' raise a confusing TypeError; fail cleanly with a clear message instead. - The pattern loop kept scanning after a successful match, so a page matching several patterns appended duplicate server entries to the config; stop after the first hit. --- .../python/Plugins/Extensions/LinuxsatPanel/plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 416d965..2105e82 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -3733,6 +3733,8 @@ def getcl(self, config_type): dat = RequestUrl() print("Request Server url is:", dat) data = make_request(dat) + if not data: + raise ValueError("No data received from server") if PY3: data = six.ensure_str(data) @@ -3764,7 +3766,7 @@ def getcl(self, config_type): regex_patterns = [ r'">C:\s+([\w.-]+)\s+(\d+)\s+(\w+)\s+([\w.-]+)\s*', r'c:\s+([\w.-]+)\s+(\d+)\s+(\w+)\s+([\w.-]+)\s*\s*C:\s+([\w.-]+)\s+(\d+)\s+(\w+)\s+([\w.-]+)\s*' + r'cline">\s*C:\s+([\w.-]+)\s+(\d+)\s+(\w+)\s+([\w.-]+)\s*', r'

C:\s+([\w.-]+)\s+(\d+)\s+(\w+)\s+([\w.-]+)\s*', r'"C: (.*?) (.*?) (.*?) (.*?)"', r'"c: (.*?) (.*?) (.*?) (.*?)"', @@ -3827,6 +3829,8 @@ def getcl(self, config_type): timeout=6 ) + break + except Exception as e: # Error handling self.session.open( From 955a5e8672c91d602d7bd52513d8d968238e1eac Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:43:50 +0000 Subject: [PATCH 04/18] Fix remove name, grid page count and hanging downloads - removenow: package filenames without an underscore lost their last character (ipk[:-1]), so opkg remove targeted a nonexistent package; strip the extension instead. - The grid page count used npics // 20 + 1, creating a phantom empty page whenever an item count is an exact multiple of 20 (all six grid screens share the formula). - requests.get in runScriptWithConsole and retfile had no timeout and could freeze the GUI thread forever on a stalled connection. --- .../Plugins/Extensions/LinuxsatPanel/plugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 2105e82..7cb6655 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -879,7 +879,7 @@ def __init__(self, session): i += 1 self.npics = len(self.names) - self.npage = int(round(self.npics // self.PIXMAPS_PER_PAGE)) + 1 + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 self.index = 0 self.maxentry = len(menu_list) - 1 @@ -1307,7 +1307,7 @@ def __init__(self, session, name): self.npics = len(self.names) # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = int(round(self.npics // self.PIXMAPS_PER_PAGE)) + 1 + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) self.index = 0 self.maxentry = len(menu_list) - 1 self.ipage = 1 @@ -1614,7 +1614,7 @@ def __init__(self, session, name): self.npics = len(self.names) # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = int(round(self.npics // self.PIXMAPS_PER_PAGE)) + 1 + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) self.index = 0 self.maxentry = len(menu_list) - 1 self.ipage = 1 @@ -2188,7 +2188,7 @@ def __init__(self, session, name): self.npics = len(self.names) # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = int(round(self.npics // self.PIXMAPS_PER_PAGE)) + 1 + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) self.index = 0 self.maxentry = len(menu_list) - 1 self.ipage = 1 @@ -2693,7 +2693,7 @@ def __init__(self, session, name): self.npics = len(self.names) # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = int(round(self.npics // self.PIXMAPS_PER_PAGE)) + 1 + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) self.index = 0 self.maxentry = len(menu_list) - 1 self.ipage = 1 @@ -3327,7 +3327,7 @@ def __init__(self, session, name): self.npics = len(self.names) # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = int(round(self.npics // self.PIXMAPS_PER_PAGE)) + 1 + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) self.index = 0 self.maxentry = len(menu_list) - 1 self.ipage = 1 @@ -3662,7 +3662,7 @@ def runScriptWithConsole(self, confirmed): script_path = "/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/Fcl.sh" url = "https://raw.githubusercontent.com/Belfagor2005/LinuxsatPanel/refs/heads/main/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/Fcl.sh" try: - response = requests.get(url) + response = requests.get(url, timeout=15) response.raise_for_status() with io.open(script_path, "w", encoding="utf-8") as file: file.write(response.text) @@ -4136,7 +4136,7 @@ def okClicked(self, choice, name, url): def retfile(self, dest): import requests - response = requests.get(self.url) + response = requests.get(self.url, timeout=30) if response.status_code == 200: with open(dest, "wb") as f: f.write(response.content) @@ -4399,7 +4399,7 @@ def removenow(self, answer=False): if ".zip" in ipk: return n2 = ipk.find("_", 0) - self.iname = ipk[:n2] + self.iname = ipk[:n2] if n2 != -1 else ipk.rsplit(".", 1)[0] cmd = "opkg remove '" + self.iname + "'" title = (_("Removing %s") % self.iname) self.session.open(lsConsole, _(title), cmdlist=[cmd]) From d70334b27a4b04fcadefa47728f1abb227f55324 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:44:03 +0000 Subject: [PATCH 05/18] Compare versions numerically in LSinfo update check check_vers compared version strings lexicographically, so an installed 2.10.x would be offered a downgrade to 2.9.x and 2.9.x would never see a 2.10.x update. Use the shared version_tuple helper. --- .../enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 7cb6655..23845f5 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -232,6 +232,7 @@ RequestUrl, make_request, refreshPlugins, + version_tuple, xmlurl, HALIGN, __version__, @@ -4538,8 +4539,7 @@ def check_vers(self): self.new_changelog = str(self.new_changelog) if not isinstance(self.new_version, str): self.new_version = str(self.new_version) - # if float(__version__) < float(remote_version): - if __version__ < remote_version: + if version_tuple(__version__) < version_tuple(remote_version): self.Update = True self.show_update_message() From c29b0314fadb9fe37a6f29375e109ef4f781a482 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:44:26 +0000 Subject: [PATCH 06/18] Fix missing labels in STB info output 'Label: %s' % x if x else 'Unknown' binds as ('Label: %s' % x) if x else 'Unknown', so any missing value printed a bare 'Unknown' line without its field label. Format through a small helper instead, and show the real boolean for the VTi/DMM image flags (False previously displayed as 'Unknown'). --- .../LinuxsatPanel/addons/stbinfo.py | 65 +++++++------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py index 4fc7a3e..9aca745 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py @@ -74,6 +74,9 @@ def __init__(self): self.pip = self.get_public_ip() def to_string(self): + def fmt(label, value): + return '%s: %s' % (label, value if value else 'Unknown') + lines = [] try: lines.append( @@ -81,52 +84,28 @@ def to_string(self): ('OpenWebif' if self.boxinfo else 'proc')) lines.append('\n') lines.append('HW Info:') - lines.append('Vendor: %s' % - str(self.hw_vendor) if self.hw_vendor else 'Unknown') - lines.append('Model: %s' % - str(self.hw_model) if self.hw_model else 'Unknown') - lines.append('Chipset: %s' % str(self.hw_chipset) - if self.hw_chipset else 'Unknown') - lines.append('Architecture: %s' % - str(self.hw_arch) if self.hw_arch else 'Unknown') - lines.append('Local Ip: %s' % - str(self.ipub) if self.ipub else 'Unknown') - lines.append('Public IP: %s' % - str(self.pip) if self.pip else 'Unknown') - lines.append('Internet: %s' % str(self.internetline) - if self.internetline else 'Unknown') + lines.append(fmt('Vendor', self.hw_vendor)) + lines.append(fmt('Model', self.hw_model)) + lines.append(fmt('Chipset', self.hw_chipset)) + lines.append(fmt('Architecture', self.hw_arch)) + lines.append(fmt('Local Ip', self.ipub)) + lines.append(fmt('Public IP', self.pip)) + lines.append(fmt('Internet', self.internetline)) lines.append('\n') lines.append('SW Info:') - lines.append( - 'Installation ID: %s' % str( - self.installation_id) if self.installation_id else 'Unknown') - lines.append( - 'Python version: %s' % str( - self.python_version) if self.python_version else 'Unknown') - lines.append('Distro: %s' % - str(self.sw_distro) if self.sw_distro else 'Unknown') - lines.append( - 'Distro version: %s' % str( - self.sw_distro_ver) if self.sw_distro_ver else 'Unknown') - lines.append( - 'Enigma version: %s' % str( - self.sw_enigma_ver) if self.sw_enigma_ver else 'Unknown') - lines.append('OE version: %s' % - str(self.sw_oe_ver) if self.sw_oe_ver else 'Unknown') + lines.append(fmt('Installation ID', self.installation_id)) + lines.append(fmt('Python version', self.python_version)) + lines.append(fmt('Distro', self.sw_distro)) + lines.append(fmt('Distro version', self.sw_distro_ver)) + lines.append(fmt('Enigma version', self.sw_enigma_ver)) + lines.append(fmt('OE version', self.sw_oe_ver)) lines.append('\n') - lines.append( - 'Video Format: %s' % str( - self.current_format) if self.current_format else 'Unknown') - lines.append('Mount Info: %s' % - str(self.mountid) if self.mountid else 'Unknown') - lines.append('Storage Info: %s' % - str(self.storhdd) if self.storhdd else 'Unknown') - lines.append('Memory Info: %s' % - str(self.memin) if self.memin else 'Unknown') - lines.append('Is VTi image: %s' % str(self.is_vti_image) - if self.is_vti_image else 'Unknown') - lines.append('Is DMM image: %s' % str(self.is_dmm_image) - if self.is_dmm_image else 'Unknown') + lines.append(fmt('Video Format', self.current_format)) + lines.append(fmt('Mount Info', self.mountid)) + lines.append(fmt('Storage Info', self.storhdd)) + lines.append(fmt('Memory Info', self.memin)) + lines.append('Is VTi image: %s' % self.is_vti_image) + lines.append('Is DMM image: %s' % self.is_dmm_image) except Exception as e: print("Error formatting info:", e) return '\n'.join(lines) From 58ad3b7ceeadedb0cb1408ad5caac66452b33aaa Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 22:44:52 +0000 Subject: [PATCH 07/18] Fix LCNScanner bouquet restore and dedup - TransferBouquetTerrestrialFinal defined its inner RestoreTerrestrial function but never called it, so the terrestrial bouquet restore after a settings install was a no-op. - addInTVBouquets compared the bouquet's full path against bouquets.tv lines that only contain the file name, so the already-present check never matched and a duplicate reference (to a possibly different hardcoded name) was appended on every run; compare and write the actual bouquet file name. - RestoreTerrestrial checked for '#NAME' inside line.lower(), which can never match, so the bouquet was never renamed to Digitale Terrestre. - Terrestrial.py's ServiceScan hook imported itself through .LCNScanner.Terrestrial, a path that does not exist from inside the LCNScanner package; the class is defined in the same module. --- .../Extensions/LinuxsatPanel/LCNScanner/Lcn.py | 13 +++++++++---- .../LinuxsatPanel/LCNScanner/Terrestrial.py | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Lcn.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Lcn.py index 1b0dbfe..dddc9e9 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Lcn.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Lcn.py @@ -293,14 +293,17 @@ def addInTVBouquets(self): try: with open('/etc/enigma2/bouquets.tv', 'r') as f: ret = f.read().splitlines() - dttbouquet_str = Bouquet() # "FROM BOUQUET \"userbouquet.terrestrial_lcn.tv\"" + dttbouquet = Bouquet() + # bouquets.tv references the bouquet by file name, not full path + name = os.path.basename( + dttbouquet) if dttbouquet else "userbouquet.terrestrial_lcn.tv" for line in ret: - if dttbouquet_str in line: + if name in line: return with open('/etc/enigma2/bouquets.tv', 'w') as f: f.write(ret[0] + "\n") f.write( - '#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.terrestrial_lcn.tv" ORDER BY bouquet\n') + '#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "%s" ORDER BY bouquet\n' % name) for line in ret[1:]: f.write(line + "\n") except Exception as e: @@ -821,7 +824,7 @@ def find_terrestrial_bouquet(): if terrestrial_bouquet_path: with open(terrestrial_bouquet_path, 'w') as bouquet_file: for line in terrestrial_channel_list: - if '#NAME' in line.lower(): + if '#name' in line.lower(): bouquet_file.write('#NAME Digitale Terrestre\n') else: bouquet_file.write(line) @@ -830,5 +833,7 @@ def find_terrestrial_bouquet(): print("Errore durante il ripristino del bouquet:", e) return False + return RestoreTerrestrial(TerChArch) + # ===== by lululla diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Terrestrial.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Terrestrial.py index 9172a1d..bd59d2f 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Terrestrial.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/LCNScanner/Terrestrial.py @@ -336,7 +336,6 @@ def Plugins(**kwargs): def __newfunc(self, *args, **kwargs): if self["scan"].isDone() and "Terrestrial" in str(self.scanList): - from .LCNScanner.Terrestrial import TerrestrialBouquet print( "[TerrestrialBouquet] rebuilding terrestrial bouquet -", TerrestrialBouquet().rebuild() or "was successful") From 5182e15b34ccc64bb4fec5fdb3a5388233f56741 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 23:01:33 +0000 Subject: [PATCH 08/18] Remove dead translate_utils.py and duplicate sh scripts translate_utils.py is imported by nothing and cannot be imported at all: it expects DEBUG, HEADERS and SYSTEM_DIR from the package, which do not exist (leftover from the ForecaOne plugin). The translation workflow uses its own copies in update_translations.py. bissfeedautokey.sh and ipaudiopro_1.4.sh are byte-identical duplicates of Bissfeedautokey.sh and Ipaudiopro_1.4.sh; the capitalized names are the ones referenced by plugin.py. Multistalker_pro.sh and multisalker_pro1_eliesat.sh differ in content and stay untouched. --- .../LinuxsatPanel/sh/bissfeedautokey.sh | 81 --- .../LinuxsatPanel/sh/ipaudiopro_1.4.sh | 120 ---- .../LinuxsatPanel/translate_utils.py | 621 ------------------ 3 files changed, 822 deletions(-) delete mode 100644 usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/bissfeedautokey.sh delete mode 100644 usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/ipaudiopro_1.4.sh delete mode 100644 usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/translate_utils.py diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/bissfeedautokey.sh b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/bissfeedautokey.sh deleted file mode 100644 index c95491e..0000000 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/bissfeedautokey.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash -#DESCRIPTION=BissFeedAutoKey -if [ -d /usr/lib/enigma2/python/Plugins/Extensions/BissFeedAutoKey ]; then -echo "> removing package please wait..." -sleep 3s -rm -rf /usr/lib/enigma2/python/Plugins/Extensions/BissFeedAutoKey > /dev/null 2>&1 -rm -rf /usr/lib/enigma2/python/Components/Renderer/ProgressPixmap.py > /dev/null 2>&1 -rm -rf /usr/lib/enigma2/python/Components/Converter/ServiceName2.py > /dev/null 2>&1 -rm -rf /usr/lib/enigma2/python/Components/Converter/ServiceInfo2.py > /dev/null 2>&1 - -status='/var/lib/opkg/status' -package='enigma2-plugin-extensions-bissfeed-autokey' -package1='enigma2-plugin-extensions-bissfeedautokey' - -if grep -q $package $status; then -opkg remove $package -fi -if grep -q $package1 $status; then -opkg remove $package1 -fi - -echo "*******************************************" -echo "* Removed Finished *" -echo "* Uploaded By Eliesat *" -echo "*******************************************" -sleep 3s - -else - -##remove unnecessary files and folders -if [ -d "/CONTROL" ]; then -rm -r /CONTROL >/dev/null 2>&1 -fi -rm -rf /control >/dev/null 2>&1 -rm -rf /postinst >/dev/null 2>&1 -rm -rf /preinst >/dev/null 2>&1 -rm -rf /prerm >/dev/null 2>&1 -rm -rf /postrm >/dev/null 2>&1 -rm -rf /tmp/*.ipk >/dev/null 2>&1 -rm -rf /tmp/*.tar.gz >/dev/null 2>&1 - -##check install deps -## Check python -pyVersion=$(python -c"from sys import version_info; print(version_info[0])") - -##config -plugin=bissfeedautokey -version=2.8 - -if [ "$pyVersion" = 3 ]; then -url=https://gitlab.com/eliesat/extensions/-/raw/main/bissfeedautokey/bissfeedautokey-py3-2.8.tar.gz -package=/var/volatile/tmp/$plugin-py3-$version.tar.gz -else -url=https://gitlab.com/eliesat/extensions/-/raw/main/bissfeedautokey/bissfeedautokey-py2-2.8.tar.gz -package=/var/volatile/tmp/$plugin-py2-$version.tar.gz -fi - -##download & install -echo "> Downloading $plugin-$version package please wait ..." -sleep 3s - -wget -O $package --no-check-certificate $url -tar -xzf $package -C / -extract=$? -rm -rf $package >/dev/null 2>&1 - - -echo '' -if [ $extract -eq 0 ]; then -echo "> $plugin-$version package installed successfully" -echo "> Uploaded By ElieSat" -sleep 3s - -else - -echo "> $plugin-$version package installation failed" -sleep 3s -fi - -fi -exit diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/ipaudiopro_1.4.sh b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/ipaudiopro_1.4.sh deleted file mode 100644 index fdc3c54..0000000 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/sh/ipaudiopro_1.4.sh +++ /dev/null @@ -1,120 +0,0 @@ -#! /bin/bash -##DESCRIPTION=IPAUDIO -wget https://raw.githubusercontent.com/emil237/updates-enigma/main/update-all-python.sh -O - | /bin/bash - -plugin="ipaudiopro" -git_url="https://github.com/emilnabil/ipaudiopro/raw/refs/heads/main" -version="1.4" -PLUGIN_PATH="/usr/lib/enigma2/python/Plugins/Extensions/IPaudioPro" -package="enigma2-plugin-extensions-$plugin" -temp_dir="/tmp" -OPKG_DIR="/etc/opkg/" -PYTHON_VERSION=$(python --version 2>&1 | awk '{print $2}') - -if [ -z "$PYTHON_VERSION" ]; then - echo "Python is not installed or could not detect Python version." - exit 1 -fi - -if command -v apt-get > /dev/null 2>&1; then -INSTALL="apt-get install -y" -CHECK_INSTALLED="dpkg -l" -CHECK_VERSION="dpkg-query -W -f='\${Version}'" -OS='DreamOS' - -elif command -v opkg > /dev/null 2>&1; then -INSTALL="opkg install --force-reinstall --force-depends" -CHECK_INSTALLED="opkg list-installed" -CHECK_VERSION="opkg info" -OS='Opensource' - -else -echo "Unsupported OS" -exit 1 - -fi - -if [ -d "$PLUGIN_PATH" ]; then -echo "Removing existing plugin..." - -if command -v opkg > /dev/null; then -opkg remove enigma2-plugin-extensions-ipaudiopro - -elif command -v apt-get > /dev/null; then -apt-get remove enigma2-plugin-extensions-ipaudiopro -y - -fi -rm -rf "$PLUGIN_PATH" -fi - -arch=$(uname -m) - -case $PYTHON_VERSION in - -3.9.*) PYTHON='PY3'; PY_VERSION='3_9';; -3.10.*) PYTHON='PY3'; PY_VERSION='3_11';; -3.11.*) PYTHON='PY3'; PY_VERSION='3_11';; -3.12.[1-5]) PYTHON='PY3'; PY_VERSION='3_12';; -3.12.[6-9]) PYTHON='PY3'; PY_VERSION='3_12_ff7';; -3.13.*) PYTHON='PY3'; PY_VERSION='py3_13_ff7';; -2.7.*) PYTHON='PY2'; PY_VERSION='2_7';; -*) -echo "Python version not supported." -sleep 4 -exit 1 -;; - -esac - -if [ "$arch" = "mips" ]; then -package_url="https://github.com/emilnabil/ipaudiopro/raw/refs/heads/main/enigma2-plugin-extensions-ipaudiopro_${version}_mips32el_py${PY_VERSION}.ipk" -elif [ "$arch" = "armv7l" ]; then -if ls "$OPKG_DIR" | grep -q "cortexa15hf-neon-vfpv4"; then -package_url="https://github.com/emilnabil/ipaudiopro/raw/refs/heads/main/enigma2-plugin-extensions-ipaudiopro_${version}_cortexa15hf-neon-vfpv4_py${PY_VERSION}.ipk" -elif ls "$OPKG_DIR" | grep -q "cortexa9hf-neon"; then -package_url="https://github.com/emilnabil/ipaudiopro/raw/refs/heads/main/enigma2-plugin-extensions-ipaudiopro_${version}_cortexa9hf-neon_py${PY_VERSION}.ipk" -elif ls "$OPKG_DIR" | grep -q "cortexa7hf-vfp"; then -package_url="https://github.com/emilnabil/ipaudiopro/raw/refs/heads/main/enigma2-plugin-extensions-ipaudiopro_${version}_cortexa7hf-vfp_py${PY_VERSION}.ipk" -elif ls "$OPKG_DIR" | grep -q "armv7ahf-neon"; then -package_url="https://github.com/emilnabil/ipaudiopro/raw/refs/heads/main/enigma2-plugin-extensions-ipaudiopro_${version}_armv7ahf-neon_py${PY_VERSION}.ipk" -else -echo "Unknown CPU architecture" -exit 1 -fi - -else -echo "Unsupported architecture" -exit 1 -fi - -cd /tmp || exit 1 - -wget "$package_url" -O enigma2-plugin-extensions-ipaudiopro.ipk - -$INSTALL "/tmp/enigma2-plugin-extensions-ipaudiopro.ipk" -rm -f "/tmp/enigma2-plugin-extensions-ipaudiopro.ipk" -wget -O "/usr/lib/enigma2/python/Plugins/Extensions/IPaudioPro/logo.png" "https://dreambox4u.com/emilnabil237/plugins/ipaudiopro/logo.png" -wget -O "/etc/enigma2/IPAudioPro.json" "https://dreambox4u.com/emilnabil237/plugins/ipaudiopro/IPAudioPro.json" - -echo "############################################################### -# IPAudioPro version 1.4 installed # -# Uploaded By Emil_Nabil # -###############################################################" - -sleep 3 -echo " Your Device Will RESTART Now " -sleep 2 -if grep -q "DreamOS" /etc/issue; then -if command -v systemctl > /dev/null 2>&1; then -echo "Restarting Enigma2 using systemctl..." -systemctl restart enigma2 -else -echo "Systemctl not found, restarting Enigma2 using killall..." -killall -9 enigma2 -fi -else -echo "Killing and restarting Enigma2 process..." -killall -9 enigma2 -fi - -exit 0 diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/translate_utils.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/translate_utils.py deleted file mode 100644 index e88fd0f..0000000 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/translate_utils.py +++ /dev/null @@ -1,621 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# Copyright (c) @Lululla 2026 -# Google Translate API for Foreca One Weather Plugin - -import hashlib -import json -import socket -import time -from json import JSONDecodeError, loads -from os import makedirs, remove -from os.path import dirname, exists, join - -from urllib.error import HTTPError, URLError -from urllib.parse import urlencode -from urllib.request import Request, urlopen - -from Components.config import config - -from . import DEBUG, HEADERS, SYSTEM_DIR - -# ============================================================ -# CUSTOM CONFIGURATION -# ============================================================ - -# Translation API URL (can be changed if needed) -TRANSLATE_API_URL = "https://translate.googleapis.com/translate_a/single" - -# Timeout for HTTP requests (in seconds) -REQUEST_TIMEOUT = 8 - -# Character limit for batch translation (to avoid errors) -MAX_CHARS_PER_REQUEST = 2000 - -# Local cache to avoid repetitive requests -CACHE_FILE = join(SYSTEM_DIR, "translation_cache.json") -_translation_cache = {} -_cache_hits = 0 -_cache_misses = 0 -_cache_dirty = False # flag to know if there are changes to save - -# Enable logging -ENABLE_LOGGING = True - - -# ============================================================ -# CACHE PERSISTENCE -# ============================================================ - - -def _ensure_cache_dir(): - """Create the directory for the cache file if it does not exist.""" - cache_dir = dirname(CACHE_FILE) - if not exists(cache_dir): - try: - makedirs(cache_dir) - except Exception as e: - _log(f"Error creating cache directory: {e}") - - -def load_cache_from_disk(): - """Load the cache from the JSON file at startup.""" - global _translation_cache - _ensure_cache_dir() - if exists(CACHE_FILE): - try: - with open(CACHE_FILE, 'r', encoding='utf-8') as f: - _translation_cache = json.load(f) - _log(f"Cache loaded from disk ({len(_translation_cache)} entries)") - except Exception as e: - _log(f"Error loading cache: {e}") - _translation_cache = {} - else: - _translation_cache = {} - - -def save_cache_to_disk(): - """Save the cache to disk if there are changes.""" - global _cache_dirty - if not _cache_dirty: - return - _ensure_cache_dir() - try: - with open(CACHE_FILE, 'w', encoding='utf-8') as f: - json.dump(_translation_cache, f, ensure_ascii=False, indent=2) - _log(f"Cache saved to disk ({len(_translation_cache)} entries)") - _cache_dirty = False - except Exception as e: - _log(f"Error saving cache: {e}") - - -# ============================================================ -# UTILITY FUNCTIONS -# ============================================================ - - -def _log(message): - """Custom logging""" - if ENABLE_LOGGING and DEBUG: - timestamp = time.time() - print(f"[Foreca-1-Translate][{timestamp:.2f}] {message}") - - -def _get_system_language(): - """Get system language in short format""" - try: - lang = config.misc.language.value - return lang.split('_')[0].lower() - except Exception: - lang = config.osd.language.value - return lang.split('_')[0].lower() - -# print("System Language:", _get_system_language()) - - -def _to_unicode(text): - """Convert any input into a Unicode string.""" - if text is None: - return "" - - if isinstance(text, str): - return text - - if isinstance(text, bytes): - try: - return text.decode("utf-8", errors="ignore") - except Exception: - return str(text, errors="ignore") - - try: - return str(text) - except Exception: - return "" - - -def _clean_whitespace(text): - text_unicode = _to_unicode(text) - while " " in text_unicode: - text_unicode = text_unicode.replace(" ", " ") - return text_unicode.strip() - - -# ============================================================ -# ARABIC LANGUAGE DETECTION -# ============================================================ - - -def _is_arabic_char(char): - """Check if a character is Arabic""" - try: - code = ord(char) - # Unicode ranges for Arabic characters - return ( - 0x0600 <= code <= 0x06FF or - 0x0750 <= code <= 0x077F or - 0x08A0 <= code <= 0x08FF or - 0xFB50 <= code <= 0xFDFF or - 0xFE70 <= code <= 0xFEFF - ) - except Exception: - return False - - -def _is_text_arabic(text): - """ - Determines whether a text is predominantly Arabic. - Returns True if more than 60% of alphabetic characters are Arabic. - """ - text_unicode = _to_unicode(text) - if not text_unicode: - return False - - total_letters = 0 - arabic_letters = 0 - - for char in text_unicode: - # Consider only alphabetic characters (exclude spaces, numbers, - # punctuation) - if char.isalpha(): - total_letters += 1 - if _is_arabic_char(char): - arabic_letters += 1 - - # If there are no letters, it's not Arabic - if total_letters == 0: - return False - - # Calculate percentage - arabic_ratio = float(arabic_letters) / float(total_letters) - - # Threshold to consider the text Arabic (60%) - return arabic_ratio >= 0.6 - - -# ============================================================ -# CACHE AND PERFORMANCE -# ============================================================ - - -def _get_cache_key(text, target_lang): - """Generate a unique cache key using MD5 (stable across runs)""" - # Use MD5 because it is fast and deterministic - key_string = f"{target_lang}:{text}".encode('utf-8') - return hashlib.md5(key_string).hexdigest() - - -def _cache_translation(text, target_lang, translated): - """Store a translation in the cache and save immediately to disk.""" - global _cache_dirty - cache_key = _get_cache_key(text, target_lang) - _translation_cache[cache_key] = translated - _cache_dirty = True - save_cache_to_disk() - return translated - - -def _get_cached_translation(text, target_lang): - """Retrieve a translation from the cache""" - global _cache_hits, _cache_misses - cache_key = _get_cache_key(text, target_lang) - - if cache_key in _translation_cache: - _cache_hits += 1 - return _translation_cache[cache_key] - - _cache_misses += 1 - return None - - -def get_cache_stats(): - """Return cache statistics""" - return { - 'hits': _cache_hits, - 'misses': _cache_misses, - 'size': len(_translation_cache), - 'hit_rate': _cache_hits / max(1, _cache_hits + _cache_misses) - } - - -def clear_cache(): - """Clear the translation cache and delete the file""" - global _cache_hits, _cache_misses, _cache_dirty - _translation_cache.clear() - _cache_hits = 0 - _cache_misses = 0 - _cache_dirty = False - if exists(CACHE_FILE): - try: - remove(CACHE_FILE) - except Exception as e: - _log(f"Error deleting cache file: {e}") - _log("Cache cleared") - - -# ============================================================ -# MAIN TRANSLATION FUNCTION -# ============================================================ - -def translate_text(text, target_lang=None, use_cache=True): - """ - Translates text using the Google Translate API. - - Args: - text (str): Text to translate - target_lang (str): Target language (e.g. 'it', 'en', 'de') - If None, uses the system language - use_cache (bool): Whether to use the local cache - - Returns: - str: Translated text or original text in case of error - """ - start_time = time.time() - _log(f"Target language: '{target_lang}'") - # Input validation - if not text: - return "" - - # Convert to Unicode - text_unicode = _to_unicode(text) - - # Use system language if not specified - if target_lang is None: - target_lang = _get_system_language() - - # Normalize language (ensure lowercase) - target_lang = target_lang.lower() - - # If the text is already Arabic, do not translate it - if _is_text_arabic(text_unicode): - _log(f"Arabic text detected, not translated: '{text_unicode[:50]}...'") - return text_unicode - - # Check cache if enabled - if use_cache: - cached = _get_cached_translation(text_unicode, target_lang) - if cached is not None: - _log(f"Cache HIT: '{text_unicode[:30]}...' -> '{cached[:30]}...'") - return cached - - # Error handling for overly long texts - if len(text_unicode) > MAX_CHARS_PER_REQUEST: - _log("Text too long (" + - str(len(text_unicode)) + - " chars), truncated to " + - str(MAX_CHARS_PER_REQUEST)) - text_unicode = text_unicode[:MAX_CHARS_PER_REQUEST] - - # Prepare the request - params = { - "client": "gtx", # Fake client to bypass restrictions - "sl": "auto", # Automatic source language - "tl": target_lang, # Target language - "dt": "t", # Response type: translation only - "q": text_unicode, # Text to translate - } - - try: - # Build the URL - query_string = urlencode(params) - url = f"{TRANSLATE_API_URL}?{query_string}" - - _log(f"Translating: '{text_unicode[:40]}...' -> {target_lang}") - - # Set timeout to avoid blocking - socket.setdefaulttimeout(REQUEST_TIMEOUT) - - # Perform the request - req = Request(url) - for key, value in HEADERS.items(): - req.add_header(key, value) - response = urlopen(req, timeout=REQUEST_TIMEOUT) - raw_data = response.read() - - # Decode the response - if isinstance(raw_data, bytes): - raw_data = raw_data.decode('utf-8') - - # Parse JSON response - data = loads(raw_data) - - # Extract the translation from the JSON structure - translated_text = "" - if isinstance(data, list) and data: - # Typical structure: [[[translation, original], ...], ...] - for item in data[0]: - if item and isinstance(item, list) and item[0]: - translated_text += item[0] - - # Clean the result - if translated_text: - translated_text = _clean_whitespace(translated_text) - - # Save to cache - if use_cache: - _cache_translation(text_unicode, target_lang, translated_text) - - elapsed = time.time() - start_time - _log(( - f"Translation completed in {elapsed:.2f}s: '{text_unicode[:30]}...' -> " - f"'{translated_text[:30]}...'" - )) - - return translated_text - else: - _log(f"Empty API response for: '{text_unicode[:30]}...'") - return text_unicode - - except socket.timeout: - _log(f"TIMEOUT during translation: '{text_unicode[:30]}...'") - return text_unicode - - except (URLError, HTTPError) as e: - _log(f"HTTP error {getattr(e, 'code', 'N/A')}: {str(e)}") - return text_unicode - - except JSONDecodeError as e: - _log(f"JSON error: {str(e)}") - return text_unicode - - except Exception as e: - error_type = type(e).__name__ - _log(f"Error {error_type}: {str(e)}") - return text_unicode - - finally: - # Restore default timeout - socket.setdefaulttimeout(None) - - -# ============================================================ -# AUXILIARY FUNCTIONS FOR SPECIAL CASES -# ============================================================ - - -def translate_batch(texts, target_lang=None, use_cache=True): - """ - Translates a list of texts in batch. - Optimized to reduce the number of HTTP requests. - - Args: - texts (list): List of texts to translate - target_lang (str): Target language - use_cache (bool): Use cache - - Returns: - list: List of translated texts - """ - if not texts: - return [] - - # Use system language if not specified - if target_lang is None: - target_lang = _get_system_language() - results = [] - batch_text = [] - batch_indices = [] - - for i, text in enumerate(texts): - text_unicode = _to_unicode(text) - - # Check cache - if use_cache: - cached = _get_cached_translation(text_unicode, target_lang) - if cached is not None: - results.append(cached) - continue - - # If the text is Arabic, do not translate it - if _is_text_arabic(text_unicode): - results.append(text_unicode) - continue - - # Add to batch - batch_text.append(text_unicode) - batch_indices.append(i) - results.append(None) # Placeholder - - # If there are texts to translate in batch - if batch_text: - try: - # Join texts with a special separator - separator = u" ||| " - combined_text = separator.join(batch_text) - - # Translate the batch - combined_translated = translate_text( - combined_text, - target_lang, - use_cache=False # Do not use cache for batch - ) - - # Split results - if separator in combined_translated: - translated_parts = combined_translated.split(separator) - else: - # Fallback: split by approximate number - translated_parts = [combined_translated] * len(batch_text) - - # Update results - for idx, translated in zip(batch_indices, translated_parts): - results[idx] = translated - - # Save to cache - if use_cache and idx < len(texts): - text_unicode = _to_unicode(texts[idx]) - _cache_translation(text_unicode, target_lang, translated) - - except Exception as e: - _log(f"Batch translation error: {str(e)}") - # Fallback: translate individually - for idx in batch_indices: - if results[idx] is None and idx < len(texts): - results[idx] = translate_text( - texts[idx], target_lang, use_cache) - - # Replace None with original text - for i in range(len(results)): - if results[i] is None: - results[i] = _to_unicode(texts[i]) - - return results - - -def safe_translate(text, fallback=None, **kwargs): - """ - Safe version of translate_text that always returns a valid string. - Args: - text (str): Text to translate - fallback (str): Fallback text if translation fails - **kwargs: Additional arguments for translate_text - Returns: - str: Translated text, fallback or original - """ - try: - translated = translate_text(text, **kwargs) - if translated and translated.strip(): - return translated - - # If translation is empty, use fallback - if fallback is not None: - return _to_unicode(fallback) - - return _to_unicode(text) - - except Exception as e: - _log(f"Error in safe_translate: {str(e)}") - if fallback is not None: - return _to_unicode(fallback) - return _to_unicode(text) - - -def trans(text, target_lang=None): - """ - Simplified translation function for single strings. - Uses cache and translate_text. - """ - if target_lang is None: - target_lang = _get_system_language() - target_lang = target_lang.lower() - - if not text or not isinstance(text, str): - return text or "" - - text = text.strip() - if not text: - return "" - - # Do not translate Arabic text - if _is_text_arabic(text): - return text - - # Check cache using full key (language + hash) - cached = _get_cached_translation(text, target_lang) - if cached is not None: - return cached - - # Translate (translate_text already handles internal caching if - # use_cache=True) - translated = translate_text(text, target_lang, use_cache=True) - if translated and translated != text: - return translated - return text - - -def translate_batch_strings(texts, target_lang=None): - """ - High-level batch translation for a list of strings. - """ - if not texts: - return [] - valid_texts = [str(t).strip() for t in texts if t and str(t).strip()] - if not valid_texts: - return [] - - # Use the existing cache via translate_batch - return translate_batch(valid_texts, target_lang, use_cache=True) - - -# ============================================================ -# TEST FUNCTION (for debugging) -# ============================================================ - - -def test_translation(): - """Test function to verify functionality""" - test_cases = [ - ("Hello world", "it", "Ciao mondo"), - ("Weather forecast", "es", "Pronóstico del tiempo"), - ("Temperature", "fr", "Température"), - ] - if DEBUG: - print("=" * 60) - print("Foreca One TRANSLATION TEST") - print("=" * 60) - - all_passed = True - - for original, lang, expected in test_cases: - result = translate_text(original, lang) - - if result and result.lower() == expected.lower(): - status = "✓ PASS" - else: - status = "✗ FAIL" - all_passed = False - if DEBUG: - print( - f"{status}: '{original}' -> '{result}' (expected: '{expected}')") - if DEBUG: - print("=" * 60) - stats = get_cache_stats() - print(( - f"Cache statistics: {stats['hits']} hits, {stats['misses']} misses, " - f"rate: {stats['hit_rate']:.1%}" - )) - print("=" * 60) - - return all_passed - - -# ============================================================ -# INITIALIZATION -# ============================================================ - -# Load cache at module startup -load_cache_from_disk() - -if __name__ == "__main__": - # Test mode when run directly - if DEBUG: - print("Google Translate API for Foreca") - print("Enhanced custom version") - - if test_translation(): - print("✓ All tests passed!") - else: - print("✗ Some tests failed") -else: - # Imported as a module - _log("Foreca One translation module loaded") - _log(f"System language: {_get_system_language()}") From bdb625d99af06af2ccc20695b51d8b9ad4265370 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 23:02:41 +0000 Subject: [PATCH 09/18] Collect system information without freezing the GUI Opening the Information screen imported stbinfo on the GUI thread; its module-level StbInfo() runs pings, TCP probes and up to four public-IP lookups, blocking the whole interface for 20+ seconds on first open (worse when offline, since every lookup runs into its timeout). - LSinfo now shows 'Collecting system information...' immediately, gathers the data in a daemon thread (including the heavy first import) and applies the text from an eTimer on the main thread. The poll stops on screen close so a late result cannot touch a dead widget. - stbinfo tracks connectivity as a boolean and skips the public-IP lookups entirely when the box is offline - previously the offline case was the slowest. Lookup timeouts trimmed to 3s. --- .../LinuxsatPanel/addons/stbinfo.py | 8 ++- .../Extensions/LinuxsatPanel/plugin.py | 49 +++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py index 9aca745..c41f08d 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/addons/stbinfo.py @@ -65,13 +65,15 @@ def __init__(self): self.is_vti_image = self._is_vti_image() self.is_dmm_image = self._is_dmm_image() + self.has_internet = False self.internetline = self.get_internet_status() self.mountid = self.get_mount_info() self.storhdd = self.get_storage_info() self.memin = self.get_memory_info() self.ipub = self.get_ip() self.current_format = self.getResolution() - self.pip = self.get_public_ip() + # Skip the slow public IP lookups when there is no connection + self.pip = self.get_public_ip() if self.has_internet else None def to_string(self): def fmt(label, value): @@ -143,6 +145,7 @@ def get_internet_status(self): # Test 1: Ping Google DNS if system("ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1") == 0: + self.has_internet = True return _("Internet: Connected") # Test 2: TCP connection to port 80 (HTTP) of a reliable server @@ -152,6 +155,7 @@ def get_internet_status(self): sock.settimeout(3) sock.connect(("www.google.com", 80)) sock.close() + self.has_internet = True return _("Internet: Connected") except BaseException: pass @@ -344,7 +348,7 @@ def get_public_ip(self): for service in services: try: - response = requests.get(service, timeout=5) + response = requests.get(service, timeout=3) if response.status_code == 200: ip = response.text.strip() if ip and '.' in ip: # Validazione base IP diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 23845f5..8675b70 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -4737,6 +4737,49 @@ def startRun(self): self["list"].setText(_("Unable to download updates!")) def openinfo(self): + # Collect in a background thread: the first stbinfo import runs + # network probes and must not freeze the GUI. The screen updates + # from an eTimer on the main thread when the data is ready. + import threading + self["list"].setText(_("Collecting system information...")) + self._info_content = None + self._info_closed = False + self.onClose.append(self._stopInfoPoll) + + def collect(): + try: + self._info_content = self._collectInfo() + except Exception as e: + print("Error in openinfo collect:", e) + self._info_content = "Error loading information" + + info_thread = threading.Thread(target=collect) + info_thread.daemon = True + info_thread.start() + + self.info_poll = eTimer() + try: + self.info_poll_conn = self.info_poll.timeout.connect( + self._checkInfoReady) + except BaseException: + self.info_poll.callback.append(self._checkInfoReady) + self.info_poll.start(250, False) + + def _stopInfoPoll(self): + self._info_closed = True + try: + self.info_poll.stop() + except BaseException: + pass + + def _checkInfoReady(self): + if self._info_closed: + return + if self._info_content is not None: + self.info_poll.stop() + self["list"].setText(str(self._info_content)) + + def _collectInfo(self): from .addons.stbinfo import stbinfo try: header = "Suggested by: @masterG - @oktus - @pcd\n" @@ -4800,14 +4843,14 @@ def openinfo(self): with open("/tmp/output.txt", "r") as filer: content = filer.read().decode("utf-8") - self["list"].setText(str(content)) + return str(content) except Exception as e: print("Final read/display error: {0}".format(str(e))) - self["list"].setText("Error loading system information") + return "Error loading system information" except Exception as e: print("Error in openinfo:", e) - self["list"].setText("Error loading information") + return "Error loading information" def cancel(self): self.close() From 7678000830e83cf016db1b9dbbf122a1ed7d9a1d Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 23:15:27 +0000 Subject: [PATCH 10/18] Make settings install safe against data loss okRun1 wiped lamedb and every .tv/.radio file from /etc/enigma2 before checking that the download or unzip had succeeded: urlretrieve had no error handling, the unzip exit code was ignored, and a zip with an unexpected layout made the cp copy nothing - any of these left the box with no channel list at all. The install now runs download -> verify -> backup -> wipe -> install: - download via requests with a timeout and clear failure message, aborting before /etc/enigma2 is touched; - unzip exit code checked; the payload must actually contain a lamedb or .tv file (channel lists at the zip root are now handled too, and the top-level folder is detected instead of walking into the deepest subdirectory); - the current /etc/enigma2 is saved to /tmp/settings_backup.tar.gz before the wipe and restored automatically if the copy fails. --- .../Extensions/LinuxsatPanel/plugin.py | 101 +++++++++++++----- 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 8675b70..2ea3a78 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -4344,34 +4344,79 @@ def okRun1(self, answer=False): if keepiptv(): print("-----save iptv channels-----") - from six.moves.urllib.request import urlretrieve - urlretrieve(url, dest) - if exists(dest) and ".zip" in dest: - fdest1 = "/tmp/unzipped" - fdest2 = "/etc/enigma2" - if exists("/tmp/unzipped"): - system("rm -rf /tmp/unzipped") - makedirs("/tmp/unzipped") - cmd2 = "unzip -o -q '/tmp/settings.zip' -d " + fdest1 - system(cmd2) - for root, dirs, files in walk(fdest1): - for name in dirs: - self.namel = name - system("rm -rf /etc/enigma2/lamedb") - system("rm -rf /etc/enigma2/*.radio") - system("rm -rf /etc/enigma2/*.tv") - system("rm -rf /etc/enigma2/*.del") - system("cp -rf '/tmp/unzipped/" + - str(self.namel) + "/'* " + fdest2) - system("rm -rf /tmp/unzipped") - system("rm -rf /tmp/settings.zip") - title = (_("Installing %s\nPlease Wait...") % self.name) - self.session.openWithCallback( - self.yes, - lsConsole, - title=_(title), - cmdlist=["wget -qO - http://127.0.0.1/web/servicelistreload?mode=0 > /tmp/inst.txt 2>&1 &"], - closeOnSuccess=False) + fdest1 = "/tmp/unzipped" + fdest2 = "/etc/enigma2" + backup = "/tmp/settings_backup.tar.gz" + + def cleanup_tmp(): + system("rm -rf " + fdest1) + system("rm -f " + dest) + + # Download and verify BEFORE touching /etc/enigma2, so a + # failed transfer can never leave the box without channels + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + with open(dest, "wb") as f: + f.write(response.content) + except Exception as e: + print("[Settings] download failed:", e) + self["info"].setText( + _("Download failed! Settings NOT installed.")) + return + + if exists(fdest1): + system("rm -rf " + fdest1) + makedirs(fdest1) + if system("unzip -o -q '%s' -d %s" % (dest, fdest1)) != 0: + print("[Settings] corrupted archive:", url) + self["info"].setText( + _("Corrupted archive! Settings NOT installed.")) + cleanup_tmp() + return + + # The channel list may live at the root of the zip or in a + # single top-level folder + srcdir = fdest1 + for root, dirs, files in walk(fdest1): + if dirs and not files: + self.namel = dirs[0] + srcdir = join(fdest1, self.namel) + break + payload = [] + for root, dirs, files in walk(srcdir): + payload.extend(files) + break + if "lamedb" not in payload and not any( + name.endswith(".tv") for name in payload): + print("[Settings] no channel list in archive:", url) + self["info"].setText( + _("No channel list in archive! Settings NOT installed.")) + cleanup_tmp() + return + + # Safety net: keep the current configuration until reboot + system("tar -czf %s -C / etc/enigma2 2>/dev/null" % backup) + + system("rm -rf /etc/enigma2/lamedb") + system("rm -rf /etc/enigma2/*.radio") + system("rm -rf /etc/enigma2/*.tv") + system("rm -rf /etc/enigma2/*.del") + if system("cp -rf '%s/'* %s" % (srcdir, fdest2)) != 0: + system("tar -xzf %s -C / 2>/dev/null" % backup) + print("[Settings] install failed, backup restored") + self["info"].setText( + _("Install failed! Previous settings restored.")) + cleanup_tmp() + return + cleanup_tmp() + title = (_("Installing %s\nPlease Wait...") % self.name) + self.session.openWithCallback( + self.yes, + lsConsole, + title=_(title), + cmdlist=["wget -qO - http://127.0.0.1/web/servicelistreload?mode=0 > /tmp/inst.txt 2>&1 &"], + closeOnSuccess=False) else: self["info"].setText(_("Settings Not Installed ...")) From ef8ab3a9249176c5fbea61f166f740873225b3e7 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 23:19:27 +0000 Subject: [PATCH 11/18] Factor the shared grid engine into LPGridScreen Six screens (LinuxsatPanel, LSskin, LSChannel, LulullaScript, CiefpInstaller, ScriptInstaller) each carried a byte-identical copy of the 20-tile grid machinery: title/skin/resolution setup, widget and ActionMap wiring, paging (openTest/paintFrame), navigation (key_left/right/up/down), sorting and the view-log prompt. Every fix in this area had to be applied six times. The engine now lives once in an LPGridScreen base class: subclasses fill their menu lists and call initGrid(). Behavior is unchanged, verified by driving every screen through full navigation wrap-around, paging and sort/restore in a stubbed enigma2 environment. Also folded in while moving the code: - _view_log kept its eTimer in a local variable, so the timer could be garbage collected before firing and the log viewer silently never opened; the timer is now held on the screen instance. - Dropped keyNumberGlobal, dead code referencing a "menu" widget that none of these screens define, with no key binding pointing at it. plugin.py shrinks by ~1150 lines net. --- .../Extensions/LinuxsatPanel/plugin.py | 1773 +++-------------- 1 file changed, 315 insertions(+), 1458 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 2ea3a78..03e7c33 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -466,10 +466,12 @@ def add_menu_item(menu_list, titles, pics, urls, title, pic_name, url=""): urls.append(url) # add missing string for URL -class LinuxsatPanel(Screen): +class LPGridScreen(Screen): + """Shared 20-tile grid engine used by all category screens.""" - def __init__(self, session): + PIXMAPS_PER_PAGE = 20 + def __init__(self, session): Screen.__init__(self, session) try: Screen.setTitle(self, _("%s") % descplug + " V." + __version__) @@ -481,7 +483,6 @@ def __init__(self, session): skin = join(skin_path, "LinuxsatPanel.xml") with codecs.open(skin, "r", encoding="utf-8") as f: self.skin = f.read() - if isWQHD(): self.pos = get_positions("WQHD") elif isFHD(): @@ -489,6 +490,231 @@ def __init__(self, session): elif isHD(): self.pos = get_positions("HD") + def initGrid(self, menu_list): + """Wire widgets, actions and paging once the menu lists are filled.""" + self.names = menu_list + self.sorted = False + self["frame"] = MovingPixmap() + self["info"] = Label() + self["info"].setText(_("Please Wait...")) + self["sort"] = Label(_("Sort A-Z")) + self["key_red"] = Label(_("Exit")) + self["pixmap"] = Pixmap() + self["actions"] = ActionMap( + [ + "OkCancelActions", + "MenuActions", + "DirectionActions", + "NumberActions", + "ColorActions", + "EPGSelectActions", + "InfoActions" + ], + { + "ok": self.okbuttonClick, + "cancel": self.closeNonRecursive, + "exit": self.closeRecursive, + "back": self.closeNonRecursive, + "red": self.closeNonRecursive, + "0": self.list_sort, + "left": self.key_left, + "right": self.key_right, + "up": self.key_up, + "down": self.key_down, + "info": self.key_info, + "menu": self.closeRecursive + }, + -1 + ) + i = 0 + while i < self.PIXMAPS_PER_PAGE: + self["label" + str(i + 1)] = StaticText() + self["pixmap" + str(i + 1)] = Pixmap() + i += 1 + self.npics = len(self.names) + self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) + self.index = 0 + self.maxentry = len(menu_list) - 1 + self.ipage = 1 + self.onLayoutFinish.append(self.openTest) + + def okbuttonClick(self): + pass + + def paintFrame(self): + try: + # If the index exceeds the maximum number of items, it returns to + # the first item + if self.index > self.maxentry: + self.index = self.minentry + self.idx = self.index + name = self.names[self.idx] + self["info"].setText(str(name)) + ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) + ipos = self.pos[ifr] + self["frame"].moveTo(ipos[0], ipos[1], 1) + self["frame"].startMoving() + except Exception as e: + print("Error in paintFrame: ", e) + + def openTest(self): + if self.ipage < self.npage: + self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 + self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE + + elif self.ipage == self.npage: + self.maxentry = len(self.pics) - 1 + self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE + i1 = 0 + while i1 < self.PIXMAPS_PER_PAGE: + self["label" + str(i1 + 1)].setText(" ") + self["pixmap" + str(i1 + 1) + ].instance.setPixmapFromFile(nss_pic) + i1 += 1 + self.npics = len(self.pics) + i = 0 + i1 = 0 + self.picnum = 0 + ln = self.maxentry - (self.minentry - 1) + while i < ln: + idx = self.minentry + i + # self["label" + str(i + 1)].setText(self.names[idx]) # this show + # label to bottom of png pixmap + pic = self.pics[idx] + if not exists(self.pics[idx]): + pic = nss_pic + self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) + i += 1 + self.index = self.minentry + self.paintFrame() + + def key_left(self): + # Decrement the index only if we are not at the first pixmap + if self.index >= 0: + self.index -= 1 + else: + # If we are at the first pixmap, go back to the last pixmap of the + # last page + self.ipage = self.npage + self.index = self.npics - 1 + # Check if we need to change pages + if self.index < self.minentry: + self.ipage -= 1 + if self.ipage < 1: # If we go beyond the first page + self.ipage = self.npage + self.index = self.npics - 1 # Back to the last pixmap of the last page + self.openTest() + else: + self.paintFrame() + + def key_right(self): + # Increment the index only if we are not at the last pixmap + if self.index < self.npics - 1: + self.index += 1 + else: + # If we are at the last pixmap, go back to the first pixmap of the + # first page + self.index = 0 + self.ipage = 1 + self.openTest() + # Check if we need to change pages + if self.index > self.maxentry: + self.ipage += 1 + if self.ipage > self.npage: # If we exceed the number of pages + self.index = 0 + self.ipage = 1 # Back to first page + self.openTest() + else: + self.paintFrame() + + def key_up(self): + if self.index == 0 and self.ipage == 1: + self.ipage = self.npage + self.index = self.minentry + self.openTest() + + elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: + self.index -= 5 + else: + if self.ipage == self.npage and self.index == self.minentry: + self.ipage = 1 + self.index = 0 + self.openTest() + else: + self.ipage = self.npage + self.index = self.npics - 1 + self.openTest() + self.paintFrame() + + def key_down(self): + if self.index <= self.maxentry - 5: + self.index += 5 + else: + if self.ipage == self.npage: + self.ipage = 1 + self.index = 0 + self.openTest() + else: + self.ipage += 1 + self.index = self.minentry + self.openTest() + + self.paintFrame() + + def list_sort(self): + if not hasattr(self, "original_data"): + self.original_data = ( + self.names[:], + self.titles[:], + self.pics[:], + self.urls[:]) + self.sorted = False + + if self.sorted: + self.names, self.titles, self.pics, self.urls = self.original_data + self.sorted = False + self["sort"].setText(_("Sort A-Z")) + else: + self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( + self.names, self.titles, self.pics, self.urls) + self.sorted = True + self["sort"].setText(_("Sort Default")) + + self.openTest() + + def closeNonRecursive(self): + self.close(False) + + def closeRecursive(self): + self.close(True) + + def createSummary(self): + return + + def key_info(self): + self.session.open(LSinfo, " Information ") + + def _view_log(self, answer): + if answer: + from enigma import eTimer + + def open_fc(): + from .addons.File_Commander import File_Commander + if fileExists(file_log): + self.session.open(File_Commander, file_log) + # keep a reference or the timer is garbage collected + # before it fires + self._fc_timer = eTimer() + self._fc_timer.callback.append(open_fc) + self._fc_timer.start(0, True) + + +class LinuxsatPanel(LPGridScreen): + + def __init__(self, session): + + LPGridScreen.__init__(self, session) + self.data = checkGZIP(xmlurl) menu_list = [] self.titles = [] @@ -836,57 +1062,8 @@ def __init__(self, session): " About ", "about.png") - self.names = menu_list - self.sorted = False - # self.combined_data = list(zip(self.names, self.titles, self.pics, self.urls)) - self["frame"] = MovingPixmap() - self["info"] = Label() - self["info"].setText(_("Please Wait...")) - self["sort"] = Label(_("Sort A-Z")) - self["key_red"] = Label(_("Exit")) - self["pixmap"] = Pixmap() - self["actions"] = ActionMap( - [ - "OkCancelActions", - "MenuActions", - "DirectionActions", - "NumberActions", - "ColorActions", - "EPGSelectActions", - "InfoActions" - ], - { - "ok": self.okbuttonClick, - "cancel": self.closeNonRecursive, - "exit": self.closeRecursive, - "back": self.closeNonRecursive, - "red": self.closeNonRecursive, - "0": self.list_sort, - "left": self.key_left, - "right": self.key_right, - "up": self.key_up, - "down": self.key_down, - "info": self.key_info, - "menu": self.closeRecursive - }, - -1 - ) - - self.PIXMAPS_PER_PAGE = 20 - i = 0 - while i < self.PIXMAPS_PER_PAGE: - self["label" + str(i + 1)] = StaticText() - self["pixmap" + str(i + 1)] = Pixmap() - i += 1 - - self.npics = len(self.names) - self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) - # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.index = 0 - self.maxentry = len(menu_list) - 1 - self.ipage = 1 - self.onLayoutFinish.append(self.openTest) - # self.onLayoutFinish.append(self.start_check_version) + self.initGrid(menu_list) + # self.onLayoutFinish.append(self.start_check_version) def start_check_version(self): self.Update = False @@ -936,153 +1113,6 @@ def start_check_version(self): else: print("No new version available.") - def paintFrame(self): - try: - # If the index exceeds the maximum number of items, it returns to - # the first item - if self.index > self.maxentry: - self.index = self.minentry - self.idx = self.index - name = self.names[self.idx] - self["info"].setText(str(name)) - ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) - ipos = self.pos[ifr] - self["frame"].moveTo(ipos[0], ipos[1], 1) - self["frame"].startMoving() - except Exception as e: - print("Error in paintFrame: ", e) - - def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: - self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 - self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 - self.index = self.minentry - self.paintFrame() - - def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: - self.index -= 1 - else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page - self.openTest() - else: - self.paintFrame() - - def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: - self.index += 1 - else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page - self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage - self.index = self.minentry - self.openTest() - - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 - else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() - self.paintFrame() - - def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 - else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() - - self.paintFrame() - - def keyNumberGlobal(self, number): - number -= 1 - if len(self["menu"].list) > number: - self["menu"].setIndex(number) - self.okbuttonClick() - - def list_sort(self): - if not hasattr(self, "original_data"): - self.original_data = ( - self.names[:], - self.titles[:], - self.pics[:], - self.urls[:]) - self.sorted = False - - if self.sorted: - self.names, self.titles, self.pics, self.urls = self.original_data - self.sorted = False - self["sort"].setText(_("Sort A-Z")) - else: - self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( - self.names, self.titles, self.pics, self.urls) - self.sorted = True - self["sort"].setText(_("Sort Default")) - - self.openTest() - def refreshPlugins(self): plugins.clearPluginList() plugins.readPluginList(resolveFilename(SCOPE_PLUGINS)) @@ -1095,12 +1125,6 @@ def closeRecursive(self): def closeNonRecursive(self): self.session.openWithCallback(self.close, AboutLSS) - def createSummary(self): - return - - def key_info(self): - self.session.open(LSinfo, " Information ") - def okbuttonClick(self): self.idx = self.index if self.idx is None: @@ -1160,30 +1184,11 @@ def okbuttonContinue(self, result): ) -class LSskin(Screen): +class LSskin(LPGridScreen): def __init__(self, session, name): - Screen.__init__(self, session) - - try: - Screen.setTitle(self, _("%s") % descplug + " V." + __version__) - except BaseException: - try: - self.setTitle(_("%s") % descplug + " V." + __version__) - except BaseException: - pass - skin = join(skin_path, "LinuxsatPanel.xml") - with codecs.open(skin, "r", encoding="utf-8") as f: - self.skin = f.read() - + LPGridScreen.__init__(self, session) self.data = checkGZIP(xmlurl) - # self.data = fetch_url(xmlurl) - if isWQHD(): - self.pos = get_positions("WQHD") - elif isFHD(): - self.pos = get_positions("FHD") - elif isHD(): - self.pos = get_positions("HD") self.name = name menu_list = [] @@ -1263,257 +1268,31 @@ def __init__(self, session, name): "Skins Oe Based ", "oebased.png") - self.names = menu_list - self.sorted = False - # self.combined_data = zip(self.names, self.titles, self.pics, self.urls) - self["frame"] = MovingPixmap() - self["info"] = Label() - self["info"].setText(_("Please Wait...")) - self["sort"] = Label(_("Sort A-Z")) - self["key_red"] = Label(_("Exit")) - self["pixmap"] = Pixmap() - self["actions"] = ActionMap( - [ - "OkCancelActions", - "MenuActions", - "DirectionActions", - "NumberActions", - "ColorActions", - "EPGSelectActions", - "InfoActions" - ], - { - "ok": self.okbuttonClick, - "cancel": self.closeNonRecursive, - "exit": self.closeRecursive, - "back": self.closeNonRecursive, - "red": self.closeNonRecursive, - "0": self.list_sort, - "left": self.key_left, - "right": self.key_right, - "up": self.key_up, - "down": self.key_down, - "info": self.key_info, - "menu": self.closeRecursive - }, - -1 - ) + self.initGrid(menu_list) - self.PIXMAPS_PER_PAGE = 20 - i = 0 - while i < self.PIXMAPS_PER_PAGE: - self["label" + str(i + 1)] = StaticText() - self["pixmap" + str(i + 1)] = Pixmap() - i += 1 + def okbuttonClick(self): + self.idx = self.index + if self.idx is None: + return + name = self.names[self.idx] + title = self.titles[self.idx] + self.data = checkGZIP(xmlurl) + if self.data is not None: + n1 = self.data.find(title, 0) + n2 = self.data.find("", n1) + url = self.data[n1:n2] + self.session.open(addInstall, url, name, None) + else: + self.session.open( + MessageBox, _("Error: No Data Find."), + MessageBox.TYPE_ERROR + ) - self.npics = len(self.names) - # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) - self.index = 0 - self.maxentry = len(menu_list) - 1 - self.ipage = 1 - self.onLayoutFinish.append(self.openTest) - def paintFrame(self): - try: - # If the index exceeds the maximum number of items, it returns to - # the first item - if self.index > self.maxentry: - self.index = self.minentry - self.idx = self.index - name = self.names[self.idx] - self["info"].setText(str(name)) - ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) - ipos = self.pos[ifr] - self["frame"].moveTo(ipos[0], ipos[1], 1) - self["frame"].startMoving() - except Exception as e: - print("Error in paintFrame: ", e) - - def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: - self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 - self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 - self.index = self.minentry - self.paintFrame() - - def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: - self.index -= 1 - else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page - self.openTest() - else: - self.paintFrame() - - def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: - self.index += 1 - else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page - self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage - self.index = self.minentry - self.openTest() - - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 - else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() - self.paintFrame() - - def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 - else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() - - self.paintFrame() - - def keyNumberGlobal(self, number): - number -= 1 - if len(self["menu"].list) > number: - self["menu"].setIndex(number) - self.okbuttonClick() - - def list_sort(self): - if not hasattr(self, "original_data"): - self.original_data = ( - self.names[:], - self.titles[:], - self.pics[:], - self.urls[:]) - self.sorted = False - - if self.sorted: - self.names, self.titles, self.pics, self.urls = self.original_data - self.sorted = False - self["sort"].setText(_("Sort A-Z")) - else: - self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( - self.names, self.titles, self.pics, self.urls) - self.sorted = True - self["sort"].setText(_("Sort Default")) - - self.openTest() - - def closeNonRecursive(self): - self.close(False) - - def closeRecursive(self): - self.close(True) - - def createSummary(self): - return - - def key_info(self): - self.session.open(LSinfo, " Information ") - - def okbuttonClick(self): - self.idx = self.index - if self.idx is None: - return - name = self.names[self.idx] - title = self.titles[self.idx] - self.data = checkGZIP(xmlurl) - if self.data is not None: - n1 = self.data.find(title, 0) - n2 = self.data.find("", n1) - url = self.data[n1:n2] - self.session.open(addInstall, url, name, None) - else: - self.session.open( - MessageBox, _("Error: No Data Find."), - MessageBox.TYPE_ERROR - ) - - -class LSChannel(Screen): +class LSChannel(LPGridScreen): def __init__(self, session, name): - Screen.__init__(self, session) - - try: - Screen.setTitle(self, _("%s") % descplug + " V." + __version__) - except BaseException: - try: - self.setTitle(_("%s") % descplug + " V." + __version__) - except BaseException: - pass - skin = join(skin_path, "LinuxsatPanel.xml") - with codecs.open(skin, "r", encoding="utf-8") as f: - self.skin = f.read() - - if isWQHD(): - self.pos = get_positions("WQHD") - elif isFHD(): - self.pos = get_positions("FHD") - elif isHD(): - self.pos = get_positions("HD") + LPGridScreen.__init__(self, session) self.name = name menu_list = [] @@ -1545,240 +1324,33 @@ def __init__(self, session, name): self.urls, "MANUTEK ", "manutek.png", - "https://www.manutek.it/isetting/index.php") - add_menu_item( - menu_list, - self.titles, - self.pics, - self.urls, - "MORPHEUS ", - "morpheus883.png", - "https://github.com/morpheus883/enigma2-zipped") - add_menu_item( - menu_list, - self.titles, - self.pics, - self.urls, - "VHANNIBAL NET ", - "vhannibal1.png", - "https://www.vhannibal.net/asd.php") - add_menu_item( - menu_list, - self.titles, - self.pics, - self.urls, - "VHANNIBAL TEK ", - "vhannibal2.png", - "https://sat.alfa-tech.net/upload/settings/vhannibal/") - - self.names = menu_list - # self.combined_data = zip(self.names, self.titles, self.pics, self.urls) - self["frame"] = MovingPixmap() - self["info"] = Label() - self["info"].setText(_("Please Wait...")) - self["sort"] = Label(_("Sort A-Z")) - self["key_red"] = Label(_("Exit")) - self["pixmap"] = Pixmap() - self["actions"] = ActionMap( - [ - "OkCancelActions", - "MenuActions", - "DirectionActions", - "NumberActions", - "ColorActions", - "EPGSelectActions", - "InfoActions" - ], - { - "ok": self.okbuttonClick, - "cancel": self.closeNonRecursive, - "exit": self.closeRecursive, - "back": self.closeNonRecursive, - "red": self.closeNonRecursive, - "0": self.list_sort, - "left": self.key_left, - "right": self.key_right, - "up": self.key_up, - "down": self.key_down, - "info": self.key_info, - "menu": self.closeRecursive - }, - -1 - ) - - self.PIXMAPS_PER_PAGE = 20 - i = 0 - while i < self.PIXMAPS_PER_PAGE: - self["label" + str(i + 1)] = StaticText() - self["pixmap" + str(i + 1)] = Pixmap() - i += 1 - - self.npics = len(self.names) - # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) - self.index = 0 - self.maxentry = len(menu_list) - 1 - self.ipage = 1 - self.onLayoutFinish.append(self.openTest) - - def paintFrame(self): - try: - # If the index exceeds the maximum number of items, it returns to - # the first item - if self.index > self.maxentry: - self.index = self.minentry - self.idx = self.index - name = self.names[self.idx] - self["info"].setText(str(name)) - ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) - ipos = self.pos[ifr] - self["frame"].moveTo(ipos[0], ipos[1], 1) - self["frame"].startMoving() - except Exception as e: - print("Error in paintFrame: ", e) - - def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: - self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 - self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 - self.index = self.minentry - self.paintFrame() - - def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: - self.index -= 1 - else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page - self.openTest() - else: - self.paintFrame() - - def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: - self.index += 1 - else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page - self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage - self.index = self.minentry - self.openTest() - - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 - else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() - self.paintFrame() - - def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 - else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() - - self.paintFrame() - - def keyNumberGlobal(self, number): - number -= 1 - if len(self["menu"].list) > number: - self["menu"].setIndex(number) - self.okbuttonClick() - - def list_sort(self): - if not hasattr(self, "original_data"): - self.original_data = ( - self.names[:], - self.titles[:], - self.pics[:], - self.urls[:]) - self.sorted = False - - if self.sorted: - self.names, self.titles, self.pics, self.urls = self.original_data - self.sorted = False - self["sort"].setText(_("Sort A-Z")) - else: - self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( - self.names, self.titles, self.pics, self.urls) - self.sorted = True - self["sort"].setText(_("Sort Default")) - - self.openTest() - - def closeNonRecursive(self): - self.close(False) - - def closeRecursive(self): - self.close(True) - - def createSummary(self): - return + "https://www.manutek.it/isetting/index.php") + add_menu_item( + menu_list, + self.titles, + self.pics, + self.urls, + "MORPHEUS ", + "morpheus883.png", + "https://github.com/morpheus883/enigma2-zipped") + add_menu_item( + menu_list, + self.titles, + self.pics, + self.urls, + "VHANNIBAL NET ", + "vhannibal1.png", + "https://www.vhannibal.net/asd.php") + add_menu_item( + menu_list, + self.titles, + self.pics, + self.urls, + "VHANNIBAL TEK ", + "vhannibal2.png", + "https://sat.alfa-tech.net/upload/settings/vhannibal/") - def key_info(self): - self.session.open(LSinfo, " Information ") + self.initGrid(menu_list) def okbuttonClick(self): self.idx = self.index @@ -1789,28 +1361,10 @@ def okbuttonClick(self): self.session.open(addInstall, url, name, "") -class LulullaScript(Screen): +class LulullaScript(LPGridScreen): def __init__(self, session, name): - Screen.__init__(self, session) - - try: - Screen.setTitle(self, _("%s") % descplug + " V." + __version__) - except BaseException: - try: - self.setTitle(_("%s") % descplug + " V." + __version__) - except BaseException: - pass - skin = join(skin_path, "LinuxsatPanel.xml") - with codecs.open(skin, "r", encoding="utf-8") as f: - self.skin = f.read() - - if isWQHD(): - self.pos = get_positions("WQHD") - elif isFHD(): - self.pos = get_positions("FHD") - elif isHD(): - self.pos = get_positions("HD") + LPGridScreen.__init__(self, session) self.name = name menu_list = [] @@ -2132,227 +1686,19 @@ def __init__(self, session, name): self.titles, self.pics, self.urls, - "XC Forever", - "xc.png", - "wget -q --no-check-certificate \"https://raw.githubusercontent.com/Belfagor2005/xc_plugin_forever/main/installer.sh?inline=false\" -O - | /bin/sh") - add_menu_item( - menu_list, - self.titles, - self.pics, - self.urls, - "XXX Plugin", - "xxx_plugin.png", - "wget -q --no-check-certificate https://raw.githubusercontent.com/Belfagor2005/xxxplugin/main/installer.sh -O - | /bin/sh") - - self.names = menu_list - self.sorted = False - # self.combined_data = zip(self.names, self.titles, self.pics, self.urls) - self["frame"] = MovingPixmap() - self["info"] = Label() - self["info"].setText(_("Please Wait...")) - self["sort"] = Label(_("Sort A-Z")) - self["key_red"] = Label(_("Exit")) - self["pixmap"] = Pixmap() - self["actions"] = ActionMap( - [ - "OkCancelActions", - "MenuActions", - "DirectionActions", - "NumberActions", - "ColorActions", - "EPGSelectActions", - "InfoActions" - ], - { - "ok": self.okbuttonClick, - "cancel": self.closeNonRecursive, - "exit": self.closeRecursive, - "back": self.closeNonRecursive, - "red": self.closeNonRecursive, - "0": self.list_sort, - "left": self.key_left, - "right": self.key_right, - "up": self.key_up, - "down": self.key_down, - "info": self.key_info, - "menu": self.closeRecursive - }, - -1 - ) - - self.PIXMAPS_PER_PAGE = 20 - i = 0 - while i < self.PIXMAPS_PER_PAGE: - self["label" + str(i + 1)] = StaticText() - self["pixmap" + str(i + 1)] = Pixmap() - i += 1 - - self.npics = len(self.names) - # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) - self.index = 0 - self.maxentry = len(menu_list) - 1 - self.ipage = 1 - self.onLayoutFinish.append(self.openTest) - - def paintFrame(self): - try: - # If the index exceeds the maximum number of items, it returns to - # the first item - if self.index > self.maxentry: - self.index = self.minentry - self.idx = self.index - name = self.names[self.idx] - self["info"].setText(str(name)) - ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) - ipos = self.pos[ifr] - self["frame"].moveTo(ipos[0], ipos[1], 1) - self["frame"].startMoving() - except Exception as e: - print("Error in paintFrame: ", e) - - def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: - self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 - self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 - self.index = self.minentry - self.paintFrame() - - def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: - self.index -= 1 - else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page - self.openTest() - else: - self.paintFrame() - - def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: - self.index += 1 - else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page - self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage - self.index = self.minentry - self.openTest() - - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 - else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() - self.paintFrame() - - def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 - else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() - - self.paintFrame() - - def keyNumberGlobal(self, number): - number -= 1 - if len(self["menu"].list) > number: - self["menu"].setIndex(number) - self.okbuttonClick() - - def list_sort(self): - if not hasattr(self, "original_data"): - self.original_data = ( - self.names[:], - self.titles[:], - self.pics[:], - self.urls[:]) - self.sorted = False - - if self.sorted: - self.names, self.titles, self.pics, self.urls = self.original_data - self.sorted = False - self["sort"].setText(_("Sort A-Z")) - else: - self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( - self.names, self.titles, self.pics, self.urls) - self.sorted = True - self["sort"].setText(_("Sort Default")) - - self.openTest() - - def closeNonRecursive(self): - self.close(False) - - def closeRecursive(self): - self.close(True) - - def createSummary(self): - return + "XC Forever", + "xc.png", + "wget -q --no-check-certificate \"https://raw.githubusercontent.com/Belfagor2005/xc_plugin_forever/main/installer.sh?inline=false\" -O - | /bin/sh") + add_menu_item( + menu_list, + self.titles, + self.pics, + self.urls, + "XXX Plugin", + "xxx_plugin.png", + "wget -q --no-check-certificate https://raw.githubusercontent.com/Belfagor2005/xxxplugin/main/installer.sh -O - | /bin/sh") - def key_info(self): - self.session.open(LSinfo, " Information ") + self.initGrid(menu_list) def okbuttonClick(self): idx = self.index @@ -2406,42 +1752,11 @@ def console_closed(*args, **kwargs): else: return - def _view_log(self, answer): - if answer: - # Timer needed to open File_Commander from a safe context - from enigma import eTimer - - def open_fc(): - from .addons.File_Commander import File_Commander - if fileExists(file_log): - self.session.open(File_Commander, file_log) - timer = eTimer() - timer.callback.append(open_fc) - timer.start(0, True) - -class CiefpInstaller(Screen): +class CiefpInstaller(LPGridScreen): def __init__(self, session, name): - Screen.__init__(self, session) - - try: - Screen.setTitle(self, _("%s") % descplug + " V." + __version__) - except BaseException: - try: - self.setTitle(_("%s") % descplug + " V." + __version__) - except BaseException: - pass - skin = join(skin_path, "LinuxsatPanel.xml") - with codecs.open(skin, "r", encoding="utf-8") as f: - self.skin = f.read() - - if isWQHD(): - self.pos = get_positions("WQHD") - elif isFHD(): - self.pos = get_positions("FHD") - elif isHD(): - self.pos = get_positions("HD") + LPGridScreen.__init__(self, session) self.name = name menu_list = [] self.titles = [] @@ -2632,232 +1947,24 @@ def __init__(self, session, name): "CiefpsettingsMotor", "ciefp_sm.png", "wget -q --no-check-certificate https://raw.githubusercontent.com/ciefp/CiefpsettingsMotor/main/installer.sh -O - | /bin/sh") - add_menu_item( - menu_list, - self.titles, - self.pics, - self.urls, - "CiefpsettingsPanel", - "ciefp_sp.png", - "wget -q --no-check-certificate https://raw.githubusercontent.com/ciefp/CiefpsettingsPanel/main/installer.sh -O - | /bin/sh") - add_menu_item( - menu_list, - self.titles, - self.pics, - self.urls, - "WebCamE2PrenjSF", - "ciefp_webcam.png", - "wget -q --no-check-certificate https://raw.githubusercontent.com/ciefp/WebCamE2PrenjSF/main/installer.sh -O - | /bin/sh") - - self.names = menu_list - self.sorted = False - # self.combined_data = zip(self.names, self.titles, self.pics, self.urls) - self["frame"] = MovingPixmap() - self["info"] = Label() - self["info"].setText(_("Please Wait...")) - self["sort"] = Label(_("Sort A-Z")) - self["key_red"] = Label(_("Exit")) - self["pixmap"] = Pixmap() - self["actions"] = ActionMap( - [ - "OkCancelActions", - "MenuActions", - "DirectionActions", - "NumberActions", - "ColorActions", - "EPGSelectActions", - "InfoActions" - ], - { - "ok": self.okbuttonClick, - "cancel": self.closeNonRecursive, - "exit": self.closeRecursive, - "back": self.closeNonRecursive, - "red": self.closeNonRecursive, - "0": self.list_sort, - "left": self.key_left, - "right": self.key_right, - "up": self.key_up, - "down": self.key_down, - "info": self.key_info, - "menu": self.closeRecursive - }, - -1 - ) - - self.PIXMAPS_PER_PAGE = 20 - i = 0 - while i < self.PIXMAPS_PER_PAGE: - self["label" + str(i + 1)] = StaticText() - self["pixmap" + str(i + 1)] = Pixmap() - i += 1 - - self.npics = len(self.names) - # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) - self.index = 0 - self.maxentry = len(menu_list) - 1 - self.ipage = 1 - self.onLayoutFinish.append(self.openTest) - - def paintFrame(self): - try: - # If the index exceeds the maximum number of items, it returns to - # the first item - if self.index > self.maxentry: - self.index = self.minentry - self.idx = self.index - name = self.names[self.idx] - self["info"].setText(str(name)) - ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) - ipos = self.pos[ifr] - self["frame"].moveTo(ipos[0], ipos[1], 1) - self["frame"].startMoving() - except Exception as e: - print("Error in paintFrame: ", e) - - def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: - self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 - self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 - self.index = self.minentry - self.paintFrame() - - def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: - self.index -= 1 - else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page - self.openTest() - else: - self.paintFrame() - - def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: - self.index += 1 - else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page - self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage - self.index = self.minentry - self.openTest() - - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 - else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() - self.paintFrame() - - def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 - else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() - - self.paintFrame() - - def keyNumberGlobal(self, number): - number -= 1 - if len(self["menu"].list) > number: - self["menu"].setIndex(number) - self.okbuttonClick() - - def list_sort(self): - if not hasattr(self, "original_data"): - self.original_data = ( - self.names[:], - self.titles[:], - self.pics[:], - self.urls[:]) - self.sorted = False - - if self.sorted: - self.names, self.titles, self.pics, self.urls = self.original_data - self.sorted = False - self["sort"].setText(_("Sort A-Z")) - else: - self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( - self.names, self.titles, self.pics, self.urls) - self.sorted = True - self["sort"].setText(_("Sort Default")) - - self.openTest() - - def closeNonRecursive(self): - self.close(False) - - def closeRecursive(self): - self.close(True) - - def createSummary(self): - return + add_menu_item( + menu_list, + self.titles, + self.pics, + self.urls, + "CiefpsettingsPanel", + "ciefp_sp.png", + "wget -q --no-check-certificate https://raw.githubusercontent.com/ciefp/CiefpsettingsPanel/main/installer.sh -O - | /bin/sh") + add_menu_item( + menu_list, + self.titles, + self.pics, + self.urls, + "WebCamE2PrenjSF", + "ciefp_webcam.png", + "wget -q --no-check-certificate https://raw.githubusercontent.com/ciefp/WebCamE2PrenjSF/main/installer.sh -O - | /bin/sh") - def key_info(self): - self.session.open(LSinfo, " Information ") + self.initGrid(menu_list) def okbuttonClick(self): idx = self.index @@ -2922,41 +2029,11 @@ def console_closed(*args, **kwargs): else: return - def _view_log(self, answer): - if answer: - from enigma import eTimer - - def open_fc(): - from .addons.File_Commander import File_Commander - if fileExists(file_log): - self.session.open(File_Commander, file_log) - timer = eTimer() - timer.callback.append(open_fc) - timer.start(0, True) - -class ScriptInstaller(Screen): +class ScriptInstaller(LPGridScreen): def __init__(self, session, name): - Screen.__init__(self, session) - - try: - Screen.setTitle(self, _("%s") % descplug + " V." + __version__) - except BaseException: - try: - self.setTitle(_("%s") % descplug + " V." + __version__) - except BaseException: - pass - skin = join(skin_path, "LinuxsatPanel.xml") - with codecs.open(skin, "r", encoding="utf-8") as f: - self.skin = f.read() - - if isWQHD(): - self.pos = get_positions("WQHD") - elif isFHD(): - self.pos = get_positions("FHD") - elif isHD(): - self.pos = get_positions("HD") + LPGridScreen.__init__(self, session) self.name = name menu_list = [] @@ -3283,56 +2360,7 @@ def __init__(self, session, name): "serviceapp.png", 'opkg update && opkg --force-reinstall --force-overwrite install ffmpeg gstplayer exteplayer3 enigma2-plugin-systemplugins-serviceapp') - self.names = menu_list - self.sorted = False - # self.combined_data = zip(self.names, self.titles, self.pics, self.urls) - self["frame"] = MovingPixmap() - self["info"] = Label() - self["info"].setText(_("Please Wait...")) - self["sort"] = Label(_("Sort A-Z")) - self["key_red"] = Label(_("Exit")) - self["pixmap"] = Pixmap() - self["actions"] = ActionMap( - [ - "OkCancelActions", - "MenuActions", - "DirectionActions", - "NumberActions", - "ColorActions", - "EPGSelectActions", - "InfoActions" - ], - { - "ok": self.okbuttonClick, - "cancel": self.closeNonRecursive, - "exit": self.closeRecursive, - "back": self.closeNonRecursive, - "red": self.closeNonRecursive, - "0": self.list_sort, - "left": self.key_left, - "right": self.key_right, - "up": self.key_up, - "down": self.key_down, - "info": self.key_info, - "menu": self.closeRecursive - }, - -1 - ) - - self.PIXMAPS_PER_PAGE = 20 - i = 0 - while i < self.PIXMAPS_PER_PAGE: - self["label" + str(i + 1)] = StaticText() - self["pixmap" + str(i + 1)] = Pixmap() - i += 1 - - self.npics = len(self.names) - # self.npage = int(float(self.npics // self.PIXMAPS_PER_PAGE)) + 1 - self.npage = max(1, (self.npics + self.PIXMAPS_PER_PAGE - 1) // self.PIXMAPS_PER_PAGE) - self.index = 0 - self.maxentry = len(menu_list) - 1 - self.ipage = 1 - self.onLayoutFinish.append(self.openTest) + self.initGrid(menu_list) def Lcn(self, answer=None): if answer is None: @@ -3400,165 +2428,6 @@ def Checkskin(self, answer=None): MessageBox.TYPE_YESNO ) - def paintFrame(self): - try: - # If the index exceeds the maximum number of items, it returns to - # the first item - if self.index > self.maxentry: - self.index = self.minentry - self.idx = self.index - name = self.names[self.idx] - self["info"].setText(str(name)) - ifr = self.index - (self.PIXMAPS_PER_PAGE * (self.ipage - 1)) - ipos = self.pos[ifr] - self["frame"].moveTo(ipos[0], ipos[1], 1) - self["frame"].startMoving() - except Exception as e: - print("Error in paintFrame: ", e) - - def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: - self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 - self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 - self.index = self.minentry - self.paintFrame() - - def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: - self.index -= 1 - else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page - self.openTest() - else: - self.paintFrame() - - def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: - self.index += 1 - else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page - self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage - self.index = self.minentry - self.openTest() - - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 - else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() - self.paintFrame() - - def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 - else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() - - self.paintFrame() - - def keyNumberGlobal(self, number): - number -= 1 - if len(self["menu"].list) > number: - self["menu"].setIndex(number) - self.okbuttonClick() - - def list_sort(self): - if not hasattr(self, "original_data"): - self.original_data = ( - self.names[:], - self.titles[:], - self.pics[:], - self.urls[:]) - self.sorted = False - - if self.sorted: - self.names, self.titles, self.pics, self.urls = self.original_data - self.sorted = False - self["sort"].setText(_("Sort A-Z")) - else: - self.names, self.titles, self.pics, self.urls = ListSortUtility.list_sort( - self.names, self.titles, self.pics, self.urls) - self.sorted = True - self["sort"].setText(_("Sort Default")) - - self.openTest() - - def closeNonRecursive(self): - self.close(False) - - def closeRecursive(self): - self.close(True) - - def createSummary(self): - return - - def key_info(self): - self.session.open(LSinfo, " Information ") - def okbuttonClick(self): idx = self.index print("[okbuttonClick] idx", idx) @@ -3639,18 +2508,6 @@ def console_closed(*args, **kwargs): else: return - def _view_log(self, answer): - if answer: - from enigma import eTimer - - def open_fc(): - from .addons.File_Commander import File_Commander - if fileExists(file_log): - self.session.open(File_Commander, file_log) - timer = eTimer() - timer.callback.append(open_fc) - timer.start(0, True) - def askForFcl(self): self.session.openWithCallback( self.runScriptWithConsole, From 9639c6cb7e94b51340bba607f6ef6768a72e5408 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sat, 18 Jul 2026 23:26:37 +0000 Subject: [PATCH 12/18] Run all network work off the GUI thread; cache the catalog Every remaining network operation ran on the enigma main thread and froze the whole GUI: the addon catalog was fetched at panel open AND again on every category click, provider pages were scraped synchronously when a channel-list screen opened, package downloads blocked until complete, and the settings install (download + unzip + copy) locked the interface for its entire duration. - New AsyncCall helper runs a blocking function in a daemon thread and delivers the result to a callback on the main thread via eTimer. The AsyncMixin (used by LPGridScreen and addInstall) cancels pending callbacks when the screen closes, so a late result never touches a dead widget. - The catalog is fetched once per session (5-minute TTL) and warmed in the background when the panel opens, so a category click is served from cache instantly; the shared _openCategory lives in LPGridScreen. - Provider page scraping, package downloads and the settings install now show a status text and run in the background; the console or result message appears when the work is done. Verified in the stubbed enigma2 environment end-to-end: cache hit counting, panel prefetch, category click -> addInstall slice, provider page failure path, async package install -> opkg console command, and a full settings install with a real zip archive through the real shell (unzip, payload check, backup, wipe, install), plus grid navigation regression on the refactored screens. --- .../Extensions/LinuxsatPanel/plugin.py | 333 ++++++++++++------ 1 file changed, 225 insertions(+), 108 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 03e7c33..baa28d7 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -466,7 +466,88 @@ def add_menu_item(menu_list, titles, pics, urls, title, pic_name, url=""): urls.append(url) # add missing string for URL -class LPGridScreen(Screen): +class AsyncCall: + """Run a blocking function in a thread and deliver its result to a + callback on the enigma main thread (polled via eTimer).""" + + def __init__(self, fn, callback, poll_ms=100): + import threading + self._fn_result = None + self._done = False + self._cancelled = False + self._callback = callback + self._timer = eTimer() + try: + self._timer_conn = self._timer.timeout.connect(self._poll) + except BaseException: + self._timer.callback.append(self._poll) + thread = threading.Thread(target=self._run, args=(fn,)) + thread.daemon = True + thread.start() + self._timer.start(poll_ms, False) + + def _run(self, fn): + try: + self._fn_result = fn() + except Exception as e: + print("[AsyncCall] error:", e) + self._fn_result = None + self._done = True + + def cancel(self): + self._cancelled = True + try: + self._timer.stop() + except BaseException: + pass + + def _poll(self): + if self._cancelled: + return + if self._done: + self._timer.stop() + self._callback(self._fn_result) + + +class AsyncMixin: + """Lets a screen start AsyncCalls that are cancelled automatically + when the screen closes, so a late result never touches a dead + widget.""" + + def _startAsync(self, fn, callback): + if not hasattr(self, "_async_calls"): + self._async_calls = [] + self.onClose.append(self._cancelAsync) + call = AsyncCall(fn, callback) + self._async_calls.append(call) + return call + + def _cancelAsync(self): + for call in getattr(self, "_async_calls", []): + call.cancel() + + +# The addon catalog is fetched once and shared for the whole session; +# a category click hits the cache and is instant +_catalog_cache = {"data": None, "time": 0} +CATALOG_TTL = 300 + + +def get_catalog(force=False): + """Blocking fetch with a session cache - call it from a thread.""" + import time + now = time.time() + if not force and _catalog_cache["data"] is not None and \ + now - _catalog_cache["time"] < CATALOG_TTL: + return _catalog_cache["data"] + data = checkGZIP(xmlurl) + if data: + _catalog_cache["data"] = data + _catalog_cache["time"] = now + return _catalog_cache["data"] + + +class LPGridScreen(AsyncMixin, Screen): """Shared 20-tile grid engine used by all category screens.""" PIXMAPS_PER_PAGE = 20 @@ -541,6 +622,28 @@ def initGrid(self, menu_list): def okbuttonClick(self): pass + def _catalogReady(self, data): + self.data = data + + def _openCategory(self, title, name): + self["info"].setText(_("Loading...")) + + def catalog_ready(data): + self.data = data + self["info"].setText(str(name)) + if data is not None: + n1 = data.find(title, 0) + n2 = data.find("", n1) + url = data[n1:n2] + self.session.open(addInstall, url, name, None) + else: + self.session.open( + MessageBox, _("Error: No Data Find."), + MessageBox.TYPE_ERROR + ) + + self._startAsync(get_catalog, catalog_ready) + def paintFrame(self): try: # If the index exceeds the maximum number of items, it returns to @@ -715,7 +818,10 @@ def __init__(self, session): LPGridScreen.__init__(self, session) - self.data = checkGZIP(xmlurl) + # Warm the catalog cache in the background so the first + # category click is instant + self.data = None + self._startAsync(get_catalog, self._catalogReady) menu_list = [] self.titles = [] self.pics = [] @@ -1171,24 +1277,15 @@ def okbuttonContinue(self, result): else: title = self.titles[self.idx] - self.data = checkGZIP(xmlurl) - if self.data is not None: - n1 = self.data.find(title, 0) - n2 = self.data.find("", n1) - url = self.data[n1:n2] - self.session.open(addInstall, url, name, None) - else: - self.session.open( - MessageBox, _("Error: No Data Find."), - MessageBox.TYPE_ERROR - ) + self._openCategory(title, name) class LSskin(LPGridScreen): def __init__(self, session, name): LPGridScreen.__init__(self, session) - self.data = checkGZIP(xmlurl) + self.data = None + self._startAsync(get_catalog, self._catalogReady) self.name = name menu_list = [] @@ -1276,17 +1373,7 @@ def okbuttonClick(self): return name = self.names[self.idx] title = self.titles[self.idx] - self.data = checkGZIP(xmlurl) - if self.data is not None: - n1 = self.data.find(title, 0) - n2 = self.data.find("", n1) - url = self.data[n1:n2] - self.session.open(addInstall, url, name, None) - else: - self.session.open( - MessageBox, _("Error: No Data Find."), - MessageBox.TYPE_ERROR - ) + self._openCategory(title, name) class LSChannel(LPGridScreen): @@ -2696,7 +2783,7 @@ def getcl(self, config_type): str(e), type=MessageBox.TYPE_INFO, timeout=8) -class addInstall(Screen): +class addInstall(AsyncMixin, Screen): def __init__(self, session, data, name, dest): Screen.__init__(self, session) @@ -2932,7 +3019,17 @@ def okClicked(self, choice, name, url): if choice == "install": folddest = "/tmp/" + self.plug - if self.retfile(folddest): + self["info"].setText(_("Downloading %s ...") % self.plug) + + def downloaded(ok): + self["info"].setText(_("Category: ") + self.name) + if not ok: + self.session.open( + MessageBox, + _("Download failed!"), + MessageBox.TYPE_ERROR, + timeout=5) + return command = "" if ".deb" in self.plug: command = "dpkg -i '/tmp/" + self.plug + "'" @@ -2955,6 +3052,8 @@ def okClicked(self, choice, name, url): [command], closeOnSuccess=False) + self._startAsync(lambda: self.retfile(folddest), downloaded) + elif choice == "uninstall": if ".deb" in self.plug: if not has_dpkg: @@ -3005,10 +3104,17 @@ def retfile(self, dest): return False def downxmlpage(self): + # The provider page is fetched in the background; parsing happens + # on the main thread once the data is here self.downloading = False - r = make_request(self.fxml) + self["info"].setText(_("Loading list...")) + self._startAsync(lambda: make_request(self.fxml), self._xmlPageReady) + + def _xmlPageReady(self, r): + self["info"].setText(_("Category: ") + self.name) if r is None: print("Error: No data received from make_request") + self["info"].setText(_("Download page get failed ...")) return self.names = [] self.urls = [] @@ -3190,93 +3296,104 @@ def okRun(self): def okRun1(self, answer=False): dest = "/tmp/settings.zip" if answer: - global setx if self.downloading is True: idx = self["list"].getSelectionIndex() url = self.urls[idx] self.namel = "" - if "dtt" not in url.lower(): - setx = 1 - terrestrial() - if keepiptv(): - print("-----save iptv channels-----") - - fdest1 = "/tmp/unzipped" - fdest2 = "/etc/enigma2" - backup = "/tmp/settings_backup.tar.gz" - - def cleanup_tmp(): - system("rm -rf " + fdest1) - system("rm -f " + dest) - - # Download and verify BEFORE touching /etc/enigma2, so a - # failed transfer can never leave the box without channels - try: - response = requests.get(url, timeout=30) - response.raise_for_status() - with open(dest, "wb") as f: - f.write(response.content) - except Exception as e: - print("[Settings] download failed:", e) - self["info"].setText( - _("Download failed! Settings NOT installed.")) - return - - if exists(fdest1): - system("rm -rf " + fdest1) - makedirs(fdest1) - if system("unzip -o -q '%s' -d %s" % (dest, fdest1)) != 0: - print("[Settings] corrupted archive:", url) - self["info"].setText( - _("Corrupted archive! Settings NOT installed.")) - cleanup_tmp() - return - - # The channel list may live at the root of the zip or in a - # single top-level folder - srcdir = fdest1 - for root, dirs, files in walk(fdest1): - if dirs and not files: - self.namel = dirs[0] - srcdir = join(fdest1, self.namel) - break - payload = [] - for root, dirs, files in walk(srcdir): - payload.extend(files) - break - if "lamedb" not in payload and not any( - name.endswith(".tv") for name in payload): - print("[Settings] no channel list in archive:", url) - self["info"].setText( - _("No channel list in archive! Settings NOT installed.")) - cleanup_tmp() - return - - # Safety net: keep the current configuration until reboot - system("tar -czf %s -C / etc/enigma2 2>/dev/null" % backup) - - system("rm -rf /etc/enigma2/lamedb") - system("rm -rf /etc/enigma2/*.radio") - system("rm -rf /etc/enigma2/*.tv") - system("rm -rf /etc/enigma2/*.del") - if system("cp -rf '%s/'* %s" % (srcdir, fdest2)) != 0: - system("tar -xzf %s -C / 2>/dev/null" % backup) - print("[Settings] install failed, backup restored") - self["info"].setText( - _("Install failed! Previous settings restored.")) - cleanup_tmp() - return - cleanup_tmp() - title = (_("Installing %s\nPlease Wait...") % self.name) - self.session.openWithCallback( - self.yes, - lsConsole, - title=_(title), - cmdlist=["wget -qO - http://127.0.0.1/web/servicelistreload?mode=0 > /tmp/inst.txt 2>&1 &"], - closeOnSuccess=False) + self["info"].setText(_("Installing settings...")) + self._startAsync( + lambda: self._installSettings(url, dest), + self._settingsDone) else: self["info"].setText(_("Settings Not Installed ...")) + def _installSettings(self, url, dest): + """Download, verify, backup, wipe, install. Runs in a background + thread; returns an error message, or "" on success.""" + global setx + if "dtt" not in url.lower(): + setx = 1 + terrestrial() + if keepiptv(): + print("-----save iptv channels-----") + + fdest1 = "/tmp/unzipped" + fdest2 = "/etc/enigma2" + backup = "/tmp/settings_backup.tar.gz" + + def cleanup_tmp(): + system("rm -rf " + fdest1) + system("rm -f " + dest) + + # Download and verify BEFORE touching /etc/enigma2, so a + # failed transfer can never leave the box without channels + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + with open(dest, "wb") as f: + f.write(response.content) + except Exception as e: + print("[Settings] download failed:", e) + return _("Download failed! Settings NOT installed.") + + if exists(fdest1): + system("rm -rf " + fdest1) + makedirs(fdest1) + if system("unzip -o -q '%s' -d %s" % (dest, fdest1)) != 0: + print("[Settings] corrupted archive:", url) + cleanup_tmp() + return _("Corrupted archive! Settings NOT installed.") + + # The channel list may live at the root of the zip or in a + # single top-level folder + srcdir = fdest1 + for root, dirs, files in walk(fdest1): + if dirs and not files: + self.namel = dirs[0] + srcdir = join(fdest1, self.namel) + break + payload = [] + for root, dirs, files in walk(srcdir): + payload.extend(files) + break + if "lamedb" not in payload and not any( + name.endswith(".tv") for name in payload): + print("[Settings] no channel list in archive:", url) + cleanup_tmp() + return _("No channel list in archive! Settings NOT installed.") + + # Safety net: keep the current configuration until reboot + system("tar -czf %s -C / etc/enigma2 2>/dev/null" % backup) + + system("rm -rf /etc/enigma2/lamedb") + system("rm -rf /etc/enigma2/*.radio") + system("rm -rf /etc/enigma2/*.tv") + system("rm -rf /etc/enigma2/*.del") + if system("cp -rf '%s/'* %s" % (srcdir, fdest2)) != 0: + system("tar -xzf %s -C / 2>/dev/null" % backup) + print("[Settings] install failed, backup restored") + cleanup_tmp() + return _("Install failed! Previous settings restored.") + cleanup_tmp() + return "" + + def _settingsDone(self, error): + if error is None: + # the worker raised unexpectedly + self["info"].setText(_("Install failed! Settings NOT installed.")) + return + if error: + self["info"].setText(error) + return + self["info"].setText(_("Category: ") + self.name) + title = (_("Installing %s\nPlease Wait...") % self.name) + self.session.openWithCallback( + self.yes, + lsConsole, + title=_(title), + cmdlist=["wget -qO - http://127.0.0.1/web/servicelistreload?mode=0 > /tmp/inst.txt 2>&1 &"], + closeOnSuccess=False) + def pas(self, call=None): pass From 40996ce480032f248765aaad00aa25d320dc2d37 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sun, 19 Jul 2026 07:19:50 +0000 Subject: [PATCH 13/18] Add search, installed badges and startup update check - Search (GREEN button, shown in the skins next to Exit): on the main panel it searches the entire addon catalog across every category and opens the matches as a normal installable list; on the category grids it jumps straight to the first matching tile, changing page if needed. Runs through the async helper, so a cold catalog never blocks the GUI. - Installed addons are shown in green in the addon lists. The installed-package scan that message() already did on click is now a shared helper (get_installed_packages) and feeds one flag per list entry. - The startup update check, present but disconnected since its onLayoutFinish line was commented out, is enabled again: it runs in the background at panel open and shows a single clear message (version, changelog, how to update) instead of the old modal-detection logic. Uses the fixed numeric version comparison. - AsyncCall hardened against double delivery of a result. Verified in the stubbed environment: update prompt on a newer remote version, global search returning only matching catalog entries plus the no-match path, grid search jumping to the correct tile and page, installed-flag computation against the package database, and list entry rendering with the installed color. All three LinuxsatPanel.xml skins extended with the key_green label and re-validated as XML. --- .../Extensions/LinuxsatPanel/plugin.py | 181 +++++++++++++----- .../LinuxsatPanel/skins/fhd/LinuxsatPanel.xml | 2 + .../LinuxsatPanel/skins/hd/LinuxsatPanel.xml | 2 + .../skins/wqhd/LinuxsatPanel.xml | 2 + 4 files changed, 139 insertions(+), 48 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index baa28d7..fa45498 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -350,7 +350,28 @@ def __init__(self, list): self.l.setFont(0, gFont("lsat", textfont)) -def LPListEntry(name, item): +INSTALLED_COLOR = 0x39B54A + + +def get_installed_packages(): + """Lowercased names of every installed package (opkg or dpkg).""" + pkgs = set() + dpkg = exists("/var/lib/dpkg/info") + path = "/var/lib/dpkg/info" if dpkg else "/var/lib/opkg/info" + for root, dirs, files in walk(path): + for name in files: + name = name.lower() + if dpkg: + if name.endswith(".list"): + pkgs.add(name[:-5]) + else: + if name.endswith(".control"): + pkgs.add(name[:-8]) + break + return pkgs + + +def LPListEntry(name, item, installed=False): res = [(name, item)] if not fileExists(pngx): @@ -377,6 +398,11 @@ def LPListEntry(name, item): icon_x_left = 5 text_x_left = 45 + # Installed addons are shown in green + colors = {} + if installed: + colors = {"color": INSTALLED_COLOR, "color_sel": INSTALLED_COLOR} + if HALIGN == RT_HALIGN_RIGHT: res.append( MultiContentEntryPixmapAlphaTest( @@ -393,7 +419,8 @@ def LPListEntry(name, item): size=text_size, font=0, text=name, - flags=HALIGN | RT_VALIGN_CENTER)) + flags=HALIGN | RT_VALIGN_CENTER, + **colors)) else: res.append( MultiContentEntryPixmapAlphaTest( @@ -410,13 +437,17 @@ def LPListEntry(name, item): size=text_size, font=0, text=name, - flags=HALIGN | RT_VALIGN_CENTER)) + flags=HALIGN | RT_VALIGN_CENTER, + **colors)) return res -def LPshowlist(data, list): - plist = [LPListEntry(name, index) for index, name in enumerate(data)] +def LPshowlist(data, list, installed=None): + plist = [] + for index, name in enumerate(data): + flag = bool(installed and index < len(installed) and installed[index]) + plist.append(LPListEntry(name, index, flag)) list.setList(plist) @@ -502,9 +533,10 @@ def cancel(self): pass def _poll(self): - if self._cancelled: + if self._cancelled or getattr(self, "_delivered", False): return if self._done: + self._delivered = True self._timer.stop() self._callback(self._fn_result) @@ -580,6 +612,7 @@ def initGrid(self, menu_list): self["info"].setText(_("Please Wait...")) self["sort"] = Label(_("Sort A-Z")) self["key_red"] = Label(_("Exit")) + self["key_green"] = Label(_("Search")) self["pixmap"] = Pixmap() self["actions"] = ActionMap( [ @@ -597,6 +630,7 @@ def initGrid(self, menu_list): "exit": self.closeRecursive, "back": self.closeNonRecursive, "red": self.closeNonRecursive, + "green": self.key_search, "0": self.list_sort, "left": self.key_left, "right": self.key_right, @@ -622,6 +656,32 @@ def initGrid(self, menu_list): def okbuttonClick(self): pass + def key_search(self): + from Screens.VirtualKeyBoard import VirtualKeyBoard + self.session.openWithCallback( + self.searchCallback, + VirtualKeyBoard, + title=_("Search addon..."), + text="") + + def searchCallback(self, text=None): + # Default behavior: jump to the first matching tile of this grid + if not text: + return + text = text.lower() + for idx, name in enumerate(self.names): + if text in str(name).lower(): + self.ipage = idx // self.PIXMAPS_PER_PAGE + 1 + self.openTest() + self.index = idx + self.paintFrame() + return + self.session.open( + MessageBox, + _("Nothing found for '%s'") % text, + MessageBox.TYPE_INFO, + timeout=5) + def _catalogReady(self, data): self.data = data @@ -1169,53 +1229,62 @@ def __init__(self, session): "about.png") self.initGrid(menu_list) - # self.onLayoutFinish.append(self.start_check_version) + self.start_check_version() - def start_check_version(self): - self.Update = False - self.new_version, self.new_changelog, update_available = check_version( - __version__, installer_url, AgentRequest - ) - if update_available: - self.Update = True - print("A new version is available:", self.new_version) - - # Check if current screen is modal before opening the MessageBox - if self.session.current_dialog and getattr( - self.session.current_dialog, "isModal", lambda: False)(): - msg = _( - "New version available\n\nChangelog:\n\nPress the green button to start the update.") - msg = msg.replace( - "available", - "available %s" % - self.new_version) - msg = msg.replace( - "Changelog:", - "Changelog: %s" % - self.new_changelog) + def searchCallback(self, text=None): + # Search the whole catalog across every category and show the + # matches as a normal installable list + if not text: + return + text = text.lower() + self["info"].setText(_("Searching...")) + + def do_search(): + data = get_catalog() + if data is None: + return None + regex = compile(r'', DOTALL) + parts = [m.group(0) for m in regex.finditer(data) + if text in m.group(1).lower()] + return "\n".join(parts) + + def done(result): + self["info"].setText(_("Search")) + if result is None: + self.session.open( + MessageBox, _("Error: No Data Find."), + MessageBox.TYPE_ERROR) + elif not result: self.session.open( MessageBox, - msg, + _("Nothing found for '%s'") % text, MessageBox.TYPE_INFO, - timeout=5 - ) + timeout=5) else: - msg = _( - "New version available\n\nChangelog:\n\nBut not downloadable!!!") - msg = msg.replace( - "available", - "available %s" % - self.new_version) - msg = msg.replace( - "Changelog:", - "Changelog: %s" % - self.new_changelog) self.session.open( - MessageBox, - msg, - MessageBox.TYPE_INFO, - timeout=5 - ) + addInstall, result, _("Search: %s") % text, None) + + self._startAsync(do_search, done) + + def start_check_version(self): + self._startAsync( + lambda: check_version(__version__, installer_url, AgentRequest), + self._versionChecked) + + def _versionChecked(self, result): + if not result: + return + new_version, new_changelog, update_available = result + if update_available: + print("A new version is available:", new_version) + msg = _("New version %s available!\n\nChangelog:\n%s\n\nPress INFO and then the GREEN button to update.") % ( + new_version, new_changelog) + self.session.open( + MessageBox, + msg, + MessageBox.TYPE_INFO, + timeout=10 + ) else: print("No new version available.") @@ -2903,9 +2972,25 @@ def openTest(self): self.names.append(name) self.urls.append(url) - LPshowlist(self.names, self["list"]) + LPshowlist(self.names, self["list"], self.installedFlags()) # self.buttons() + def installedFlags(self): + """One flag per url: is that package already installed?""" + pkgs = get_installed_packages() + flags = [] + for url in self.urls: + fname = url[url.rfind("/") + 1:].lower() + flag = False + if ".ipk" in fname or ".deb" in fname: + plug = fname.split("_")[0] + if plug.endswith((".ipk", ".deb")): + plug = plug.rsplit(".", 1)[0] + if plug: + flag = any(p.startswith(plug) for p in pkgs) + flags.append(flag) + return flags + def buttons(self): """ if HALIGN == RT_HALIGN_RIGHT: diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/fhd/LinuxsatPanel.xml b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/fhd/LinuxsatPanel.xml index e1ca2d8..92973a8 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/fhd/LinuxsatPanel.xml +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/fhd/LinuxsatPanel.xml @@ -61,5 +61,7 @@ + + \ No newline at end of file diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/hd/LinuxsatPanel.xml b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/hd/LinuxsatPanel.xml index 9696f54..9d328c9 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/hd/LinuxsatPanel.xml +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/hd/LinuxsatPanel.xml @@ -59,5 +59,7 @@ + + \ No newline at end of file diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/wqhd/LinuxsatPanel.xml b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/wqhd/LinuxsatPanel.xml index 9df678a..20185d1 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/wqhd/LinuxsatPanel.xml +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/skins/wqhd/LinuxsatPanel.xml @@ -59,5 +59,7 @@ + + \ No newline at end of file From bfba748aa5a081acc3e6f273413e18a0db2edf1c Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sun, 19 Jul 2026 04:17:57 +0000 Subject: [PATCH 14/18] Run CI workflows on develop as well Lint (ruff, pylint, autopep8) and the translation updater only triggered on main, so develop work got no checks until release. The two auto-commit workflows also hardcoded 'origin main' in their pull/push steps; they now use the triggering branch, and the autopep8 commit step is limited to push events so pull requests are lint-only. Autotag stays main-only since tags mark releases. --- .github/workflows/flake8.yml | 8 ++++++-- .github/workflows/pylint.yml | 4 ++-- .github/workflows/ruff.yml | 1 + .github/workflows/update_translations.yml | 6 +++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index a9da50c..806942d 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -5,9 +5,11 @@ on: push: branches: - main + - develop pull_request: branches: - main + - develop jobs: pep8-check: @@ -41,11 +43,13 @@ jobs: git config --global user.name "GitHub Actions" - name: Pull latest changes + if: github.event_name == 'push' run: | - git pull origin main --no-rebase || echo "No remote changes to pull" + git pull origin ${{ github.ref_name }} --no-rebase || echo "No remote changes to pull" - name: Commit and push if changes exist + if: github.event_name == 'push' run: | git add . git diff --cached --quiet || git commit -m "Apply auto PEP8 aggressive fixes" - git push origin main || echo "Push failed (e.g. due to race condition)" + git push origin ${{ github.ref_name }} || echo "Push failed (e.g. due to race condition)" diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 125d38b..7cba48e 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ "main" ] + branches: [ "main", "develop" ] pull_request: - branches: [ "main" ] + branches: [ "main", "develop" ] jobs: build: diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index a6fd0a1..b04a70c 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - develop pull_request: jobs: diff --git a/.github/workflows/update_translations.yml b/.github/workflows/update_translations.yml index 7b16060..730a260 100644 --- a/.github/workflows/update_translations.yml +++ b/.github/workflows/update_translations.yml @@ -2,7 +2,7 @@ name: Update Translations on: push: - branches: [ main ] + branches: [ main, develop ] workflow_dispatch: jobs: @@ -49,8 +49,8 @@ jobs: if ! git diff --cached --quiet; then git commit -m "Update translations via workflow" - git pull --rebase origin main - git push origin main + git pull --rebase origin ${{ github.ref_name }} + git push origin ${{ github.ref_name }} else echo "No changes to commit" fi From 51170b35064eb07f313928c1e58b4149c8ae8542 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sun, 19 Jul 2026 04:18:20 +0000 Subject: [PATCH 15/18] Release v3.0.0 Version bump across __init__.py, CONTROL/control, installer.sh and the README badge. The installer.sh changelog (what the in-plugin update check displays) summarizes the 3.0.0 line: - Search all addons with the GREEN button - Installed addons shown in green in the lists - No more GUI freezes: all network work is asynchronous and the addon catalog is cached per session - Settings install verifies the download and keeps an automatic backup - Update check runs at panel start - Grid engine unified in one base class, dead code removed and a long list of bug fixes (see the individual commits) --- CONTROL/control | 2 +- README.md | 2 +- installer.sh | 4 ++-- .../python/Plugins/Extensions/LinuxsatPanel/__init__.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTROL/control b/CONTROL/control index 29f9517..e4a3375 100644 --- a/CONTROL/control +++ b/CONTROL/control @@ -1,5 +1,5 @@ Package: enigma2-plugin-extensions-linuxsat-panel -Version: 2.9.2 +Version: 3.0.0 Description: addons panel Section: extra Priority: optional diff --git a/README.md b/README.md index 13e76e0..7262b3e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

- Version + Version diff --git a/installer.sh b/installer.sh index 9dd3f88..dfa8f13 100644 --- a/installer.sh +++ b/installer.sh @@ -1,7 +1,7 @@ #!/bin/bash -version='2.9.2' -changelog="\n--Add WQHD PANEL" +version='3.0.0' +changelog="\n--Search all addons with GREEN button\n--Installed addons shown in green\n--No more GUI freezes (async downloads, catalog cache)\n--Safe settings install with automatic backup\n--Update check on panel start\n--Big cleanup and many bugfixes" TMPPATH=/tmp/LinuxsatPanel-install FILEPATH=/tmp/LinuxsatPanel-main.tar.gz diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py index 2d2b152..ac11ad8 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py @@ -35,7 +35,7 @@ __email__ = "ekekaz@gmail.com" __copyright__ = 'Copyright (c) 2024 Lululla' __license__ = "GPL-v2" -__version__ = "2.9.2" +__version__ = "3.0.0" def check_and_install_requests(): From 4b604342186d01c53cc20094f95e67865512c7e9 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sun, 19 Jul 2026 09:19:56 +0000 Subject: [PATCH 16/18] Fix grid navigation: UP was broken, LEFT wrap landed wrong key_up's conditions almost never matched a real cursor state: pressing UP anywhere but a page's first tile fell into a branch that jumped to the last page, and because openTest() resets the index to the page start, on page 2 the cursor just snapped to the first tile instead of moving up a row. key_left's wrap had the same openTest problem, landing on the first tile of the previous page instead of the last one. All four direction keys are rewritten with plain row/column logic: LEFT/RIGHT step one tile and wrap page-to-page at the edges; UP/DOWN move one row and cross pages keeping the current column (clamped on a partial last row). Verified exhaustively in the stubbed environment: every key from every position on grids of 6, 16, 20, 25, 37, 40 and 45 items keeps the cursor inside the visible page, full LEFT/RIGHT cycles visit every tile exactly once and wrap home, UP/DOWN are inverse operations within a page, and the reported case (UP on page 2) moves one row up. --- .../Extensions/LinuxsatPanel/plugin.py | 85 +++++++------------ 1 file changed, 31 insertions(+), 54 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index fa45498..8655f3d 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -583,6 +583,7 @@ class LPGridScreen(AsyncMixin, Screen): """Shared 20-tile grid engine used by all category screens.""" PIXMAPS_PER_PAGE = 20 + GRID_COLS = 5 def __init__(self, session): Screen.__init__(self, session) @@ -752,75 +753,51 @@ def openTest(self): self.paintFrame() def key_left(self): - # Decrement the index only if we are not at the first pixmap - if self.index >= 0: + # One step back; from the first tile of a page wrap to the last + # tile of the previous page (or the last page from page 1) + if self.index > self.minentry: self.index -= 1 else: - # If we are at the first pixmap, go back to the last pixmap of the - # last page - self.ipage = self.npage - self.index = self.npics - 1 - # Check if we need to change pages - if self.index < self.minentry: - self.ipage -= 1 - if self.ipage < 1: # If we go beyond the first page - self.ipage = self.npage - self.index = self.npics - 1 # Back to the last pixmap of the last page + self.ipage = self.npage if self.ipage == 1 else self.ipage - 1 self.openTest() - else: - self.paintFrame() + self.index = self.maxentry + self.paintFrame() def key_right(self): - # Increment the index only if we are not at the last pixmap - if self.index < self.npics - 1: + # One step forward; from the last tile of a page wrap to the + # first tile of the next page (or page 1 from the last page) + if self.index < self.maxentry: self.index += 1 else: - # If we are at the last pixmap, go back to the first pixmap of the - # first page - self.index = 0 - self.ipage = 1 - self.openTest() - # Check if we need to change pages - if self.index > self.maxentry: - self.ipage += 1 - if self.ipage > self.npage: # If we exceed the number of pages - self.index = 0 - self.ipage = 1 # Back to first page + self.ipage = 1 if self.ipage == self.npage else self.ipage + 1 self.openTest() - else: - self.paintFrame() - - def key_up(self): - if self.index == 0 and self.ipage == 1: - self.ipage = self.npage self.index = self.minentry - self.openTest() + self.paintFrame() - elif self.index >= 5 and not self.ipage == self.npage and self.index == self.minentry: - self.index -= 5 + def key_up(self): + # One row up; from the top row go to the bottom row of the + # previous page (or the last page), keeping the column + if self.index - self.GRID_COLS >= self.minentry: + self.index -= self.GRID_COLS else: - if self.ipage == self.npage and self.index == self.minentry: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage = self.npage - self.index = self.npics - 1 - self.openTest() + col = (self.index - self.minentry) % self.GRID_COLS + self.ipage = self.npage if self.ipage == 1 else self.ipage - 1 + self.openTest() + rows = (self.maxentry - self.minentry) // self.GRID_COLS + self.index = min( + self.minentry + rows * self.GRID_COLS + col, self.maxentry) self.paintFrame() def key_down(self): - if self.index <= self.maxentry - 5: - self.index += 5 + # One row down; from the bottom row go to the top row of the + # next page (or page 1), keeping the column + if self.index + self.GRID_COLS <= self.maxentry: + self.index += self.GRID_COLS else: - if self.ipage == self.npage: - self.ipage = 1 - self.index = 0 - self.openTest() - else: - self.ipage += 1 - self.index = self.minentry - self.openTest() + col = (self.index - self.minentry) % self.GRID_COLS + self.ipage = 1 if self.ipage == self.npage else self.ipage + 1 + self.openTest() + self.index = min(self.minentry + col, self.maxentry) self.paintFrame() From 8ab9357c71b281b9ac6ddc7ae6980cf34e055811 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sun, 19 Jul 2026 09:32:44 +0000 Subject: [PATCH 17/18] Hide unused tiles on a partial grid page The last page pre-filled all 20 slots with the LSS.png placeholder and then overwrote only the occupied ones, so a page with e.g. 14 entries showed 6 leftover placeholder icons. openTest now draws the occupied tiles and hides the unused pixmaps (re-showing them when a full page is displayed again); the min/max entry computation for full and last pages is unified in the process. Verified for 6/16/20/25/34/37/45-item grids: the last page shows exactly its own tiles, returning to a full page restores all 20, and the navigation invariants still hold from every position. --- .../Extensions/LinuxsatPanel/plugin.py | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py index 8655f3d..3d1eeb9 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/plugin.py @@ -722,33 +722,27 @@ def paintFrame(self): print("Error in paintFrame: ", e) def openTest(self): - if self.ipage < self.npage: - self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - - elif self.ipage == self.npage: + if self.ipage == self.npage: self.maxentry = len(self.pics) - 1 - self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE - i1 = 0 - while i1 < self.PIXMAPS_PER_PAGE: - self["label" + str(i1 + 1)].setText(" ") - self["pixmap" + str(i1 + 1) - ].instance.setPixmapFromFile(nss_pic) - i1 += 1 + else: + self.maxentry = (self.PIXMAPS_PER_PAGE * self.ipage) - 1 + self.minentry = (self.ipage - 1) * self.PIXMAPS_PER_PAGE self.npics = len(self.pics) - i = 0 - i1 = 0 - self.picnum = 0 - ln = self.maxentry - (self.minentry - 1) - while i < ln: - idx = self.minentry + i - # self["label" + str(i + 1)].setText(self.names[idx]) # this show - # label to bottom of png pixmap - pic = self.pics[idx] - if not exists(self.pics[idx]): - pic = nss_pic - self["pixmap" + str(i + 1)].instance.setPixmapFromFile(pic) - i += 1 + ln = self.maxentry - self.minentry + 1 + for i in range(self.PIXMAPS_PER_PAGE): + pixmap = self["pixmap" + str(i + 1)] + if i < ln: + idx = self.minentry + i + pic = self.pics[idx] + if not exists(pic): + pic = nss_pic + pixmap.instance.setPixmapFromFile(pic) + pixmap.show() + else: + # a partial last page hides the unused tiles instead of + # filling them with the placeholder picture + self["label" + str(i + 1)].setText(" ") + pixmap.hide() self.index = self.minentry self.paintFrame() From ffe6131b231a54b5c3c21d2cf2837704764a447b Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Sun, 19 Jul 2026 09:49:36 +0000 Subject: [PATCH 18/18] Silence ruff F401 on the requests availability probe The import inside check_and_install_requests exists only to test whether the module is installed before falling back to opkg/apt-get, so the unused-import warning is intentional. importlib.util.find_spec is not an option while the Python 2 code paths remain. --- .../enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py index ac11ad8..517ac42 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/LinuxsatPanel/__init__.py @@ -40,7 +40,7 @@ def check_and_install_requests(): try: - import requests + import requests # noqa: F401 (availability probe) return except ImportError: pass