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
+