basic XMLTV support

This commit is contained in:
DYefremov
2022-06-14 20:14:08 +03:00
parent 748b41e31c
commit c3bc3a1160
9 changed files with 686 additions and 28 deletions

View File

@@ -155,6 +155,12 @@ class PlayStreamsMode(IntEnum):
M3U = 2
class EpgSource(IntEnum):
HTTP = 0 # HTTP API -> WebIf
DAT = 1 # epg.dat file
XML = 2 # XML TV
class Settings:
__INSTANCE = None
__VERSION = 2
@@ -532,6 +538,30 @@ class Settings:
def epg_options(self, value):
self._cp_settings["epg_options"] = value
@property
def epg_source(self):
return EpgSource(self._cp_settings.get("epg_source", EpgSource.HTTP))
@epg_source.setter
def epg_source(self, value):
self._cp_settings["epg_source"] = value
@property
def epg_update_interval(self):
return self._cp_settings.get("epg_update_interval", 5)
@epg_update_interval.setter
def epg_update_interval(self, value):
self._cp_settings["epg_update_interval"] = value
@property
def epg_xml_source(self):
return self._cp_settings.get("epg_xml_source", "")
@epg_xml_source.setter
def epg_xml_source(self, value):
self._cp_settings["epg_xml_source"] = value
# *********** FTP ************ #
@property

View File

