diff --git a/app/eparser/__init__.py b/app/eparser/__init__.py index 4a688d4a..dad92073 100644 --- a/app/eparser/__init__.py +++ b/app/eparser/__init__.py @@ -2,7 +2,7 @@ from app.commons import run_task from app.settings import SettingsType from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid from .enigma.blacklist import get_blacklist, write_blacklist -from .enigma.bouquets import get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id +from .enigma.bouquets import to_bouquet_id, BouquetsWriter, BouquetsReader from .enigma.lamedb import get_services as get_enigma_services, write_services as write_enigma_services from .iptv import parse_m3u from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets @@ -27,7 +27,7 @@ def write_services(path, channels, s_type, format_version): def get_bouquets(path, s_type): if s_type is SettingsType.ENIGMA_2: - return get_enigma_bouquets(path) + return BouquetsReader(path).get() elif s_type is SettingsType.NEUTRINO_MP: return get_neutrino_bouquets(path) @@ -35,7 +35,7 @@ def get_bouquets(path, s_type): @run_task def write_bouquets(path, bouquets, s_type, force_bq_names=False): if s_type is SettingsType.ENIGMA_2: - write_enigma_bouquets(path, bouquets, force_bq_names) + BouquetsWriter(path, bouquets, force_bq_names).write() elif s_type is SettingsType.NEUTRINO_MP: write_neutrino_bouquets(path, bouquets) diff --git a/app/eparser/ecommons.py b/app/eparser/ecommons.py index 4c399f89..09f8a0bc 100644 --- a/app/eparser/ecommons.py +++ b/app/eparser/ecommons.py @@ -14,6 +14,7 @@ class BqServiceType(Enum): IPTV = "IPTV" MARKER = "MARKER" # 64 SPACE = "SPACE" # 832 [hidden marker] + ALT = "ALT" # Service with alternatives Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"]) diff --git a/app/eparser/enigma/bouquets.py b/app/eparser/enigma/bouquets.py index f3a6bf9c..8fdef722 100644 --- a/app/eparser/enigma/bouquets.py +++ b/app/eparser/enigma/bouquets.py @@ -1,76 +1,180 @@ """ Module for working with Enigma2 bouquets. """ import re from collections import Counter +from pathlib import Path from app.commons import log from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType -_TV_ROOT_FILE_NAME = "bouquets.tv" -_RADIO_ROOT_FILE_NAME = "bouquets.radio" +_TV_FILE = "bouquets.tv" +_RADIO_FILE = "bouquets.radio" _DEFAULT_BOUQUET_NAME = "favourites" -def get_bouquets(path): - return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio", - BqType.RADIO.value) - - -def write_bouquets(path, bouquets, force_bq_names=False): - """ Creating and writing bouquets files. +class BouquetsWriter: + """ Class for creating and writing bouquet files.. If "force_bq_names" then naming the files using the name of the bouquet. Some images may have problems displaying the favorites list! """ - srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n' - line = [] - pattern = re.compile("[^\\w_()]+") - m_index = [0] - s_index = [0] + _SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n' + _MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n" + _SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n" + _ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n' - for bqs in bouquets: - line.clear() - line.append("#NAME {}\n".format(bqs.name)) + def __init__(self, path, bouquets, force_bq_names=False): + self._path = path + self._bouquets = bouquets + self._force_bq_names = force_bq_names + self._marker_index = 0 + self._space_index = 0 - for index, bq in enumerate(bqs.bouquets): - bq_name = bq.name - if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)": - bq_name = _DEFAULT_BOUQUET_NAME + def write(self): + line = [] + pattern = re.compile("[^\\w_()]+") + + for bqs in self._bouquets: + line.clear() + line.append("#NAME {}\n".format(bqs.name)) + + for index, bq in enumerate(bqs.bouquets): + bq_name = bq.name + if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)": + bq_name = _DEFAULT_BOUQUET_NAME + else: + bq_name = re.sub(pattern, "_", bq.name) if self._force_bq_names else "de{0:02d}".format(index) + line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type)) + self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services) + + with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file: + file.writelines(line) + + def write_bouquet(self, path, name, services): + """ Writes single bouquet file. """ + bouquet = ["#NAME {}\n".format(name)] + for srv in services: + s_type = srv.service_type + if s_type == BqServiceType.IPTV.name: + bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip())) + elif s_type == BqServiceType.MARKER.name: + m_data = srv.fav_id.strip().split(":") + m_data[2] = self._marker_index + self._marker_index += 1 + bouquet.append(self._MARKER.format(m_data[2], m_data[-1])) + elif s_type == BqServiceType.SPACE.name: + bouquet.append(self._SPACE.format(self._space_index)) + self._space_index += 1 + elif s_type == BqServiceType.ALT.name: + services = srv.transponder + if services: + p = Path(path) + f_name = "alternatives.{}{}".format(re.sub("[<>:\"/\\|?*\-\s]", "_", srv.service), p.suffix) + alt_path = "{}/{}".format(p.parent, f_name) + bouquet.append(self._ALT.format(f_name)) + self.write_bouquet(alt_path, srv.service, services) else: - bq_name = re.sub(pattern, "_", bq.name) if force_bq_names else "de{0:02d}".format(index) - line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type)) - write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services, m_index, s_index) + data = to_bouquet_id(srv) + if srv.service: + bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service)) + else: + bouquet.append("#SERVICE {}\n".format(data)) - with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file: - file.writelines(line) + with open(path, "w", encoding="utf-8") as file: + file.writelines(bouquet) -def write_bouquet(path, name, services, current_marker, current_space): - bouquet = ["#NAME {}\n".format(name)] - marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n" - space = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n" +class BouquetsReader: + """ Class for reading and parsing bouquets. """ + _ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*") + _BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*") + _STREAM_TYPES = {"4097", "5001", "5002", "8193"} - for srv in services: - s_type = srv.service_type + __slots__ = ["_path"] - if s_type == BqServiceType.IPTV.name: - bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip())) - elif s_type == BqServiceType.MARKER.name: - m_data = srv.fav_id.strip().split(":") - m_data[2] = current_marker[0] - current_marker[0] += 1 - bouquet.append(marker.format(m_data[2], m_data[-1])) - elif s_type == BqServiceType.SPACE.name: - bouquet.append(space.format(current_space[0])) - current_space[0] += 1 - else: - data = to_bouquet_id(srv) - if srv.service: - bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service)) - else: - bouquet.append("#SERVICE {}\n".format(data)) + def __init__(self, path): + self._path = path - with open(path, "w", encoding="utf-8") as file: - file.writelines(bouquet) + def get(self): + """ Returns a tuple of TV and Radio bouquets. """ + return self.parse_bouquets(_TV_FILE, BqType.TV.value), self.parse_bouquets(_RADIO_FILE, BqType.RADIO.value) + + def parse_bouquets(self, bq_name, bq_type): + with open(self._path + bq_name, encoding="utf-8", errors="replace") as file: + lines = file.readlines() + bouquets = None + nm_sep = "#NAME" + b_names = set() + real_b_names = Counter() + + for line in lines: + if nm_sep in line: + _, _, name = line.partition(nm_sep) + bouquets = Bouquets(name.strip(), bq_type, []) + if bouquets and "#SERVICE" in line: + name = re.match(self._BQ_PAT, line) + if name: + b_name = name.group(1) + if b_name in b_names: + log("The list of bouquets contains duplicate [{}] names!".format(b_name)) + else: + b_names.add(b_name) + + rb_name, services = self.get_bouquet(self._path, b_name, bq_type) + if rb_name in real_b_names: + log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type, + rb_name)) + real_b_names[rb_name] += 1 + rb_name = "{} {}".format(rb_name, real_b_names[rb_name]) + else: + real_b_names[rb_name] = 0 + + bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None)) + else: + raise ValueError("No bouquet name found for: {}".format(line)) + + return bouquets + + @staticmethod + def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"): + """ Parsing services ids from bouquet file. """ + with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file: + chs_list = file.read() + services = [] + srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering [''] + # May come across empty[wrong] files! + if not srvs: + log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type)) + return "{} [empty]".format(bq_name), services + + bq_name = srvs.pop(0) + + for num, srv in enumerate(srvs, start=1): + srv_data = srv.strip().split(":") + s_type = srv_data[1] + if s_type == "64": + m_data, sep, desc = srv.partition("#DESCRIPTION") + services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num)) + elif s_type == "832": + m_data, sep, desc = srv.partition("#DESCRIPTION") + services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num)) + elif s_type == "134": + alt = re.match(BouquetsReader._ALT_PAT, srv) + if alt: + alt_name, alt_type = alt.group(1), alt.group(2) + alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives") + services.append(BouquetService(alt_bq_name, BqServiceType.ALT, srv.lstrip(), tuple(alt_srvs))) + elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")): + stream_data, sep, desc = srv.partition("#DESCRIPTION") + desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip() + services.append(BouquetService(desc, BqServiceType.IPTV, srv, num)) + else: + fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6]) + name = None + if len(srv_data) == 12: + name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION") + services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num)) + + return bq_name.lstrip("#NAME").strip(), services def to_bouquet_id(srv): @@ -82,82 +186,5 @@ def to_bouquet_id(srv): return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id) -def get_bouquet(path, bq_name, bq_type): - """ Parsing services ids from bouquet file. """ - with open(path + "userbouquet.{}.{}".format(bq_name, bq_type), encoding="utf-8", errors="replace") as file: - chs_list = file.read() - services = [] - srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering [''] - # May come across empty[wrong] files! - if not srvs: - log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type)) - return "{} [empty]".format(bq_name), services - - bq_name = srvs.pop(0) - stream_types = {"4097", "5001", "5002", "8193"} - - for num, srv in enumerate(srvs, start=1): - srv_data = srv.strip().split(":") - if srv_data[1] == "64": - m_data, sep, desc = srv.partition("#DESCRIPTION") - services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num)) - elif srv_data[1] == "832": - m_data, sep, desc = srv.partition("#DESCRIPTION") - services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num)) - elif srv_data[10].startswith(("http", "rtsp")) or srv_data[0].strip() in stream_types: - stream_data, sep, desc = srv.partition("#DESCRIPTION") - desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip() - services.append(BouquetService(desc, BqServiceType.IPTV, srv, num)) - else: - fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6]) - name = None - if len(srv_data) == 12: - name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION") - services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num)) - - return bq_name.lstrip("#NAME").strip(), services - - -def parse_bouquets(path, bq_name, bq_type): - with open(path + bq_name, encoding="utf-8", errors="replace") as file: - lines = file.readlines() - bouquets = None - nm_sep = "#NAME" - bq_pattern = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*") - b_names = set() - real_b_names = Counter() - - for line in lines: - if nm_sep in line: - _, _, name = line.partition(nm_sep) - bouquets = Bouquets(name.strip(), bq_type, []) - if bouquets and "#SERVICE" in line: - name = re.match(bq_pattern, line) - if name: - b_name = name.group(1) - if b_name in b_names: - log("The list of bouquets contains duplicate [{}] names!".format(b_name)) - else: - b_names.add(b_name) - - rb_name, services = get_bouquet(path, b_name, bq_type) - if rb_name in real_b_names: - log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type, rb_name)) - real_b_names[rb_name] += 1 - rb_name = "{} {}".format(rb_name, real_b_names[rb_name]) - else: - real_b_names[rb_name] = 0 - - bouquets[2].append(Bouquet(name=rb_name, - type=bq_type, - services=services, - locked=None, - hidden=None)) - else: - raise ValueError("No bouquet name found for: {}".format(line)) - - return bouquets - - if __name__ == "__main__": pass diff --git a/app/ui/imports.py b/app/ui/imports.py index d4041b1b..595751dd 100644 --- a/app/ui/imports.py +++ b/app/ui/imports.py @@ -2,9 +2,8 @@ from contextlib import suppress from pathlib import Path from app.commons import run_idle, log -from app.eparser import get_bouquets, get_services +from app.eparser import get_bouquets, get_services, BouquetsReader from app.eparser.ecommons import BqType, BqServiceType, Bouquet -from app.eparser.enigma.bouquets import get_bouquet from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets from app.settings import SettingsType from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message @@ -67,7 +66,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa def get_enigma2_bouquet(path): path, sep, f_name = path.rpartition("userbouquet.") name, sep, suf = f_name.rpartition(".") - bq = get_bouquet(path, name, suf) + bq = BouquetsReader.get_bouquet(path, name, suf) bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None) return bouquet diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index eb6fba60..3ba6574d 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -254,6 +254,10 @@ class Application(Gtk.Application): self._signal_level_bar.bind_property("visible", builder.get_object("record_button"), "visible") self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4) self._receiver_info_box.bind_property("visible", self._signal_box, "visible") + # Alternatives + self._alt_model = builder.get_object("alt_list_store") + self._alt_revealer = builder.get_object("alt_revealer") + self._alt_revealer.bind_property("visible", self._alt_revealer, "reveal-child") # Control self._control_button = builder.get_object("control_button") self._receiver_info_box.bind_property("visible", self._control_button, "visible") @@ -1497,6 +1501,7 @@ class Application(Gtk.Application): def update_data(self, data_path, callback=None): self._profile_combo_box.set_sensitive(False) + self._alt_revealer.set_visible(False) self._wait_dialog.show() yield from self.clear_current_data() @@ -1620,6 +1625,10 @@ class Application(Gtk.Application): srv = Service(None, None, icon, srv.name, locked, None, None, s_type.name, self._picons.get(picon_id, None), picon_id, *agr, data_id, fav_id, None) self._services[fav_id] = srv + elif s_type is BqServiceType.ALT: + srv = Service(None, None, None, srv.name, locked, None, None, s_type.name, + None, None, *agr, fav_id, fav_id, srv.num) + self._services[fav_id] = srv elif srv.name: extra_services[fav_id] = srv.name services.append(fav_id) @@ -1727,10 +1736,12 @@ class Application(Gtk.Application): Column.BQ_HIDDEN, Column.BQ_TYPE) bq_id = "{}:{}".format(bq_name, bq_type) favs = self._bouquets[bq_id] - ex_s = self._extra_bouquets.get(bq_id) + ex_s = self._extra_bouquets.get(bq_id, None) bq_s = list(filter(None, [self._services.get(f_id, None) for f_id in favs])) + if profile is SettingsType.ENIGMA_2: - bq_s = list(map(lambda s: s._replace(service=ex_s.get(s.fav_id, None) if ex_s else None), bq_s)) + bq_s = self.get_enigma_bq_services(bq_s, ex_s) + bq = Bouquet(bq_name, bq_type, bq_s, locked, hidden) bqs.append(bq) if len(b_path) == 1: @@ -1745,7 +1756,7 @@ class Application(Gtk.Application): services = [Service(*row[: Column.SRV_TOOLTIP]) for row in services_model] write_services(path, services, profile, self.get_format_version() if profile is SettingsType.ENIGMA_2 else 0) yield True - # removing bouquet files + if profile is SettingsType.ENIGMA_2: # blacklist write_blacklist(path, self._blacklist) @@ -1755,6 +1766,20 @@ class Application(Gtk.Application): if callback: callback() + def get_enigma_bq_services(self, services, ext_services): + """ Preparing a list of services for the Enigma2 bouquet. """ + s_list = [] + for srv in services: + if srv.service_type == BqServiceType.ALT.name: + # Alternatives to service in a bouquet. + alts = list(map(lambda s: s._replace(service=None), + filter(None, [self._services.get(s.data, None) for s in srv.transponder or []]))) + s_list.append(srv._replace(transponder=alts)) + else: + # Extra names for service in bouquet. + s_list.append(srv._replace(service=ext_services.get(srv.fav_id, None) if ext_services else None)) + return s_list + def on_new_configuration(self, action, value=None): """ Creates new empty configuration """ if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL: @@ -1784,11 +1809,24 @@ class Application(Gtk.Application): yield True def on_fav_selection(self, model, path, column): - if self._control_box and self._control_box.update_epg: - ref = self.get_service_ref(path) - if not ref: - return - self._control_box.on_service_changed(ref) + row = model[path][:] + if row[Column.FAV_TYPE] == BqServiceType.ALT.name: + self._alt_model.clear() + srv = self._services.get(row[Column.FAV_ID], None) + if srv: + for index, s in enumerate(srv[-1] or [], start=1): + srv = self._services.get(s.data, None) + if srv: + self._alt_model.append((index, srv.service, srv.service_type, srv.pos)) + self._alt_revealer.set_visible(True) + else: + self._alt_revealer.set_visible(False) + + if self._control_box and self._control_box.update_epg: + ref = self.get_service_ref(path) + if not ref: + return + self._control_box.on_service_changed(ref) def on_services_selection(self, model, path, column): self.update_service_bar(model, path) diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 85ad9d90..46390a2e 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -33,6 +33,18 @@ Author: Dmitriy Yefremov + + + + + + + + + + + + True False @@ -2397,185 +2409,319 @@ Author: Dmitriy Yefremov - + True False 2 - in + vertical + 2 - + True - True - fav_list_store - False - 2 - True - both - 9 - True - - - - - - - - - - - - - - - multiple - - + False + in - - True - 25 - Num - True - 0.5 - - - - 0.20000000298023224 - 5 - 5 + + True + True + fav_list_store + False + 2 + True + both + 9 + True + + + + + + + + + + + + + + + multiple - - 10 - 0 - 0 - - - - - - True - 50 - Service - True - True - 0.5 - - - 2 + + True + 25 + Num + True + 0.5 + + + + 0.20000000298023224 + 5 + 5 + + + 10 + 0 + 0 + + - - 10 - 8 - - - 2 + + True + 50 + Service + True + True + 0.5 + + + + 2 + + + 10 + 8 + + + + + 2 + + + 10 + 1 + + + + + end + 25 + + + 10 + 2 + + + + + 2 + + + 10 + 3 + + + + + 2 + + + 10 + 4 + + - - 10 - 1 - - - end - 25 + + True + 25 + Type + True + True + 0.5 + + + + 0.50999999046325684 + + + 10 + 5 + + - - 10 - 2 - - - 2 + + 25 + Pos + True + True + 0.5 + + + + 0.50999999046325684 + + + 10 + 6 + + - - 10 - 3 - - - 2 + + False + fav_id + + + + 7 + + - - 10 - 4 - - - - - - True - 25 - Type - True - True - 0.5 - - - 0.50999999046325684 + + False + extra + + + + 9 + + + + + - - 10 - 5 - - - - - - - 25 - Pos - True - True - 0.5 - - - - 0.50999999046325684 - - - 10 - 6 - - - - - - - False - fav_id - - - - 7 - - - - - - - False - extra - - - - 9 - - - - + + True + True + 0 + + + + + False + slide-up + + + 200 + True + False + vertical + 2 + + + True + False + Alternatives + + + + + + False + True + 0 + + + + + True + True + in + + + True + True + alt_list_store + False + 0 + + + + + + 25 + Num + 0.5 + + + 5 + 5 + + + 0 + + + + + + + True + 50 + Service + True + 0.5 + + + + 1 + + + + + + + True + Type + True + 0.5 + + + 0.50999999046325684 + + + 2 + + + + + + + 25 + Pos + 0.5 + + + 0.50999999046325684 + + + 3 + + + + + + + + + True + True + 1 + + + + + + + False + True + 1 +