added basic support for alternatives

This commit is contained in:
DYefremov
2021-01-06 01:54:10 +03:00
parent 412a66e5e5
commit 2f55fb4e64
6 changed files with 502 additions and 291 deletions

View File

@@ -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)

View File

@@ -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"])

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -33,6 +33,18 @@ Author: Dmitriy Yefremov
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="alt_list_store">
<columns>
<!-- column-name num -->
<column type="gint"/>
<!-- column-name service -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name pos -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkImage" id="backups_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -2397,185 +2409,319 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="fav_scrolled_window">
<object class="GtkBox" id="fav_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">2</property>
<property name="shadow_type">in</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkTreeView" id="fav_tree_view">
<object class="GtkScrolledWindow" id="fav_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">fav_list_store</property>
<property name="enable_search">False</property>
<property name="search_column">2</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">9</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_fav_press" object="fav_popup_menu" swapped="no"/>
<signal name="button-press-event" handler="on_view_press" swapped="yes"/>
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
<signal name="drag-data-get" handler="on_view_drag_data_get" swapped="no"/>
<signal name="drag-data-received" handler="on_view_drag_data_received" swapped="no"/>
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
<signal name="focus-in-event" handler="on_view_focus" swapped="no"/>
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
<signal name="key-release-event" handler="on_tree_view_key_release" swapped="no"/>
<signal name="query-tooltip" handler="on_fav_view_query_tooltip" swapped="no"/>
<signal name="row-activated" handler="on_fav_selection" object="fav_list_store" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="fav_selection">
<property name="mode">multiple</property>
</object>
</child>
<property name="can_focus">False</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeViewColumn" id="fav_num_column">
<property name="resizable">True</property>
<property name="min_width">25</property>
<property name="title" translatable="yes">Num</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="num_cellrenderertext">
<property name="xalign">0.20000000298023224</property>
<property name="width_chars">5</property>
<property name="max_width_chars">5</property>
<object class="GtkTreeView" id="fav_tree_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">fav_list_store</property>
<property name="enable_search">False</property>
<property name="search_column">2</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">9</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_fav_press" object="fav_popup_menu" swapped="no"/>
<signal name="button-press-event" handler="on_view_press" swapped="yes"/>
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
<signal name="drag-data-get" handler="on_view_drag_data_get" swapped="no"/>
<signal name="drag-data-received" handler="on_view_drag_data_received" swapped="no"/>
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
<signal name="focus-in-event" handler="on_view_focus" swapped="no"/>
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
<signal name="key-release-event" handler="on_tree_view_key_release" swapped="no"/>
<signal name="query-tooltip" handler="on_fav_view_query_tooltip" swapped="no"/>
<signal name="row-activated" handler="on_fav_selection" object="fav_list_store" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="fav_selection">
<property name="mode">multiple</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="visible">0</attribute>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fav_service_column">
<property name="resizable">True</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererPixbuf" id="fav_picon_cellrendererpixbuf">
<property name="xpad">2</property>
<object class="GtkTreeViewColumn" id="fav_num_column">
<property name="resizable">True</property>
<property name="min_width">25</property>
<property name="title" translatable="yes">Num</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="num_cellrenderertext">
<property name="xalign">0.20000000298023224</property>
<property name="width_chars">5</property>
<property name="max_width_chars">5</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="visible">0</attribute>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">8</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_coded_cellrendererpixbuf">
<property name="xpad">2</property>
<object class="GtkTreeViewColumn" id="fav_service_column">
<property name="resizable">True</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererPixbuf" id="fav_picon_cellrendererpixbuf">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">8</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_coded_cellrendererpixbuf">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">1</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="fav_service_cellrenderertext">
<property name="ellipsize">end</property>
<property name="width_chars">25</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="text">2</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_locked_cellrendererpixbuf">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">3</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_hide_cellrendererpixbuf">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">4</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">1</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="fav_service_cellrenderertext">
<property name="ellipsize">end</property>
<property name="width_chars">25</property>
<object class="GtkTreeViewColumn" id="fav_type_column">
<property name="resizable">True</property>
<property name="min_width">25</property>
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="type_cellrenderertext">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="text">2</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_locked_cellrendererpixbuf">
<property name="xpad">2</property>
<object class="GtkTreeViewColumn" id="fav_pos_column">
<property name="min_width">25</property>
<property name="title" translatable="yes">Pos</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="pos_cellrenderertext">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="text">6</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">3</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_hide_cellrendererpixbuf">
<property name="xpad">2</property>
<object class="GtkTreeViewColumn" id="fav_id_column">
<property name="visible">False</property>
<property name="title">fav_id</property>
<child>
<object class="GtkCellRendererText" id="fav_id_cellrenderertext4"/>
<attributes>
<attribute name="text">7</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="pixbuf">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fav_type_column">
<property name="resizable">True</property>
<property name="min_width">25</property>
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="type_cellrenderertext">
<property name="xalign">0.50999999046325684</property>
<object class="GtkTreeViewColumn" id="fav_extra_column">
<property name="visible">False</property>
<property name="title" translatable="yes">extra</property>
<child>
<object class="GtkCellRendererText" id="fav_tooltip_cellrenderertext"/>
<attributes>
<attribute name="text">9</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_background_cellrenderertext"/>
</child>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fav_pos_column">
<property name="min_width">25</property>
<property name="title" translatable="yes">Pos</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<signal name="clicked" handler="on_fav_sort" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="pos_cellrenderertext">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="cell-background-rgba">10</attribute>
<attribute name="text">6</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fav_id_column">
<property name="visible">False</property>
<property name="title">fav_id</property>
<child>
<object class="GtkCellRendererText" id="fav_id_cellrenderertext4"/>
<attributes>
<attribute name="text">7</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fav_extra_column">
<property name="visible">False</property>
<property name="title" translatable="yes">extra</property>
<child>
<object class="GtkCellRendererText" id="fav_tooltip_cellrenderertext"/>
<attributes>
<attribute name="text">9</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererPixbuf" id="fav_background_cellrenderertext"/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRevealer" id="alt_revealer">
<property name="can_focus">False</property>
<property name="transition_type">slide-up</property>
<child>
<object class="GtkBox" id="alt_box">
<property name="height_request">200</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="alt_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Alternatives</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="alt_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="alt_tree_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">alt_list_store</property>
<property name="enable_search">False</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="alt_num_column">
<property name="min_width">25</property>
<property name="title" translatable="yes">Num</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="alt_num_cellrenderertext">
<property name="width_chars">5</property>
<property name="max_width_chars">5</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="alt_service_column">
<property name="resizable">True</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="alt_service_cellrenderertext"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="alt_type_column">
<property name="resizable">True</property>
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="alt_type_cellrenderertext">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="alt_pos_column">
<property name="min_width">25</property>
<property name="title" translatable="yes">Pos</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="alt_pos_cellrenderertext">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>