@@ -27,12 +27,21 @@
""" Module for working with epg.dat file. """
import abc
import os
import shutil
import struct
import sys
from collections import namedtuple
from datetime import datetime, timezone
from tempfile import NamedTemporaryFile
from urllib.parse import urlparse
from xml.dom.minidom import parse, Node, Document
import xml.etree.ElementTree as ET
import requests
from app.commons import log, run_task
from app.eparser.ecommons import BqServiceType, BouquetService
ENCODING = "utf-8"
@@ -44,6 +53,18 @@ except ModuleNotFoundError:
else:
DETECT_ENCODING = True
EpgEvent = namedtuple("EpgEvent", ["title", "time", "desc", "event_data"])
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", None) # For Python3 < 3.7
class Reader(metaclass=abc.ABCMeta):
@abc.abstractmethod
def download(self, clb=None): pass
@abc.abstractmethod
def get_current_events(self, ids: set) -> dict: pass
class EPG:
""" Base EPG class. """
@@ -51,7 +72,7 @@ class EPG:
# datetime.datetime.toordinal(1858,11,17) => 678576
ZERO_DAY = 678576
EpgEvent = namedtuple("EpgEvent", ["id", "data", "start", "duration", "title", "desc", "ext_desc"])
Event = namedtuple("EpgEvent", ["id", "data", "start", "duration", "title", "desc", "ext_desc"])
class EventData:
""" Event data representation class. """
@@ -86,7 +107,7 @@ class EPG:
return EPG.get_from_bcd(self.raw_data[7]) * 3600 + EPG.get_from_bcd(
self.raw_data[8]) * 60 + EPG.get_from_bcd(self.raw_data[9])
class Reader:
class DatReader(Reader):
""" The epd.dat file reading class.
The read algorithm was taken from the eEPGCache::load() function from this source:
@@ -98,6 +119,12 @@ class EPG:
self._refs = {}
self._desc = {}
def download(self, clb=None):
pass
def get_current_events(self, ids: set) -> dict:
pass
def get_refs(self):
return self._refs.keys()
@@ -135,7 +162,7 @@ class EPG:
elif desc_type == 78: # Extended event descriptor -> 0x4e -> 78
ext_desc = data[9:].decode(encoding, errors="ignore") if data[7] and data[8] < 32 else None
return EPG.EpgEvent(e_id, evd, start, duration, title, desc, ext_desc)
return EPG.Event(e_id, evd, start, duration, title, desc, ext_desc)
def get_events(self, ref):
return self._refs.get(ref, {})
@@ -190,6 +217,152 @@ class EPG:
return ((value & 0xF0) >> 4) * 10 + (value & 0xF)
class XmlTvReader(Reader):
PR_TAG = "programme"
CH_TAG = "channel"
DSP_NAME_TAG = "display-name"
ICON_TAG = "icon"
TITLE_TAG = "title"
DESC_TAG = "desc"
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
Service = namedtuple("Service", ["id", "name", "logo", "events"])
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
def __init__(self, path, url):
self._path = path
self._url = url
self._ids = {}
@run_task
def download(self, clb=None):
""" Downloads and processes an XMLTV file. """
res = urlparse(self._url)
if not all((res.scheme, res.netloc)):
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
return
with requests.get(url=self._url, stream=True) as request:
if request.reason == "OK":
suf = self._url[self._url.rfind("."):]
if suf not in (".gz", ".xz", ".lzma"):
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
return
data_len = request.headers.get("content-length")
with NamedTemporaryFile(suffix=suf) as tf:
downloaded = 0
data_len = int(data_len)
log("Downloading XMLTV file...")
for data in request.iter_content(chunk_size=1024):
downloaded += len(data)
tf.write(data)
done = int(50 * downloaded / data_len)
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
sys.stdout.flush()
tf.seek(0)
sys.stdout.write("\n")
os.makedirs(os.path.dirname(self._path), exist_ok=True)
if suf.endswith(".gz"):
try:
shutil.copyfile(tf.name, self._path)
except OSError as e:
log(f"{self.__class__.__name__} [download *.gz] error: {e}")
elif self._url.endswith((".xz", ".lzma")):
import lzma
try:
with lzma.open(tf, "rb") as lzf:
shutil.copyfileobj(lzf, self._path)
except (lzma.LZMAError, OSError) as e:
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
else:
log(f"{self.__class__.__name__} [download] error: {request.reason}")
if clb:
clb()
def get_current_events(self, names: set) -> dict:
events = {}
dt = datetime.utcnow()
utc = dt.timestamp()
offset = datetime.now() - dt
for srv in filter(lambda s: s.name in names, self._ids.values()):
ev = list(filter(lambda s: s.start < utc, srv.events))
if ev:
ev = ev[-1]
start = datetime.fromtimestamp(ev.start) + offset
end_time = datetime.fromtimestamp(ev.duration) + offset
tm = f"{start.strftime('%H:%M')} - {end_time.strftime('%H:%M')}"
events[srv.name] = EpgEvent(ev.title, tm, ev.desc, ev)
return events
def parse(self):
""" Parses XML. """
try:
import gzip
with gzip.open(self._path, "rb") as gzf:
log("Processing XMLTV data...")
list(map(self.process_node, ET.iterparse(gzf)))
log("XMLTV data parsing is complete.")
except OSError as e:
log(f"{self.__class__.__name__} [parse] error: {e}")
def process_node(self, node):
event, element = node
if element.tag == self.CH_TAG:
ch_id = element.get("id", None)
name, logo = None, None
for c in element:
if c.tag == self.DSP_NAME_TAG:
name = c.text
elif c.tag == self.ICON_TAG:
logo = c.get("src", None)
self._ids[ch_id] = self.Service(ch_id, name, logo, [])
elif element.tag == self.PR_TAG:
channel = self._ids.get(element.get(self.CH_TAG, None), None)
if channel:
events = channel[-1]
start = element.get("start", None)
if start:
start = self.get_utc_time(start)
stop = element.get("stop", None)
if stop:
stop = self.get_utc_time(stop)
title, desc = None, None
for c in element:
if c.tag == self.TITLE_TAG:
title = c.text
elif c.tag == self.DESC_TAG:
desc = c.text
if all((start, stop, title)):
events.append(self.Event(start, stop, title, desc))
def to_epg_dat(self):
""" Converts and saves imported data to 'epg.dat' file. """
raise ValueError("Not implemented yet!")
@staticmethod
def get_utc_time(time_str):
""" Returns the UTC time in seconds. """
t, sep, delta = time_str.partition(" ")
t = datetime(*map(int, (t[:4], t[4:6], t[6:8], t[8:10], t[10:12], t[12:]))).timestamp()
if delta:
t -= (3600 * int(delta) // 100)
return t
class ChannelsParser:
_COMMENT = "File was created in DemonEditor"

0
app/ui/epg/__init__.py Normal file
View File

View File

@@ -33,7 +33,6 @@ import os
import re
import shutil
import urllib.request
from collections import namedtuple
from datetime import datetime
from enum import Enum
from urllib.error import HTTPError, URLError
@@ -44,15 +43,12 @@ from gi.repository import GLib
from app.commons import run_idle, run_task, run_with_delay
from app.connections import download_data, DownloadType, HttpAPI
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP
from app.tools.epg import EPG, ChannelsParser
from app.settings import SEP, EpgSource
from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
from app.ui.timers import TimerTool
from .main_helper import on_popup_menu, update_entry_data
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page
EpgEvent = namedtuple("EpgEvent", ["title", "time", "desc", "event_data"])
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", None) # For Python3 < 3.7
from ..main_helper import on_popup_menu, update_entry_data
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page
class RefsSource(Enum):
@@ -64,40 +60,121 @@ class EpgCache(dict):
def __init__(self, app):
super().__init__()
self._current_bq = None
self._reader = None
self._settings = app.app_settings
self._src = self._settings.epg_source
self._app = app
self._app.connect("bouquet-changed", self.on_bouquet_changed)
self._app.connect("profile-changed", self.on_profile_changed)
self.init()
@run_task
def init(self):
GLib.timeout_add_seconds(3, self.update_epg_data, priority=GLib.PRIORITY_LOW)
if self._src is EpgSource.XML:
url = self._settings.epg_xml_source
gz_file = f"{self._settings.profile_data_path}epg{os.sep}epg.gz"
self._reader = XmlTvReader(gz_file, url)
if os.path.isfile(gz_file):
# Difference calculation between the current time and file modification.
dif = datetime.now() - datetime.fromtimestamp(os.path.getmtime(gz_file))
# We will update daily. -> Temporarily!!!
self._reader.download(self._reader.parse) if dif.days > 0 else self._reader.parse()
else:
self._reader.download(self._reader.parse)
elif self._src is EpgSource.DAT:
self._reader = EPG.DatReader(f"{self._settings.profile_data_path}epg{os.sep}epg.dat")
self._reader.download()
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.update_epg_data, priority=GLib.PRIORITY_LOW)
def on_bouquet_changed(self, app, bq):
self._current_bq = bq
def on_profile_changed(self, app, bq):
def on_profile_changed(self, app, p):
self.clear()
def update_epg_data(self):
api = self._app.http_api
bq = self._app.current_bouquet_files.get(self._current_bq, None)
if self._src is EpgSource.HTTP:
api = self._app.http_api
bq = self._app.current_bouquet_files.get(self._current_bq, None)
if bq and api:
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"')
api.send(HttpAPI.Request.EPG_NOW, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_data)
if bq and api:
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"')
api.send(HttpAPI.Request.EPG_NOW, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_http_data)
elif self._src is EpgSource.XML:
self.update_xml_data()
return self._app.display_epg
@run_idle
def update_data(self, epg):
def update_http_data(self, epg):
for e in (EpgTool.get_event(e, False) for e in epg.get("event_list", []) if e.get("e2eventid", "").isdigit()):
self[e.event_data.get("e2eventservicename", "")] = e
def update_xml_data(self):
services = self._app.current_services
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, [])}
for name, e in self._reader.get_current_events(names).items():
self[name] = e
def get_current_event(self, service_name):
return self.get(service_name, EpgEvent())
class EpgSettingsPopover(Gtk.Popover):
def __init__(self, app, **kwarg):
super().__init__(**kwarg)
self._app = app
self._app.connect("profile-changed", self.on_profile_changed)
handlers = {"on_save": self.on_save,
"on_close": lambda b: self.popdown()}
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}settings.glade", handlers)
self.add(builder.get_object("main_box"))
self._http_src_button = builder.get_object("http_src_button")
self._xml_src_button = builder.get_object("xml_src_button")
self._dat_src_button = builder.get_object("dat_src_button")
self._interval_button = builder.get_object("interval_button")
self._url_entry = builder.get_object("url_entry")
self._dat_path_box = builder.get_object("dat_path_box")
self.init()
def init(self):
settings = self._app.app_settings
src = settings.epg_source
if src is EpgSource.HTTP:
self._http_src_button.set_active(True)
elif src is EpgSource.XML:
self._xml_src_button.set_active(True)
else:
self._dat_src_button.set_active(True)
self._interval_button.set_value(settings.epg_update_interval)
self._url_entry.set_text(settings.epg_xml_source)
self._dat_path_box.set_active_id(settings.epg_dat_path)
def on_save(self, button):
settings = self._app.app_settings
if self._http_src_button.get_active():
settings.epg_source = EpgSource.HTTP
elif self._xml_src_button.get_active():
settings.epg_source = EpgSource.XML
else:
settings.epg_source = EpgSource.DAT
settings.epg_update_interval = self._interval_button.get_value()
settings.epg_xml_source = self._url_entry.get_text()
settings.epg_dat_path = self._dat_path_box.get_active_id()
self.popdown()
def on_profile_changed(self, app, p):
self.init()
class EpgTool(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -110,7 +187,7 @@ class EpgTool(Gtk.Box):
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_epg_filter_toggled": self.on_epg_filter_toggled}
builder = get_builder(UI_RESOURCES_PATH + "epg.glade", handlers)
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}tab.glade", handlers)
self._view = builder.get_object("epg_view")
self._model = builder.get_object("epg_model")
@@ -243,7 +320,7 @@ class EpgDialog:
self._refs_source = RefsSource.SERVICES
self._download_xml_is_active = False
builder = get_builder(f"{UI_RESOURCES_PATH}epg_dialog.glade", handlers)
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}dialog.glade", handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(self._app.app_window)
@@ -348,7 +425,7 @@ class EpgDialog:
refs = None
if self._enable_dat_filter:
try:
epg_reader = EPG.Reader(f"{self._epg_dat_path_entry.get_text()}epg.dat")
epg_reader = EPG.DatReader(f"{self._epg_dat_path_entry.get_text()}epg.dat")
epg_reader.read()
refs = epg_reader.get_refs()
except (OSError, ValueError) as e:

356
app/ui/epg/settings.glade Normal file
View File

@@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAdjustment" id="interval_adjustment">
<property name="lower">3</property>
<property name="upper">60</property>
<property name="value">3</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Source:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="src_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkButtonBox" id="source_selection_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkRadioButton" id="http_src_button">
<property name="label" translatable="yes">WebIf</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">dat_src_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="xml_src_button">
<property name="label" translatable="yes">XML TV</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">dat_src_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="dat_src_button">
<property name="label" translatable="yes">*.dat file</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">http_src_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="interval_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="interval_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Update interval (sec):</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="interval_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_width_chars">4</property>
<property name="adjustment">interval_adjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<property name="value">3</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="xml_source_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="sensitive" bind-source="xml_src_button" bind-property="active"/>
<child>
<object class="GtkLabel" id="url_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Url to *.xml.gz file:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="input_purpose">url</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="download_interval_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="download_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Download:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="download_interval_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="halign">end</property>
<property name="active">0</property>
<property name="active_id">daily</property>
<items>
<item id="daily" translatable="yes">Daily</item>
</items>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="dat_source_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="sensitive" bind-source="dat_src_button" bind-property="active"/>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">STB path:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="dat_path_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">0</property>
<property name="active_id">/etc/enigma2</property>
<items>
<item id="/etc/enigma2/">/etc/enigma2/</item>
<item id="/media/hdd/">/media/hdd/</item>
<item id="/media/usb/">/media/usb/</item>
<item id="/media/mmc/">/media/mmc/</item>
<item id="/media/cf/">/media/cf/</item>
</items>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="actions_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="save_button">
<property name="label" translatable="yes">Save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_save" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="close_button">
<property name="label" translatable="yes">Close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">16</property>
</packing>
</child>
</object>
</interface>

View File

@@ -3282,7 +3282,25 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<placeholder/>
<object class="GtkMenuButton" id="epg_menu_button">
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">EPG source</property>
<child>
<object class="GtkImage" id="epg_menu_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">insert-text-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>

View File

@@ -50,7 +50,7 @@ from app.settings import (SettingsType, Settings, SettingsException, SettingsRea
IS_DARWIN, PlayStreamsMode, IS_LINUX)
from app.tools.media import Recorder
from app.ui.control import ControlTool
from app.ui.epg import EpgDialog, EpgTool, EpgCache
from app.ui.epg.epg import EpgCache, EpgSettingsPopover, EpgDialog, EpgTool
from app.ui.ftp import FtpClientBox
from app.ui.logs import LogsClient
from app.ui.playback import PlayerBox
@@ -530,6 +530,9 @@ class Application(Gtk.Application):
self._epg_cache = None
fav_service_column = builder.get_object("fav_service_column")
fav_service_column.set_cell_data_func(builder.get_object("fav_service_renderer"), self.fav_service_data_func)
self._epg_menu_button = builder.get_object("epg_menu_button")
self._epg_menu_button.connect("realize", lambda b: b.set_popover(EpgSettingsPopover(self)))
self.bind_property("is_enigma", self._epg_menu_button, "sensitive")
# Hiding for Neutrino.
self.bind_property("is_enigma", builder.get_object("services_button_box"), "visible")
# Setting the last size of the window if it was saved.
@@ -2638,7 +2641,7 @@ class Application(Gtk.Application):
self.update_profile_label()
is_enigma = self._s_type is SettingsType.ENIGMA_2
self.set_property("is-enigma", is_enigma)
self.update_stack_elements_visibility(is_enigma)
self.update_elements_visibility(is_enigma)
def update_profiles(self):
self._profile_combo_box.remove_all()
@@ -2646,7 +2649,7 @@ class Application(Gtk.Application):
self._profile_combo_box.append(p, p)
@run_idle
def update_stack_elements_visibility(self, is_enigma=False):
def update_elements_visibility(self, is_enigma=False):
self._stack_services_frame.set_visible(self._settings.get("show_bouquets", True))
self._stack_satellite_box.set_visible(self._settings.get("show_satellites", True))
self._stack_picon_box.set_visible(self._settings.get("show_picons", True))
@@ -2928,8 +2931,9 @@ class Application(Gtk.Application):
action.set_state(value)
set_display = bool(value)
self._settings.display_epg = set_display
self._display_epg = set_display
self._epg_menu_button.set_visible(set_display)
self._epg_cache = EpgCache(self) if set_display else None
self._display_epg = set_display
def on_epg_list_configuration(self, action, value=None):
if self._s_type is not SettingsType.ENIGMA_2: