mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-02-24 15:41:28 +01:00
basic XMLTV support
This commit is contained in:
@@ -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
|
||||
|
||||
179
app/tools/epg.py
179
app/tools/epg.py
@@ -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
0
app/ui/epg/__init__.py
Normal 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
356
app/ui/epg/settings.glade
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user