diff --git a/AutoBouquetsMaker/custom/hd_sat_192_sky_deutschland_CustomLCN.xml b/AutoBouquetsMaker/custom/hd_sat_192_sky_deutschland_CustomLCN.xml
deleted file mode 100644
index 0789c078..00000000
--- a/AutoBouquetsMaker/custom/hd_sat_192_sky_deutschland_CustomLCN.xml
+++ /dev/null
@@ -1,119 +0,0 @@
-
- yes
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/AutoBouquetsMaker/lib/dvbreader.c b/AutoBouquetsMaker/lib/dvbreader.c
index 6adf0aaf..4d1db1ef 100644
--- a/AutoBouquetsMaker/lib/dvbreader.c
+++ b/AutoBouquetsMaker/lib/dvbreader.c
@@ -1380,6 +1380,59 @@ PyObject *ss_read_fastscan(PyObject *self, PyObject *args) {
return ret;
}
+// Sky Deutschland / Sky Q channel-list carousel (table 0x9E, variable_id 0x0055 on PID 0x08AE).
+// One section carries a raw chunk of a larger NDS-ARCHIVE blob; the Python layer reassembles
+// all sections of the same version_number and parses the archive.
+PyObject *ss_parse_header_skyq(unsigned char *data, int length)
+{
+ return Py_BuildValue("{s:i,s:i,s:i,s:i,s:i,s:i,s:i}",
+ "table_id", data[0],
+ "section_length", length,
+ "variable_id", (data[3] << 8) | data[4],
+ "version_number", (data[5] >> 1) & 0x1F,
+ "current_next_indicator", data[5] & 0x01,
+ "section_number", data[6],
+ "last_section_number", data[7]);
+}
+
+PyObject *ss_read_skyq(PyObject *self, PyObject *args) {
+ PyObject *content = NULL, *header = NULL;
+ unsigned char buffer[4096], table_id;
+ int fd;
+
+ if (!PyArg_ParseTuple(args, "ib", &fd, &table_id))
+ return Py_None;
+
+ int size = read(fd, buffer, sizeof(buffer));
+ if (size < 14)
+ return Py_None;
+
+ if (buffer[0] != table_id)
+ return Py_None;
+
+ int section_length = ((buffer[1] & 0x0f) << 8) | buffer[2];
+
+ if (size != section_length + 3)
+ return Py_None;
+
+ // payload: bytes 13..section_length-2 inclusive (excludes 8-byte section header,
+ // 5-byte NDS sub-header and trailing 4-byte CRC32)
+ int payload_len = section_length - 14;
+ if (payload_len < 0)
+ return Py_None;
+
+ header = ss_parse_header_skyq(buffer, section_length);
+ content = PyBytes_FromStringAndSize((const char *)(buffer + 13), payload_len);
+
+ if (!header || !content)
+ return Py_None;
+
+ PyObject *ret = Py_BuildValue("{s:O,s:O}", "header", header, "content", content);
+ Py_DECREF(header);
+ Py_DECREF(content);
+ return ret;
+}
+
PyObject *ss_read_nit(PyObject *self, PyObject *args) {
PyObject *content = NULL, *header = NULL;
unsigned char buffer[4096], table_id_current, table_id_other;
@@ -1419,6 +1472,7 @@ static PyMethodDef dvbreaderMethods[] = {
{ "read_nit", ss_read_nit, METH_VARARGS },
{ "read_sdt", ss_read_sdt, METH_VARARGS },
{ "read_fastscan", ss_read_fastscan, METH_VARARGS },
+ { "read_skyq", ss_read_skyq, METH_VARARGS },
{ "read_ts", ss_read_ts, METH_VARARGS },
{ NULL, NULL }
};
diff --git a/AutoBouquetsMaker/providers/sat_130_ncplus.xml b/AutoBouquetsMaker/providers/sat_130_ncplus.xml
index 206534c3..e4eeb40f 100644
--- a/AutoBouquetsMaker/providers/sat_130_ncplus.xml
+++ b/AutoBouquetsMaker/providers/sat_130_ncplus.xml
@@ -7,7 +7,7 @@
frequency="10719000"
symbol_rate="27500000"
polarization="1"
- fec_inner="5"
+ fec_inner="4"
inversion="2"
system="1"
modulation="2"
@@ -21,7 +21,7 @@
- NC+
+ CANAL+
diff --git a/AutoBouquetsMaker/providers/sat_192_sky_deutschland.xml b/AutoBouquetsMaker/providers/sat_192_sky_deutschland.xml
index 656f9ea2..b645b55c 100644
--- a/AutoBouquetsMaker/providers/sat_192_sky_deutschland.xml
+++ b/AutoBouquetsMaker/providers/sat_192_sky_deutschland.xml
@@ -1,7 +1,7 @@
Sky Deutschland
dvbs
- nolcn
+ skyde
+
+ Satellit HD
+
+
+
+ Sky Showcase HD
+ HISTORY Channel HD
+
@@ -41,6 +54,12 @@ for number in service["numbers"]:
if service["service_name"] in blacklist:
skip.skip = True
+# Sky DE sometimes wraps service names in DVB emphasis bytes (0x86/0x87),
+# which defeats startswith() — use a substring check so we catch e.g.
+# "STEST1" too.
+if "STEST" in service["service_name"]:
+ skip.skip = True
+
]]>
diff --git a/AutoBouquetsMaker/src/scanner/dvbscanner.py b/AutoBouquetsMaker/src/scanner/dvbscanner.py
index 72c2cc39..acafe420 100644
--- a/AutoBouquetsMaker/src/scanner/dvbscanner.py
+++ b/AutoBouquetsMaker/src/scanner/dvbscanner.py
@@ -4,13 +4,102 @@
import dvbreader
import datetime
+import re
+import struct
import time
from Components.config import config
+# --- Sky Q / Sky Deutschland NDS-ARCHIVE parser -------------------------------
+# The channel-list carousel on 19.2E (PID 0x08AE, table 0x9E, variable_id 0x0055)
+# delivers an NDS-ARCHIVE container holding a nested NDS-ARCHIVE of per-bouquet
+# .txt files. Each bouquet file maps LCN to (ONID, TSID, SID) using lines of
+# the form "==dvb://..;;".
+
+_SKYQ_LINE_RE = re.compile(rb"^\s*(\d+)=(\d+)=dvb://([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)")
+_SKYQ_NDS_MAGIC = b"NDS-ARCHIVE\x00"
+
+
+def _skyq_parse_archive(data, magic_off, archive_base):
+ """Return list of (name, bytes) for one NDS-ARCHIVE container.
+
+ magic_off absolute offset of the 'NDS-ARCHIVE\\0' magic inside data
+ archive_base origin for the file-offset field in each entry; 0 for the
+ outer archive, magic_off - 6 empirically for nested
+ archives (a 6-byte pre-magic header we do not decode).
+ """
+ if data[magic_off:magic_off + 12] != _SKYQ_NDS_MAGIC:
+ raise ValueError("NDS-ARCHIVE magic not found at 0x%x" % magic_off)
+ off = magic_off + 12
+ # 18-byte post-magic header: FF FF | 32-bit unknown | 16-bit unknown | uint32 nfiles | 6 pad
+ nfiles = struct.unpack(">I", data[off + 8:off + 12])[0]
+ off += 18
+ entries = []
+ for _ in range(nfiles):
+ if off + 23 > len(data):
+ break
+ entry_len = data[off]
+ file_off = struct.unpack(">I", data[off + 5:off + 9])[0]
+ file_size = struct.unpack(">I", data[off + 9:off + 13])[0]
+ fname_len = data[off + 22]
+ fname = data[off + 23:off + 23 + fname_len].rstrip(b"\x00").decode("latin-1", "replace")
+ start = archive_base + file_off
+ entries.append((fname, data[start:start + file_size]))
+ off += entry_len
+ return entries
+
+
+def skyq_extract_bouquet_file(archive_data, bouquet_file):
+ """Locate a given '.txt' filename inside the Sky Q NDS-ARCHIVE blob.
+ Raises KeyError listing the available channel-list files on miss."""
+ magic_positions = [m.start() for m in re.finditer(_SKYQ_NDS_MAGIC, archive_data)]
+ if len(magic_positions) < 2:
+ raise RuntimeError("Malformed NDS-ARCHIVE: expected at least 2 magics, got %d" % len(magic_positions))
+ root = _skyq_parse_archive(archive_data, magic_positions[0], archive_base=0)
+ nested = None
+ for name, body in root:
+ if name.endswith("sky_de_configuration.nsa"):
+ nested = body
+ break
+ if nested is None:
+ raise RuntimeError("sky_de_configuration.nsa not found in NDS-ARCHIVE")
+ nested_magic = nested.find(_SKYQ_NDS_MAGIC)
+ if nested_magic < 0:
+ raise RuntimeError("Nested NDS-ARCHIVE magic not found")
+ inner = _skyq_parse_archive(nested, nested_magic, archive_base=nested_magic - 6)
+ for name, body in inner:
+ if name == bouquet_file:
+ return body
+ available = sorted(n for n, _ in inner if "channels-" in n and not n.endswith(".headers"))
+ raise KeyError("bouquet_file %r not in archive; available: %s" % (bouquet_file, available))
+
+
+def skyq_parse_channel_list(body):
+ """Parse a bouquet .txt into {(onid, tsid, sid): logical_channel_number}.
+ LCN 0 entries (data/interactive slots) are skipped."""
+ out = {}
+ for line in body.split(b"\r\n"):
+ m = _SKYQ_LINE_RE.match(line)
+ if not m:
+ continue
+ lcn = int(m.group(1))
+ if lcn == 0:
+ continue
+ onid = int(m.group(3), 16)
+ tsid = int(m.group(4), 16)
+ sid = int(m.group(5), 16)
+ key = (onid, tsid, sid)
+ if key not in out:
+ out[key] = lcn
+ return out
+
+# --- end Sky Q NDS-ARCHIVE parser ---------------------------------------------
+
+
class DvbScanner():
TIMEOUT_SEC = 20
SDT_TIMEOUT = 20
+ SKYQ_TIMEOUT = 60
VIDEO_ALLOWED_TYPES = [1, 4, 5, 17, 22, 24, 25, 27, 31, 135]
AUDIO_ALLOWED_TYPES = [2, 10]
@@ -34,6 +123,9 @@ def __init__(self):
self.bat_table_id = 0x4a
self.fastscan_pid = 0x00
self.fastscan_table_id = 0x00
+ self.skyq_pid = 0x08ae
+ self.skyq_table_id = 0x9e
+ self.skyq_variable_id = 0x0055
self.ignore_visible_service_flag = 0
self.extra_debug = config.autobouquetsmaker.extra_debug.value
self.namespace_complete = not (config.usage.subnetwork.value if hasattr(config.usage, "subnetwork") else True) # config.usage.subnetwork not available in all images
@@ -103,6 +195,18 @@ def setFastscanTableId(self, value):
self.fastscan_table_id = value
print("[ABM-DvbScanner] Fastscan table id: 0x%x" % self.fastscan_table_id, file=log)
+ def setSkyqPid(self, value):
+ self.skyq_pid = value
+ print("[ABM-DvbScanner] SkyQ pid: 0x%x" % self.skyq_pid, file=log)
+
+ def setSkyqTableId(self, value):
+ self.skyq_table_id = value
+ print("[ABM-DvbScanner] SkyQ table id: 0x%x" % self.skyq_table_id, file=log)
+
+ def setSkyqVariableId(self, value):
+ self.skyq_variable_id = value
+ print("[ABM-DvbScanner] SkyQ variable id: 0x%x" % self.skyq_variable_id, file=log)
+
def setVisibleServiceFlagIgnore(self, value):
self.ignore_visible_service_flag = value
print("[ABM-DvbScanner] Ignore visible service flag: %d" % self.ignore_visible_service_flag, file=log)
@@ -727,6 +831,117 @@ def updateAndReadServicesLCN(self, transponders, servicehacks, TSID_ONID_list, l
"radio": radio_services
}
+ def readSkyQArchive(self):
+ """Reassemble the Sky Q / Sky Deutschland channel-list NDS-ARCHIVE.
+ Returns raw archive bytes, or None on timeout / incomplete delivery."""
+ print("[ABM-DvbScanner] Reading Sky Q channel-list carousel (PID 0x%x, table 0x%x, var_id 0x%x)..."
+ % (self.skyq_pid, self.skyq_table_id, self.skyq_variable_id), file=log)
+
+ fd = dvbreader.open(self.demuxer_device, self.skyq_pid, self.skyq_table_id, 0xff, self.frontend)
+ if fd < 0:
+ print("[ABM-DvbScanner] Cannot open the demuxer", file=log)
+ return None
+
+ version = None
+ expected = None
+ received = {}
+
+ timeout = datetime.datetime.now()
+ timeout += datetime.timedelta(0, self.SKYQ_TIMEOUT)
+ while True:
+ if datetime.datetime.now() > timeout:
+ print("[ABM-DvbScanner] Timed out reading Sky Q archive", file=log)
+ break
+
+ section = dvbreader.read_skyq(fd, self.skyq_table_id)
+ if section is None:
+ time.sleep(0.1)
+ continue
+
+ header = section["header"]
+ if header["variable_id"] != self.skyq_variable_id:
+ continue
+
+ ver = header["version_number"]
+ if version is None:
+ version = ver
+ expected = header["last_section_number"] + 1
+ elif ver != version:
+ # carousel updated mid-read - start over
+ version = ver
+ expected = header["last_section_number"] + 1
+ received = {}
+
+ sn = header["section_number"]
+ if sn in received:
+ continue
+ received[sn] = section["content"]
+ if len(received) == expected:
+ break
+
+ dvbreader.close(fd)
+
+ if not expected or len(received) != expected:
+ print("[ABM-DvbScanner] Incomplete Sky Q archive (%d/%s sections)"
+ % (len(received), expected), file=log)
+ return None
+
+ blob = b"".join(received[n] for n in range(expected))
+ print("[ABM-DvbScanner] Sky Q archive v%d assembled, %d bytes, %d sections"
+ % (version, len(blob), expected), file=log)
+ return blob
+
+ def updateAndReadServicesSKYDE(self, bouquet_file, transponders, servicehacks, provider_config, lcn_overrides=None):
+ print("[ABM-DvbScanner] Reading services (SKYDE, bouquet_file=%s)..." % bouquet_file, file=log)
+
+ blob = self.readSkyQArchive()
+ if not blob:
+ return {"video": {}, "radio": {}}
+
+ try:
+ body = skyq_extract_bouquet_file(blob, bouquet_file)
+ except (KeyError, RuntimeError, ValueError) as e:
+ print("[ABM-DvbScanner] Sky Q archive: %s" % e, file=log)
+ return {"video": {}, "radio": {}}
+
+ lcn_map = skyq_parse_channel_list(body)
+ print("[ABM-DvbScanner] Sky Q channel list %r: %d entries" % (bouquet_file, len(lcn_map)), file=log)
+
+ # Provider-level manual overrides — for services Sky broadcasts but does
+ # not list in the carousel (e.g. after a transponder re-allocation that
+ # Sky has not yet propagated to the channel-list .txt). Applied only
+ # when the carousel itself has no entry for the same (ONID, TSID, SID).
+ if lcn_overrides:
+ injected = 0
+ for ov in lcn_overrides:
+ key = (ov["onid"], ov["tsid"], ov["sid"])
+ if key not in lcn_map:
+ lcn_map[key] = ov["lcn"]
+ injected += 1
+ if injected:
+ print("[ABM-DvbScanner] Injected %d provider LCN override(s) for services missing from carousel" % injected, file=log)
+ if not lcn_map:
+ return {"video": {}, "radio": {}}
+
+ # Convert into the shape the shared LCN path expects
+ TSID_ONID_list = []
+ logical_channel_number_dict = {}
+ for (onid, tsid, sid), lcn in lcn_map.items():
+ key = "%x:%x" % (tsid, onid)
+ if key not in TSID_ONID_list:
+ TSID_ONID_list.append(key)
+ servicekey = "%x:%x:%x" % (tsid, onid, sid)
+ logical_channel_number_dict[servicekey] = {
+ "transport_stream_id": tsid,
+ "original_network_id": onid,
+ "service_id": sid,
+ "logical_channel_number": lcn,
+ }
+
+ return self.updateAndReadServicesLCN(
+ transponders, servicehacks, TSID_ONID_list,
+ logical_channel_number_dict, {}, "skyde", bouquet_file, provider_config)
+
def updateAndReadServicesFastscan(self, transponders, servicehacks, logical_channel_number_dict, provider_config):
print("[ABM-DvbScanner] Reading services (fastscan)...", file=log)
diff --git a/AutoBouquetsMaker/src/scanner/manager.py b/AutoBouquetsMaker/src/scanner/manager.py
index dc1bc915..08d6b29a 100644
--- a/AutoBouquetsMaker/src/scanner/manager.py
+++ b/AutoBouquetsMaker/src/scanner/manager.py
@@ -185,7 +185,7 @@ def read(self, provider_config, providers, motorised):
# providers = Providers().read()
if provider_key in providers:
- if bouquet_key in providers[provider_key]["bouquets"] or providers[provider_key]["protocol"] != "sky":
+ if bouquet_key in providers[provider_key]["bouquets"] or providers[provider_key]["protocol"] not in ("sky", "skyde"):
scanner = DvbScanner()
scanner.setAdapter(self.adapter)
scanner.setDemuxer(self.demuxer)
@@ -261,7 +261,28 @@ def read(self, provider_config, providers, motorised):
self.serviceVideoRead += len(list(self.services[provider_key]["video"].keys()))
self.serviceAudioRead += len(list(self.services[provider_key]["radio"].keys()))
+ elif providers[provider_key]["protocol"] == "skyde":
+ scanner.setSdtPid(providers[provider_key]["transponder"]["sdt_pid"])
+ scanner.setSdtCurrentTableId(providers[provider_key]["transponder"]["sdt_current_table_id"])
+ scanner.setSdtOtherTableId(providers[provider_key]["transponder"]["sdt_other_table_id"])
+ scanner.setSkyqPid(providers[provider_key]["transponder"]["skyq_pid"])
+ scanner.setSkyqTableId(providers[provider_key]["transponder"]["skyq_table_id"])
+ scanner.setSkyqVariableId(providers[provider_key]["transponder"]["skyq_variable_id"])
+
+ scanner.updateTransponders(self.transponders, False)
+ bouquet = providers[provider_key]["bouquets"][bouquet_key]
+ self.services[provider_key] = scanner.updateAndReadServicesSKYDE(
+ bouquet["bouquet_file"], self.transponders,
+ providers[provider_key]["servicehacks"], provider_config,
+ lcn_overrides=providers[provider_key].get("lcn_overrides", []))
+
+ ret = len(list(self.services[provider_key]["video"].keys())) > 0 or len(list(self.services[provider_key]["radio"].keys())) > 0
+
+ self.serviceVideoRead += len(list(self.services[provider_key]["video"].keys()))
+ self.serviceAudioRead += len(list(self.services[provider_key]["radio"].keys()))
+
elif providers[provider_key]["protocol"] == "freesat":
+
scanner.setSdtPid(providers[provider_key]["transponder"]["sdt_pid"])
scanner.setSdtCurrentTableId(providers[provider_key]["transponder"]["sdt_current_table_id"])
scanner.setSdtOtherTableId(providers[provider_key]["transponder"]["sdt_other_table_id"])
diff --git a/AutoBouquetsMaker/src/scanner/providers.py b/AutoBouquetsMaker/src/scanner/providers.py
index 5cdbc3ef..e3f101c2 100644
--- a/AutoBouquetsMaker/src/scanner/providers.py
+++ b/AutoBouquetsMaker/src/scanner/providers.py
@@ -18,7 +18,7 @@
class Providers():
- VALID_PROTOCOLS = ("fastscan", "freesat", "lcn", "lcn2", "lcnbat", "lcnbat2", "nolcn", "sky", "vmuk", "vmuk2")
+ VALID_PROTOCOLS = ("fastscan", "freesat", "lcn", "lcn2", "lcnbat", "lcnbat2", "nolcn", "sky", "skyde", "vmuk", "vmuk2")
PROVIDERS_DIR = os.path.dirname(__file__) + "/../providers"
USER_PROVIDERS_DIR = os.path.realpath(resolveFilename(SCOPE_CONFIG)) + "/AutoBouquetsMaker/providers"
@@ -100,6 +100,7 @@ def read(self):
provider["ignore_visible_service_flag"] = 0
provider["custom_list"] = False
provider["show_fta_options"] = True
+ provider["lcn_overrides"] = []
if dom.documentElement.nodeType == dom.documentElement.ELEMENT_NODE and dom.documentElement.tagName == "provider":
for node in dom.documentElement.childNodes:
if node.nodeType != node.ELEMENT_NODE:
@@ -135,6 +136,9 @@ def read(self):
transponder["bat_table_id"] = 0x4a
transponder["fastscan_pid"] = 0x00 # no default value
transponder["fastscan_table_id"] = 0x00 # no default value
+ transponder["skyq_pid"] = 0x08ae # Sky Q channel-list carousel (PID on 19.2E Sky DE)
+ transponder["skyq_table_id"] = 0x9e
+ transponder["skyq_variable_id"] = 0x0055
transponder["system"] = eDVBFrontendParametersSatellite.System_DVB_S
transponder["polarization"] = eDVBFrontendParametersSatellite.Polarisation_Horizontal
transponder["fec_inner"] = eDVBFrontendParametersSatellite.FEC_Auto
@@ -197,12 +201,18 @@ def read(self):
transponder["fastscan_pid"] = int(node.attributes.item(i).value, 16)
elif node.attributes.item(i).name == "fastscan_table_id":
transponder["fastscan_table_id"] = int(node.attributes.item(i).value, 16)
+ elif node.attributes.item(i).name == "skyq_pid":
+ transponder["skyq_pid"] = int(node.attributes.item(i).value, 16)
+ elif node.attributes.item(i).name == "skyq_table_id":
+ transponder["skyq_table_id"] = int(node.attributes.item(i).value, 16)
+ elif node.attributes.item(i).name == "skyq_variable_id":
+ transponder["skyq_variable_id"] = int(node.attributes.item(i).value, 16)
elif node.attributes.item(i).name == "onid":
transponder["onid"] = int(node.attributes.item(i).value)
elif node.attributes.item(i).name == "tsid":
transponder["tsid"] = int(node.attributes.item(i).value)
- if len(list(transponder.keys())) in (22, 18):
+ if len(list(transponder.keys())) in (25, 18):
provider["transponder"] = transponder
elif node.tagName == "bouquettype":
@@ -227,12 +237,15 @@ def read(self):
elif node2.attributes.item(i).name == "region":
# allow region to be a list of values (e.g. so we can accept both SD and HD descriptors)
configuration["region"] = list(map(lambda x: int(x.strip(), 16), node2.attributes.item(i).value.split(",")))
+ elif node2.attributes.item(i).name == "bouquet_file":
+ # skyde protocol: filename inside the Sky Q NDS-ARCHIVE
+ configuration["bouquet_file"] = self.encodeNODE(node2.attributes.item(i).value)
node2.normalize()
if len(node2.childNodes) == 1 and node2.childNodes[0].nodeType == node2.TEXT_NODE:
configuration["name"] = self.encodeNODE(node2.childNodes[0].data)
- if len(list(configuration.keys())) == 4:
+ if len(list(configuration.keys())) in (4, 5):
provider["bouquets"][configuration["key"]] = configuration
elif node.tagName == "dvbcconfigs":
@@ -357,6 +370,21 @@ def read(self):
if len(list(transponder.keys())) == 8:
provider["transponder"] = transponder
+ elif node.tagName == "lcn_overrides":
+ provider["lcn_overrides"] = []
+ for node2 in node.childNodes:
+ if node2.nodeType == node2.ELEMENT_NODE and node2.tagName == "override":
+ ov = {}
+ for i in list(range(0, node2.attributes.length)):
+ n = node2.attributes.item(i).name
+ v = node2.attributes.item(i).value
+ if n in ("onid", "tsid", "sid"):
+ ov[n] = int(v, 16 if v.lower().startswith("0x") else 10)
+ elif n == "lcn":
+ ov[n] = int(v)
+ if set(ov.keys()) >= {"onid", "tsid", "sid", "lcn"}:
+ provider["lcn_overrides"].append(ov)
+
elif node.tagName == "sections":
provider["sections"] = {}
for node2 in node.childNodes: