Compare commits

..

115 Commits
0.4.3 ... 0.4.5

Author SHA1 Message Date
DYefremov
3550f58603 update spanish, dutch and portuguese 2019-06-11 21:24:06 +03:00
DYefremov
729c85be77 fix type for radio bouquets 2019-06-11 21:03:51 +03:00
DYefremov
f8aee1b807 update russian 2019-06-08 16:04:47 +03:00
DYefremov
75d93f6a19 improved xml download for the epg dialog 2019-06-08 15:45:41 +03:00
DYefremov
4581cc7d4f upd README 2019-06-05 11:53:52 +03:00
DYefremov
2f8ea069e1 bouquet name for xml file when saving from the epg dialog 2019-06-04 13:31:54 +03:00
DYefremov
8afcec6b7e changed date format in the header 2019-06-04 13:06:02 +03:00
DYefremov
b5a9321c5c small refactoring of getting refs from xml 2019-06-04 01:22:26 +03:00
DYefremov
7ee781c39b lazy init of epg data 2019-06-03 15:47:04 +03:00
DYefremov
48f3c1a4d6 added export to m3u for neutrino 2019-05-30 15:56:04 +03:00
DYefremov
717bac6446 fix on open 2019-05-30 12:57:31 +03:00
DYefremov
fe1323f8cf deleted extra dialog 2019-05-30 11:12:22 +03:00
DYefremov
0f30d74edc changing header bar elements 2019-05-29 14:31:44 +03:00
DYefremov
0686c91a5d fix provider name for neutrino 2019-05-29 12:57:03 +03:00
DYefremov
a84090cda7 added support of coupled satellites 2019-05-27 22:17:29 +03:00
DYefremov
291b3aa289 minor appearance changes 2019-05-27 11:02:41 +03:00
DYefremov
dd92ffc9b1 fix getting some transponders 2019-05-27 00:17:38 +03:00
DYefremov
97a8f793c3 lazy loading of satellites list 2019-05-26 22:11:52 +03:00
DYefremov
3ad2e3d6b6 disable cache 2019-05-26 21:50:02 +03:00
DYefremov
7d6763ffb5 fix show iptv services in the info box of import dialog 2019-05-20 20:01:28 +03:00
DYefremov
6582be7a0d added arg for the start script 2019-05-19 14:22:43 +03:00
DYefremov
1e45621bd8 added data recovery if download error 2019-05-19 00:37:07 +03:00
DYefremov
61bcb85bbc global update settings from the download dialog 2019-05-14 22:12:36 +03:00
DYefremov
9eee9ac424 small refactoring of the init of dynamic elems 2019-05-13 14:42:23 +03:00
DYefremov
3f720afedc added command line params support 2019-05-12 16:26:58 +03:00
DYefremov
cd19c5fd9c optional logging 2019-05-12 16:26:19 +03:00
DYefremov
61ca2f3e8b minor changes for the input dialog 2019-05-11 13:27:46 +03:00
DYefremov
75fc7adc88 input dialog refactoring 2019-05-11 00:09:20 +03:00
DYefremov
3678a9d29d satellite dialogs refactoring 2019-05-10 14:42:32 +03:00
DYefremov
a5927dd2b6 service details refactoring 2019-05-10 14:41:33 +03:00
DYefremov
34e0ed4748 setting selected bouquet after rename 2019-05-09 23:51:47 +03:00
DYefremov
d7a214b445 iptv dialogs refactoring 2019-05-09 14:48:29 +03:00
DYefremov
e194827af7 get about dialog 2019-05-09 12:53:11 +03:00
DYefremov
e9e53da5cc dialogs refactoring 2019-05-09 11:11:54 +03:00
DYefremov
822497317d minor changes 2019-05-09 00:01:49 +03:00
DYefremov
c2047bd7b5 question dialog refactoring 2019-05-08 23:35:42 +03:00
DYefremov
3636da60d6 input dialog refactoring 2019-05-08 23:05:32 +03:00
DYefremov
2eebd55b77 updating counter on reset 2019-05-07 22:08:04 +03:00
DYefremov
12f76f8e28 added reset for the epg dialog 2019-05-07 17:22:18 +03:00
DYefremov
28e6cca919 update spanish, dutch and portuguese 2019-05-07 13:25:17 +03:00
DYefremov
9b53538da6 new impl of data mapping for the epg dialog 2019-05-07 00:04:53 +03:00
DYefremov
994541bad5 update russian 2019-05-05 11:49:24 +03:00
DYefremov
3cbb16febe minor gui changes 2019-05-05 11:26:11 +03:00
DYefremov
2b61fa07b9 update russian 2019-05-05 11:08:16 +03:00
DYefremov
406f4bd0f0 little mapping improvements for services with cyrillic names 2019-05-04 23:54:58 +03:00
DYefremov
1ec6b817e9 support of epg.dat download from the receiver 2019-05-04 20:13:57 +03:00
DYefremov
7c55692c99 small decoupling of dialogs 2019-05-04 11:21:20 +03:00
DYefremov
3aa29a788d added groups support by export to m3u 2019-05-01 17:21:51 +03:00
DYefremov
55b0dccc80 added info dialog 2019-05-01 17:19:31 +03:00
DYefremov
edb97cbf8c added keyboard shortcuts for the epg dialog 2019-05-01 13:11:19 +03:00
DYefremov
7620f03e2b added info bars for the epg dialog 2019-04-30 14:17:45 +03:00
DYefremov
cced856297 added base support of xml sources for epg dialog 2019-04-27 19:05:37 +03:00
DYefremov
3bcfd66971 added elements in the epg options widget 2019-04-26 22:07:21 +03:00
DYefremov
e7e7c667e9 added options widget for the epg dialog 2019-04-25 00:18:49 +03:00
DYefremov
6de0bc4201 added popup menus for epg dialog 2019-04-24 21:53:01 +03:00
DYefremov
878520b7f9 epg config dialog skeleton 2019-04-24 20:27:47 +03:00
DYefremov
63ac413982 saving list to xml 2019-04-22 20:25:19 +03:00
DYefremov
171c58c546 epg assignment by drag 2019-04-22 00:12:04 +03:00
DYefremov
6758ae3d16 assign epg data 2019-04-21 21:48:47 +03:00
DYefremov
329513d2a7 epg config dialog skeleton 2019-04-21 01:18:54 +03:00
DYefremov
be195e9001 added epg icon 2019-04-20 20:44:56 +03:00
DYefremov
635a3fb966 added epg skeleton 2019-04-18 23:05:19 +03:00
DYefremov
281f7a28f3 added export to m3u 2019-04-18 21:43:35 +03:00
DYefremov
507f5817c2 update version 2019-04-18 19:12:52 +03:00
DYefremov
d3822474ba small internal refactoring of iptv list config dialog 2019-04-14 20:24:57 +03:00
DYefremov
e1ce9f3006 added non-rec stream types for iptv 2019-04-14 00:03:52 +03:00
DYefremov
c2b0768857 minor cleaning 2019-04-13 15:23:24 +03:00
DYefremov
283d85ef8e fix reading of bouquet names for some configs 2019-04-12 23:29:04 +03:00
DYefremov
f5656d8d5f update spanish, dutch and portuguese 2019-04-08 11:28:49 +03:00
DYefremov
5dd5a09bfc show error message if no item is selected by import 2019-04-05 13:46:37 +03:00
DYefremov
1b5f3372b4 update russian 2019-04-04 21:01:58 +03:00
DYefremov
974e964f42 minor gui changes 2019-04-04 20:38:30 +03:00
DYefremov
8cb6ed02d2 update russian 2019-04-01 10:11:57 +03:00
DYefremov
8bb3b780d1 minor tooltips changes 2019-04-01 10:09:55 +03:00
DYefremov
3000c8830c minor gui changes 2019-03-31 21:51:53 +03:00
DYefremov
ac550e016d update russian 2019-03-31 21:40:55 +03:00
DYefremov
7420751806 Merge remote-tracking branch 'origin/development' into development 2019-03-31 21:10:41 +03:00
DYefremov
f35889e8e4 minor changes in the gui of the settings dialog 2019-03-31 21:10:27 +03:00
DYefremov
857b252f4c upd README 2019-03-28 10:34:13 +03:00
DYefremov
572584a14f fix transponders duplication 2019-03-23 11:16:43 +03:00
DYefremov
7e4ac3e69c fix pls mode 2019-03-22 00:54:44 +03:00
DYefremov
0d73ffa79d show error dialog refactoring 2019-03-19 21:44:05 +03:00
DYefremov
5e2f1ddb84 added extra method for error dialog showing 2019-03-19 00:12:33 +03:00
DYefremov
6c4040901f upd README 2019-03-18 23:37:37 +03:00
DYefremov
103e09b900 added download/upload data using Ctrl + D/U/B shortcuts 2019-03-18 23:04:05 +03:00
DYefremov
8ddc517ab7 added skip message for http test 2019-03-18 23:03:42 +03:00
DYefremov
b26d982db4 added skip message for http test 2019-03-18 22:54:59 +03:00
DYefremov
3733bc395b fix insert stream 2019-03-14 13:43:13 +03:00
DYefremov
26bfbafc0e little cleaning 2019-03-14 12:40:32 +03:00
DYefremov
84d1a18111 added single import for neutrino 2019-03-14 12:37:48 +03:00
DYefremov
1cdacd5276 added support of multistream transponders by update from web 2019-03-12 13:39:30 +03:00
DYefremov
354715558c fix set pls mode for transponder dialog 2019-03-12 10:39:55 +03:00
DYefremov
bca1613bff Added Ctrl + O/Q shortcuts 2019-03-10 18:11:38 +03:00
DYefremov
36b533b890 added double click mode option for bouquet list 2019-03-10 15:33:28 +03:00
DYefremov
2eabccc1a9 fix single import for empty bouquets list 2019-03-06 08:40:38 +03:00
DYefremov
75cd78277e added remove all unused picons 2019-03-03 12:50:40 +03:00
DYefremov
5181b732ed added confirmation dialog before import 2019-02-27 20:12:41 +03:00
DYefremov
513c0e8d3d update dutch, spanish and portuguese 2019-02-26 20:52:01 +03:00
DYefremov
f932feb305 update russian 2019-02-25 23:37:05 +03:00
DYefremov
6a2fda5ec0 changes for single bouquet import 2019-02-25 23:35:50 +03:00
DYefremov
86c30dd2c1 changes for single bouquet import 2019-02-25 23:35:20 +03:00
DYefremov
0ed41c473d minor gui changes 2019-02-24 15:47:19 +03:00
DYefremov
5078a854d2 import single bouquet skeleton 2019-02-23 13:54:00 +03:00
DYefremov
353bf04924 added import elements 2019-02-23 13:53:16 +03:00
DYefremov
474ff8e303 changed data opening from import dialog 2019-02-15 13:04:52 +03:00
DYefremov
5834bd4a0b added selection with space key 2019-02-09 15:25:44 +03:00
DYefremov
81f31e5d8d added space key 2019-02-09 15:16:03 +03:00
DYefremov
267f645c16 little refactoring of working with models 2019-02-09 12:46:06 +03:00
DYefremov
8c10d7d6a5 added columns for bouquets model 2019-02-09 12:43:27 +03:00
DYefremov
bbdb47ee7a enabled import for neutrino 2019-02-09 10:25:49 +03:00
DYefremov
7f6856e6aa import for empty config 2019-02-09 09:55:35 +03:00
DYefremov
3439a3ad0a base implementation of import 2019-02-08 19:11:30 +03:00
DYefremov
ee2e2ac49d added bouquet details for import dialog 2019-02-06 00:19:04 +03:00
DYefremov
a745167fb7 update version 2019-02-05 17:02:23 +03:00
DYefremov
8551bc2459 added import bouquets dialog 2019-02-05 16:58:54 +03:00
48 changed files with 5000 additions and 1371 deletions

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Dmitriy Yefremov
Copyright (c) 2018-2019 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

View File

@@ -20,17 +20,23 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list.
* **Ctrl + O** - (re)load user data from current dir.
* **Ctrl + D** - load data from receiver.
* **Ctrl + U/B** upload data/bouquets to receiver.
### Extra:
* Multiple selections in lists only with Space key (as in file managers).
* Ability to import IPTV into bouquet (Neutrino WEBTV) from m3u files.
* Ability to download picons and update satellites (transponders) from web.
* Preview (playing) IPTV or other streams directly from the bouquet list(should be installed VLC).
* Import feature.
* Multiple selections in lists only with Space key (as in file managers).
* Ability to download picons and update satellites (transponders) from web.
* Ability to import into bouquet (Neutrino WEB TV) from m3u.
* Ability to export bouquets with IPTV services to m3u.
* Assignment EPG from DVB or XML for IPTV services (Enigma2 only).
* Preview (playing) IPTV or other streams directly from the bouquet list (should be installed VLC).
### Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings.
### Launching
### Launching:
To start the program, in most cases it is enough to download the archive, unpack and run it by
double clicking on DemonEditor.desktop in the root directory, or launching from the console
with the command: ```./start.py```
@@ -39,12 +45,13 @@ Extra folders can be deleted, excluding the *app* folder and root files like *De
### Note.
To create a simple **debian package**, you can use the *build-deb.sh.*
Tests only with openATV image and Formuler F1 receiver in my preferred Linux distros
(latest Linux Mint 18.* and 19 MATE 64-bit)!
The program is tested only with openATV image and Formuler F1 receiver in my favourite Linux distributions.
(the latest versions of Linux Mint 18.* and 19* MATE 64-bit)!
**Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!**
**Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!**
Main supported **lamedb** format is version **4**. Versions **3** and **5** has only experimental support!
For version **3** is only read mode available. When saving, version **4** format is used instead!
**Important:**
Main supported **lamedb** format is version **4**. Versions **3** and **5** has only experimental support!
For version **3** is only read mode available. When saving, version **4** format is used instead!

View File

@@ -7,11 +7,19 @@ from gi.repository import GLib
_LOG_FILE = "demon-editor.log"
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
_LOGGER_NAME = "main_logger"
logging.Logger(_LOGGER_NAME)
logging.basicConfig(level=logging.INFO,
filename=_LOG_FILE,
format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT)
_USE_LOG = False
def init_logger():
global _USE_LOG
_USE_LOG = True
logging.Logger(_LOGGER_NAME)
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT,
handlers=[logging.FileHandler(_LOG_FILE),
logging.StreamHandler()])
log("Logging is enabled.", level=logging.INFO)
def get_logger():
@@ -19,7 +27,7 @@ def get_logger():
def log(message, level=logging.ERROR):
get_logger().log(level, message)
get_logger().log(level, message) if _USE_LOG else print(message)
def run_idle(func):

View File

@@ -28,6 +28,7 @@ class DownloadType(Enum):
SATELLITES = 2
PICONS = 3
WEBTV = 4
EPG = 5
class HttpRequestType(Enum):
@@ -41,14 +42,14 @@ class TestException(Exception):
pass
def download_data(*, properties, download_type=DownloadType.ALL, callback=None):
def download_data(*, properties, download_type=DownloadType.ALL, callback):
with FTP(host=properties["host"], user=properties["user"], passwd=properties["password"]) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
save_path = properties["data_dir_path"]
os.makedirs(os.path.dirname(save_path), exist_ok=True)
files = []
# bouquets section
# bouquets
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(properties["services_path"])
ftp.dir(files.append)
@@ -58,7 +59,7 @@ def download_data(*, properties, download_type=DownloadType.ALL, callback=None):
if name.endswith(file_list):
name = name.split()[-1]
download_file(ftp, name, save_path, callback)
# satellites.xml and webtv section
# satellites.xml and webtv
if download_type in (DownloadType.ALL, DownloadType.SATELLITES, DownloadType.WEBTV):
ftp.cwd(properties["satellites_xml_path"])
files.clear()
@@ -70,6 +71,20 @@ def download_data(*, properties, download_type=DownloadType.ALL, callback=None):
download_file(ftp, _SAT_XML_FILE, save_path, callback)
if download_type in (DownloadType.ALL, DownloadType.WEBTV) and name.endswith(_WEBTV_XML_FILE):
download_file(ftp, _WEBTV_XML_FILE, save_path, callback)
# epg.dat
if download_type is DownloadType.EPG:
stb_path = properties["services_path"]
epg_options = properties.get("epg_options", None)
if epg_options:
stb_path = epg_options.get("epg_dat_stb_path", stb_path)
save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(stb_path)
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith("epg.dat"):
name = name.split()[-1]
download_file(ftp, name, save_path, callback)
if callback is not None:
callback("\nDone.\n")
@@ -292,10 +307,11 @@ def test_ftp(host, port, user, password, timeout=5):
raise TestException(e)
def test_http(host, port, user, password, timeout=5):
def test_http(host, port, user, password, timeout=5, skip_message=False):
try:
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
url = "http://{}:{}/api/message?{}".format(host, port, params)
params = "statusinfo" if skip_message else "message?{}".format(params)
url = "http://{}:{}/api/{}".format(host, port, params)
# authentication
init_auth(user, password, url)

View File

@@ -14,9 +14,9 @@ def get_bouquets(path):
def write_bouquets(path, bouquets):
srv_line = '#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
line = []
pattern = re.compile("[^\w_()]+")
pattern = re.compile("[^\\w_()]+")
for bqs in bouquets:
line.clear()
@@ -28,7 +28,7 @@ def write_bouquets(path, bouquets):
bq_name = _DEFAULT_BOUQUET_NAME
else:
bq_name = re.sub(pattern, "_", bq.name)
line.append(srv_line.format(bq_name, bq.type))
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)
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
@@ -90,18 +90,23 @@ def parse_bouquets(path, bq_name, bq_type):
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
bq_pattern = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
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:
b_name, services = get_bouquet(path, line.split(".")[1], bq_type)
bouquets[2].append(Bouquet(name=b_name,
type=bq_type,
services=services,
locked=None,
hidden=None))
name = re.match(bq_pattern, line)
if name:
b_name, services = get_bouquet(path, name.group(1), bq_type)
bouquets[2].append(Bouquet(name=b_name,
type=bq_type,
services=services,
locked=None,
hidden=None))
else:
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets

View File

@@ -1,4 +1,5 @@
""" Module for IPTV and streams support """
import re
import urllib.request
from enum import Enum
@@ -15,6 +16,8 @@ MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
class StreamType(Enum):
DVB_TS = "1"
NONE_TS = "4097"
NONE_REC_1 = "5001"
NONE_REC_2 = "5002"
def parse_m3u(path, profile):
@@ -51,5 +54,28 @@ def parse_m3u(path, profile):
return services
def export_to_m3u(path, bouquet, profile):
pattern = re.compile(".*:(http.*):.*") if profile is Profile.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
for s in bouquet.services:
s_type = s.type
if s_type is BqServiceType.IPTV:
res = re.match(pattern, s.data)
if not res:
continue
data = res.group(1)
lines.append("#EXTINF:-1,{}\n".format(s.name))
if current_grp:
lines.append(current_grp)
lines.append("{}\n".format(urllib.request.unquote(data.strip())))
elif s_type is BqServiceType.MARKER:
current_grp = "#EXTGRP:{}\n".format(s.name)
with open(path + "{}.m3u".format(bouquet.name), "w", encoding="utf-8") as file:
file.writelines(lines)
if __name__ == "__main__":
pass

View File

@@ -50,10 +50,9 @@ def parse_bouquets(file, name, bq_type):
if BqType(bq_type) is BqType.BOUQUET:
for bq in bouquets.bouquets:
if bq.services:
name = bq.name
name = name[name.index("]") + 1:]
key = int(bq.services[0].data.split(":")[1], 16)
if key not in PROVIDER:
pos, sep, name = bq.name.partition("]")
PROVIDER[key] = name
return bouquets

View File

@@ -2,13 +2,10 @@
For more info see __COMMENT
"""
from functools import lru_cache
from xml.dom.minidom import parse, Document
import os
from app.commons import log
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, PLS_MODE, Transponder, Satellite, get_key_by_value
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, Transponder, Satellite, get_key_by_value
__COMMENT = (" File was created in DemonEditor\n\n"
"usable flags are\n"
@@ -33,7 +30,7 @@ __COMMENT = (" File was created in DemonEditor\n\n"
def get_satellites(path):
return parse_satellites(path, os.path.getsize(path))
return parse_satellites(path)
def write_satellites(satellites, data_path):
@@ -60,7 +57,7 @@ def write_satellites(satellites, data_path):
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
if tr.pls_mode:
transponder_child.setAttribute("pls_mode", get_key_by_value(PLS_MODE, tr.pls_mode))
transponder_child.setAttribute("pls_mode", tr.pls_mode)
if tr.pls_code:
transponder_child.setAttribute("pls_code", tr.pls_code)
if tr.is_id:
@@ -88,7 +85,7 @@ def parse_transponders(elem, sat_name):
FEC[atr["fec_inner"].value],
SYSTEM[atr["system"].value],
MODULATION[atr["modulation"].value],
PLS_MODE[atr["pls_mode"].value] if "pls_mode" in atr else None,
atr["pls_mode"].value if "pls_mode" in atr else None,
atr["pls_code"].value if "pls_code" in atr else None,
atr["is_id"].value if "is_id" in atr else None)
except Exception as e:
@@ -109,8 +106,7 @@ def parse_sat(elem):
parse_transponders(elem, sat_name))
@lru_cache(maxsize=1)
def parse_satellites(path, file_size):
def parse_satellites(path):
""" Parsing satellites from xml"""
dom = parse(path)
satellites = []

View File

@@ -48,7 +48,8 @@ def get_default_settings():
"backup_dir_path": DATA_PATH + "enigma2/backup/",
"backup_before_save": True, "backup_before_downloading": True,
"v5_support": False, "http_api_support": False,
"use_colors": True, "new_color": "rgb(255,230,204)", "extra_color": "rgb(179,230,204)"},
"use_colors": True, "new_color": "rgb(255,230,204)", "extra_color": "rgb(179,230,204)",
"fav_click_mode": 0},
Profile.NEUTRINO_MP.value: {
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root",
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2,
@@ -57,7 +58,8 @@ def get_default_settings():
"satellites_xml_path": "/var/tuxbox/config/", "data_dir_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/", "picons_dir_path": DATA_PATH + "neutrino/picons/",
"backup_dir_path": DATA_PATH + "neutrino/backup/",
"backup_before_save": True, "backup_before_downloading": True},
"backup_before_save": True, "backup_before_downloading": True,
"fav_click_mode": 0},
"profile": Profile.ENIGMA_2.value}

110
app/tools/epg.py Normal file
View File

@@ -0,0 +1,110 @@
""" Module for working with epg.dat file """
import struct
from datetime import datetime
from xml.dom.minidom import parse, Node, Document
from app.eparser.ecommons import BqServiceType, BouquetService
class EPG:
@staticmethod
def get_epg_refs(path):
""" The read algorithm was taken from the eEPGCache::load() function from this source:
https://github.com/OpenPLi/enigma2/blob/44d9b92f5260c7de1b3b3a1b9a9cbe0f70ca4bf0/lib/dvb/epgcache.cpp#L1300
"""
refs = set()
with open(path, mode="rb") as f:
crc = struct.unpack("<I", f.read(4))[0]
if crc != int(0x98765432):
raise ValueError("Epg file has incorrect byte order!")
header = f.read(13).decode()
if header != "ENIGMA_EPG_V7":
raise ValueError("Unsupported format of epd.dat file!")
channels_count = struct.unpack("<I", f.read(4))[0]
for i in range(channels_count):
sid, nid, tsid, events_size = struct.unpack("<IIII", f.read(16))
service_id = "{:X}:{:X}:{:X}".format(sid, tsid, nid)
for j in range(events_size):
_type, _len = struct.unpack("<BB", f.read(2))
f.read(10)
n_crc = (_len - 10) // 4
if n_crc > 0:
[f.read(4) for n in range(n_crc)]
refs.add(service_id)
return refs
class ChannelsParser:
_COMMENT = "File was created in DemonEditor"
@staticmethod
def get_refs_from_xml(path):
""" Returns tuple from references and description. """
refs = []
dom = parse(path)
description = "".join(n.data + "\n" for n in dom.childNodes if n.nodeType == Node.COMMENT_NODE)
for elem in dom.getElementsByTagName("channels"):
c_count = 0
comment_count = 0
current_data = ""
if elem.hasChildNodes():
for n in elem.childNodes:
if n.nodeType == Node.COMMENT_NODE:
c_count += 1
comment_count += 1
txt = n.data.strip()
if comment_count:
comment_count -= 1
else:
ref_data = current_data.split(":")
refs.append(BouquetService(name=txt,
type=BqServiceType.DEFAULT,
data="{}:{}:{}:{}".format(*ref_data[3:7]).upper(),
num="{}:{}:{}".format(*ref_data[3:6]).upper()))
if n.hasChildNodes():
for s_node in n.childNodes:
if s_node.nodeType == Node.TEXT_NODE:
comment_count -= 1
current_data = s_node.data
return refs, description
@staticmethod
def write_refs_to_xml(path, services):
header = '<?xml version="1.0" encoding="utf-8"?>\n<!-- {} -->\n<!-- {} -->\n<channels>\n'.format(
"Created in DemonEditor.", datetime.now().strftime("%d.%m.%Y %H:%M:%S"))
doc = Document()
lines = [header]
for srv in services:
srv_type = srv.type
if srv_type is BqServiceType.IPTV:
channel_child = doc.createElement("channel")
channel_child.setAttribute("id", str(srv.num))
data = srv.data.strip().split(":")
channel_child.appendChild(doc.createTextNode(":".join(data[:10])))
comment = doc.createComment(srv.name)
lines.append("{} {}\n".format(str(channel_child.toxml()), str(comment.toxml())))
elif srv_type is BqServiceType.MARKER:
comment = doc.createComment(srv.name)
lines.append("{}\n".format(str(comment.toxml())))
lines.append("</channels>")
doc.unlink()
with open(path, "w", encoding="utf-8") as f:
f.writelines(lines)
if __name__ == "__main__":
pass

View File

@@ -2,11 +2,14 @@
for replace or update current satellites.xml file.
"""
import re
import requests
from enum import Enum
from html.parser import HTMLParser
from app.commons import log
from app.eparser import Satellite, Transponder, is_transponder_valid
from app.eparser.ecommons import PLS_MODE
class SatelliteSource(Enum):
@@ -79,14 +82,14 @@ class SatellitesParser(HTMLParser):
try:
request = requests.get(url=src, headers=self._HEADERS)
except requests.exceptions.ConnectionError as e:
print(repr(e))
log(repr(e))
return []
else:
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
print(reason)
log(reason)
if self._rows:
if self._source is SatelliteSource.FLYSAT:
@@ -102,7 +105,9 @@ class SatellitesParser(HTMLParser):
r_len = len(row)
if r_len == 7:
current_pos = self.parse_position(row[2])
sats.append((row[4], current_pos, row[5], row[1], False))
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, row[5], row[1], False)) # coupled [all in one] satellites
sats.append((row[4], current_pos, row[5], row[3], False))
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
@@ -123,7 +128,7 @@ class SatellitesParser(HTMLParser):
def get_satellite(self, sat):
pos = sat[1]
return Satellite(name=sat[0] + " ({})".format(pos),
return Satellite(name="{} {}".format(pos, sat[0]),
flags="0",
position=self.get_position(pos.replace(".", "")),
transponders=self.get_transponders(sat[3]))
@@ -137,6 +142,7 @@ class SatellitesParser(HTMLParser):
return "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
def get_transponders(self, sat_url):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=self._HEADERS)
@@ -148,13 +154,23 @@ class SatellitesParser(HTMLParser):
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
return trs
return sorted(trs, key=lambda x: int(x.frequency))
def get_transponders_for_fly_sat(self, trs):
""" Parsing transponders for FlySat """
pls_pattern = re.compile("(PLS:)+ (Root|Gold|Combo)+ (\\d+)?")
is_id_pattern = re.compile("(Stream) (\\d+)")
pls_modes = {v: k for k, v in PLS_MODE.items()}
n_trs = []
if self._rows:
zeros = "000"
is_ids = []
for r in self._rows:
if len(r) == 1:
is_ids.extend(re.findall(is_id_pattern, r[0]))
continue
if len(r) < 3:
continue
data = r[2].split(" ")
@@ -171,18 +187,41 @@ class SatellitesParser(HTMLParser):
sys, mod = sys
mod = "QPSK" if sys == "DVB-S" else mod
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, None, None)
if is_transponder_valid(tr):
trs.append(tr)
pls = re.findall(pls_pattern, r[1])
pls_code = None
pls_mode = None
if pls:
pls_code = pls[0][2]
pls_mode = pls_modes.get(pls[0][1], None)
if is_ids:
tr = trs.pop()
for index, is_id in enumerate(is_ids):
tr = tr._replace(is_id=is_id[1])
if is_transponder_valid(tr):
n_trs.append(tr)
else:
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, None)
if is_transponder_valid(tr):
trs.append(tr)
is_ids.clear()
trs.extend(n_trs)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat """
frq_pol_pattern = re.compile("(\d{4,5}).*([RLHV])(.*\d$)")
sr_fec_pattern = re.compile("^(\d{4,5})-(\d/\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?)(.*)?")
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
sr_fec_pattern = re.compile("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
re.IGNORECASE)
zeros = "000"
pls_modes = {v: k for k, v in PLS_MODE.items()}
for r in filter(lambda x: len(x) > 8, self._rows):
freq = re.match(frq_pol_pattern, r[2])
for frq in r[1], r[2], r[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
break
if not freq:
continue
frq, pol = freq.group(1), freq.group(2)
@@ -191,12 +230,18 @@ class SatellitesParser(HTMLParser):
continue
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
mod = mod.strip() if mod else "Auto"
sys = re.match(sys_pattern, r[-4])
if not sys:
continue
sys = sys.group(1)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, None, None, None)
res = re.match(sys_pattern, r[-4])
if not res:
continue
sys = res.group(1)
pls_mode = res.group(3)
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
pls_code = res.group(4)
pls_id = res.group(6)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
if is_transponder_valid(tr):
trs.append(tr)

View File

@@ -185,17 +185,29 @@ class BackupDialog:
self.restore(RestoreType.BOUQUETS)
def backup_data(path, backup_path):
""" Creating data backup from a folder at the specified path """
def backup_data(path, backup_path, move=True):
""" Creating data backup from a folder at the specified path
Returns full path to the compressed file.
"""
backup_path = "{}{}/".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
# backup files in data dir(skipping dirs and satellites.xml)
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
shutil.move(os.path.join(path, file), backup_path + file)
src, dst = os.path.join(path, file), backup_path + file
shutil.move(src, dst) if move else shutil.copy(src, dst)
# compressing to zip and delete remaining files
shutil.make_archive(backup_path, "zip", backup_path)
zip_file = shutil.make_archive(backup_path, "zip", backup_path)
shutil.rmtree(backup_path)
return zip_file
def restore_data(src, dst):
""" Unpacks backup data. """
clear_data_path(dst)
shutil.unpack_archive(src, dst)
def clear_data_path(path):
""" Clearing data at the specified path excluding satellites.xml file """

View File

@@ -40,8 +40,8 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">0.4.3 Pre-alpha</property>
<property name="copyright">2018 Dmitriy Yefremov
<property name="version">0.4.5 Pre-alpha</property>
<property name="copyright">2018-2019 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellites list editor for GNU/Linux</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
@@ -75,121 +75,57 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkMessageDialog" id="error_dialog">
<property name="width_request">320</property>
<object class="GtkDialog" id="input_dialog">
<property name="use-header-bar">{use_header}</property>
<property name="title" translatable="yes">Transponder</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">{title}</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center</property>
<property name="default_width">320</property>
<property name="default_height">240</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">accessories-text-editor</property>
<property name="type_hint">dialog</property>
<property name="message_type">error</property>
<property name="buttons">ok</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="error_dialog_vbox">
<property name="can_focus">False</property>
<property name="resize_mode">immediate</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="messagedialog-action_area8">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="action">
<object class="GtkButton" id="input_dialog_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
</object>
<object class="GtkDialog" id="input_dialog">
<property name="can_focus">False</property>
<property name="title"> </property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">gtk-edit</property>
<property name="type_hint">dialog</property>
<child type="titlebar">
<placeholder/>
<child type="action">
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="input_dialog_vbox">
<property name="width_request">320</property>
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area2">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button3">
<property name="label">gtk-undo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button4">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</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">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="input_dialog_box">
<object class="GtkEntry" id="input_entry">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkEntry" id="input_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<property name="can_focus">True</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
</object>
<packing>
<property name="expand">False</property>
@@ -200,108 +136,10 @@ Author: Dmitriy Yefremov
</object>
</child>
<action-widgets>
<action-widget response="-6">button3</action-widget>
<action-widget response="-5">button4</action-widget>
<action-widget response="cancel">input_dialog_cancel_button</action-widget>
<action-widget response="ok">input_dialog_ok_button</action-widget>
</action-widgets>
</object>
<object class="GtkFileChooserDialog" id="path_chooser_dialog">
<property name="can_focus">False</property>
<property name="title" translatable="yes"> </property>
<property name="modal">True</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">document-open</property>
<property name="type_hint">dialog</property>
<property name="action">select-folder</property>
<property name="do_overwrite_confirmation">True</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="filechooser_dialog_vbox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="filechooser_dialog_action_area">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button2">
<property name="label">gtk-undo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button1">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</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">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-6">button2</action-widget>
<action-widget response="-12">button1</action-widget>
</action-widgets>
</object>
<object class="GtkMessageDialog" id="question_dialog">
<property name="width_request">320</property>
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="default_width">320</property>
<property name="default_height">240</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="message_type">question</property>
<property name="buttons">ok-cancel</property>
<property name="text" translatable="yes">Are you sure?</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="question_dialog_vbox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="messagedialog-action_area">
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkDialog" id="wait_dialog">
<property name="can_focus">False</property>
<property name="resizable">False</property>
@@ -316,8 +154,8 @@ Author: Dmitriy Yefremov
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox4">
<property name="width_request">118</property>
<object class="GtkBox" id="wait_dialog_vbox">
<property name="width_request">120</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child internal-child="action_area">

View File

@@ -1,9 +1,32 @@
""" Common module for showing dialogs """
import locale
from enum import Enum
from functools import lru_cache
from app.commons import run_idle
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
class Dialog(Enum):
MESSAGE = """
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkMessageDialog" id="message_dialog">
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="default_width">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<property name="message_type">{message_type}</property>
<property name="buttons">{buttons_type}</property>
</object>
</interface>
"""
class Action(Enum):
@@ -12,12 +35,16 @@ class Action(Enum):
class DialogType(Enum):
INPUT = "input_dialog"
CHOOSER = "path_chooser_dialog"
ERROR = "error_dialog"
QUESTION = "question_dialog"
ABOUT = "about_dialog"
WAIT = "wait_dialog"
INPUT = "input"
CHOOSER = "chooser"
ERROR = "error"
QUESTION = "question"
INFO = "info"
ABOUT = "about"
WAIT = "wait"
def __str__(self):
return self.value
class WaitDialog:
@@ -42,54 +69,16 @@ class WaitDialog:
def show_dialog(dialog_type: DialogType, transient, text=None, options=None, action_type=None, file_filter=None):
""" Shows dialogs by name """
builder, dialog = get_dialog_from_xml(dialog_type, transient)
if dialog_type is DialogType.CHOOSER and options:
if action_type is not None:
dialog.set_action(action_type)
if file_filter is not None:
dialog.add_filter(file_filter)
path = options.get("data_dir_path")
dialog.set_current_folder(path)
response = dialog.run()
if response == -12: # -12 for fix assertion 'gtk_widget_get_can_default (widget)' failed
if dialog.get_filename():
path = dialog.get_filename()
if action_type is not Gtk.FileChooserAction.OPEN:
path = path + "/"
response = path
dialog.destroy()
return response
if dialog_type is DialogType.INPUT:
entry = builder.get_object("input_entry")
entry.set_text(text if text else "")
response = dialog.run()
txt = entry.get_text()
dialog.destroy()
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
if text:
dialog.set_markup(get_message(text))
response = dialog.run()
dialog.destroy()
return response
def get_dialog_from_xml(dialog_type, transient):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", (dialog_type.value,))
dialog = builder.get_object(dialog_type.value)
dialog.set_transient_for(transient)
return builder, dialog
if dialog_type in (DialogType.INFO, DialogType.ERROR):
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
elif dialog_type is DialogType.CHOOSER and options:
return get_file_chooser_dialog(transient, text, options, action_type, file_filter)
elif dialog_type is DialogType.INPUT:
return get_input_dialog(transient, text)
elif dialog_type is DialogType.QUESTION:
return get_message_dialog(transient, DialogType.QUESTION, Gtk.ButtonsType.OK_CANCEL, "Are you sure?")
elif dialog_type is DialogType.ABOUT:
return get_about_dialog(transient)
def get_chooser_dialog(transient, options, pattern, name):
@@ -104,10 +93,84 @@ def get_chooser_dialog(transient, options, pattern, name):
file_filter=file_filter)
def get_file_chooser_dialog(transient, text, options, action_type, file_filter):
dialog = Gtk.FileChooserDialog(get_message(text) if text else "", transient,
action_type if action_type is not None else Gtk.FileChooserAction.SELECT_FOLDER,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
use_header_bar=IS_GNOME_SESSION)
if file_filter is not None:
dialog.add_filter(file_filter)
path = options.get("data_dir_path")
dialog.set_current_folder(path)
response = dialog.run()
if response == Gtk.ResponseType.OK:
if dialog.get_filename():
path = dialog.get_filename()
if action_type is not Gtk.FileChooserAction.OPEN:
path = path + "/"
response = path
dialog.destroy()
return response
def get_input_dialog(transient, text):
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=IS_GNOME_SESSION)
entry = builder.get_object("input_entry")
entry.set_text(text if text else "")
response = dialog.run()
txt = entry.get_text()
dialog.destroy()
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
def get_message_dialog(transient, message_type, buttons_type, text):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
dialog_str = Dialog.MESSAGE.value.format(use_header=0, message_type=message_type, buttons_type=int(buttons_type))
builder.add_from_string(dialog_str)
dialog = builder.get_object("message_dialog")
dialog.set_transient_for(transient)
dialog.set_markup(get_message(text))
response = dialog.run()
dialog.destroy()
return response
def get_about_dialog(transient):
builder, dialog = get_dialog_from_xml(DialogType.ABOUT, transient)
dialog.set_transient_for(transient)
response = dialog.run()
dialog.destroy()
return response
def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
dialog_name = dialog_type.value + "_dialog"
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
dialog_str = get_dialogs_string(UI_RESOURCES_PATH + "dialogs.glade").format(use_header=use_header, title=title)
builder.add_objects_from_string(dialog_str, (dialog_name,))
dialog = builder.get_object(dialog_name)
dialog.set_transient_for(transient)
return builder, dialog
def get_message(message):
""" returns translated message """
return locale.dgettext(TEXT_DOMAIN, message)
@lru_cache(maxsize=5)
def get_dialogs_string(path):
with open(path, "r") as f:
return "".join(f)
if __name__ == "__main__":
pass

View File

@@ -3,7 +3,7 @@ from gi.repository import GLib
from app.commons import run_idle, run_task
from app.connections import download_data, DownloadType, upload_data
from app.properties import Profile, get_config
from app.ui.backup import backup_data
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
@@ -11,10 +11,11 @@ from .dialogs import show_dialog, DialogType, get_message
class DownloadDialog:
def __init__(self, transient, properties, open_data_callback, profile=Profile.ENIGMA_2):
def __init__(self, transient, properties, open_data_callback, update_settings_callback, profile=Profile.ENIGMA_2):
self._profile_properties = properties.get(profile.value)
self._properties = properties
self._open_data_callback = open_data_callback
self._update_settings_callback = update_settings_callback
self._profile = profile
handlers = {"on_receive": self.on_receive,
@@ -50,28 +51,24 @@ class DownloadDialog:
self._timeout_entry = builder.get_object("timeout_entry")
self._settings_buttons_box = builder.get_object("settings_buttons_box")
self._use_http_switch = builder.get_object("use_http_switch")
self._http_radio_button = builder.get_object("http_radio_button")
self._use_http_box = builder.get_object("use_http_box")
self.init_properties()
if profile is Profile.NEUTRINO_MP:
self._webtv_radio_button.set_visible(True)
builder.get_object("http_radio_button").set_visible(False)
builder.get_object("use_http_box").set_visible(False)
self._use_http_switch.set_active(False)
def show(self):
self._dialog_window.show()
def init_properties(self):
self._host_entry.set_text(self._profile_properties["host"])
self._data_path_entry.set_text(self._profile_properties["data_dir_path"])
is_enigma = self._profile is Profile.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._http_radio_button.set_visible(is_enigma)
self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma)
@run_idle
def on_receive(self, item):
if self._profile_properties.get("backup_before_downloading", True):
data_path = self._profile_properties.get("data_dir_path", self._data_path_entry.get_text())
backup_path = self._profile_properties.get("backup_dir_path", data_path + "backup/")
backup_data(data_path, backup_path)
self.download(True, self.get_download_type())
@run_idle
@@ -114,12 +111,14 @@ class DownloadDialog:
def on_preferences(self, item):
show_settings_dialog(self._dialog_window, self._properties)
self._profile = Profile(self._properties.get("profile", Profile.ENIGMA_2.value))
self._profile_properties = get_config().get(self._profile.value)
self.init_properties()
self._update_settings_callback()
for button in self._settings_buttons_box.get_children():
if button.get_active():
self.on_settings_button(button)
self.init_properties()
break
def on_info_bar_close(self, bar=None, resp=None):
@@ -128,11 +127,16 @@ class DownloadDialog:
@run_task
def download(self, download, d_type):
""" Download/upload data from/to receiver """
try:
self._expander.set_expanded(True)
self.clear_output()
self._expander.set_expanded(True)
self.clear_output()
backup, backup_src, data_path = self._profile_properties.get("backup_before_downloading", True), None, None
try:
if download:
if backup and d_type is not DownloadType.SATELLITES:
data_path = self._profile_properties.get("data_dir_path", self._data_path_entry.get_text())
backup_path = self._profile_properties.get("backup_dir_path", data_path + "backup/")
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
download_data(properties=self._profile_properties, download_type=d_type, callback=self.append_output)
else:
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
@@ -146,6 +150,8 @@ class DownloadDialog:
except Exception as e:
message = str(getattr(e, "message", str(e)))
self.show_info_message(message, Gtk.MessageType.ERROR)
if all((download, backup, data_path)):
restore_data(backup_src, data_path)
else:
if download and d_type is not DownloadType.SATELLITES:
GLib.idle_add(self._open_data_callback)

1283
app/ui/epg_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

549
app/ui/epg_dialog.py Normal file
View File

@@ -0,0 +1,549 @@
import gzip
import locale
import os
import re
import shutil
import urllib.request
from enum import Enum
from urllib.error import HTTPError, URLError
from gi.repository import GLib
from app.commons import run_idle
from app.connections import download_data, DownloadType
from app.eparser.ecommons import BouquetService, BqServiceType
from app.tools.epg import EPG, ChannelsParser
from app.ui.dialogs import get_message, show_dialog, DialogType
from .main_helper import on_popup_menu, update_entry_data
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey
class RefsSource(Enum):
SERVICES = 0
XML = 1
class EpgDialog:
def __init__(self, transient, options, services, bouquet, fav_model, bouquet_name):
handlers = {"on_close_dialog": self.on_close_dialog,
"on_apply": self.on_apply,
"on_update": self.on_update,
"on_save_to_xml": self.on_save_to_xml,
"on_auto_configuration": self.on_auto_configuration,
"on_filter_toggled": self.on_filter_toggled,
"on_filter_changed": self.on_filter_changed,
"on_info_bar_close": self.on_info_bar_close,
"on_popup_menu": on_popup_menu,
"on_bouquet_popup_menu": self.on_bouquet_popup_menu,
"on_copy_ref": self.on_copy_ref,
"on_assign_ref": self.on_assign_ref,
"on_reset": self.on_reset,
"on_list_reset": self.on_list_reset,
"on_drag_begin": self.on_drag_begin,
"on_drag_data_get": self.on_drag_data_get,
"on_drag_data_received": self.on_drag_data_received,
"on_resize": self.on_resize,
"on_names_source_changed": self.on_names_source_changed,
"on_options_save": self.on_options_save,
"on_use_web_source_switch": self.on_use_web_source_switch,
"on_enable_filtering_switch": self.on_enable_filtering_switch,
"on_update_on_start_switch": self.on_update_on_start_switch,
"on_field_icon_press": self.on_field_icon_press,
"on_key_release": self.on_key_release}
self._services = {}
self._ex_services = services
self._ex_fav_model = fav_model
self._options = options
self._bouquet = bouquet
self._bouquet_name = bouquet_name
self._current_ref = []
self._enable_dat_filter = False
self._use_web_source = False
self._update_epg_data_on_start = False
self._refs_source = RefsSource.SERVICES
self._show_tooltips = True
self._download_xml_is_active = False
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "epg_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(transient)
self._source_view = builder.get_object("source_view")
self._bouquet_view = builder.get_object("bouquet_view")
self._bouquet_model = builder.get_object("bouquet_list_store")
self._services_model = builder.get_object("services_list_store")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._assign_ref_popup_item = builder.get_object("bouquet_assign_ref_popup_item")
self._left_header_box = builder.get_object("left_header_box")
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_entry = builder.get_object("filter_entry")
self._services_filter_model = builder.get_object("services_filter_model")
self._services_filter_model.set_visible_func(self.services_filter_function)
# Info
self._source_count_label = builder.get_object("source_count_label")
self._source_info_label = builder.get_object("source_info_label")
self._bouquet_count_label = builder.get_object("bouquet_count_label")
self._bouquet_epg_count_label = builder.get_object("bouquet_epg_count_label")
# Options
self._xml_radiobutton = builder.get_object("xml_radiobutton")
self._xml_chooser_button = builder.get_object("xml_chooser_button")
self._names_source_box = builder.get_object("names_source_box")
self._web_source_box = builder.get_object("web_source_box")
self._use_web_source_switch = builder.get_object("use_web_source_switch")
self._url_to_xml_entry = builder.get_object("url_to_xml_entry")
self._enable_filtering_switch = builder.get_object("enable_filtering_switch")
self._epg_dat_path_entry = builder.get_object("epg_dat_path_entry")
self._epg_dat_stb_path_entry = builder.get_object("epg_dat_stb_path_entry")
self._update_on_start_switch = builder.get_object("update_on_start_switch")
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
# Setting the last size of the dialog window
window_size = self._options.get("epg_tool_window_size", None)
if window_size:
self._dialog.resize(*window_size)
self.init_drag_and_drop()
self.on_update()
def show(self):
self._dialog.show()
def on_close_dialog(self, window, event):
self._download_xml_is_active = False
@run_idle
def on_apply(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self._bouquet.clear()
list(map(self._bouquet.append, [r[Column.FAV_ID] for r in self._bouquet_model]))
for index, row in enumerate(self._ex_fav_model):
fav_id = self._bouquet[index]
row[Column.FAV_ID] = fav_id
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name:
old_fav_id = self._services[fav_id]
srv = self._ex_services.pop(old_fav_id, None)
if srv:
self._ex_services[fav_id] = srv._replace(fav_id=fav_id)
self._dialog.destroy()
@run_idle
def on_update(self, item=None):
self.clear_data()
self.init_options()
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_data(self):
self._services_model.clear()
self._bouquet_model.clear()
self._services.clear()
self._source_info_label.set_text("")
self._bouquet_epg_count_label.set_text("")
self.on_info_bar_close()
def init_data(self):
gen = self.init_bouquet_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
refs = None
if self._enable_dat_filter:
if self._update_epg_data_on_start:
try:
self.download_epg_from_stb()
except OSError as e:
self.show_info_message("Download epg.dat file error: {}".format(e), Gtk.MessageType.ERROR)
return
yield True
try:
refs = EPG.get_epg_refs(self._epg_dat_path_entry.get_text() + "epg.dat")
except FileNotFoundError as e:
self.show_info_message("Read data error: {}".format(e), Gtk.MessageType.ERROR)
return
yield True
if self._refs_source is RefsSource.SERVICES:
self.init_lamedb_source(refs)
elif self._refs_source is RefsSource.XML:
xml_gen = self.init_xml_source(refs)
try:
yield from xml_gen
except ValueError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message("Unknown names source!", Gtk.MessageType.ERROR)
yield True
def init_bouquet_data(self):
for r in self._ex_fav_model:
row = [*r[:]]
fav_id = r[Column.FAV_ID]
self._services[fav_id] = self._ex_services[fav_id].fav_id
yield self._bouquet_model.append(row)
self._bouquet_count_label.set_text(str(len(self._bouquet_model)))
yield True
def init_lamedb_source(self, refs):
srvs = {k[:k.rfind(":")]: v for k, v in self._ex_services.items()}
s_types = (BqServiceType.MARKER.value, BqServiceType.IPTV.value)
filtered = filter(None, [srvs.get(ref) for ref in refs]) if refs else filter(
lambda s: s.service_type not in s_types, self._ex_services.values())
list(map(self._services_model.append, map(lambda s: (s.service, s.fav_id), filtered)))
self.update_source_count_info()
def init_xml_source(self, refs):
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
if not path:
self.show_info_message("The path to the xml file is not set!", Gtk.MessageType.ERROR)
return
if self._use_web_source:
# Downloading gzipped xml file that contains services names with references from the web.
self._download_xml_is_active = True
self.update_active_header_elements(False)
url = self._url_to_xml_entry.get_text()
try:
with urllib.request.urlopen(url, timeout=2) as fp:
headers = fp.info()
content_type = headers.get("Content-Type", "")
if content_type != "application/gzip":
self._download_xml_is_active = False
raise ValueError("{} {} {}".format(get_message("Download XML file error."),
get_message("Unsupported file type:"),
content_type))
file_name = os.path.basename(url)
data_path = self._epg_dat_path_entry.get_text()
with open(data_path + file_name, "wb") as tfp:
bs = 1024 * 8
size = -1
read = 0
b_num = 0
if "content-length" in headers:
size = int(headers["Content-Length"])
while self._download_xml_is_active:
block = fp.read(bs)
if not block:
break
read += len(block)
tfp.write(block)
b_num += 1
self.update_download_progress(b_num * bs / size)
yield True
path = tfp.name.rstrip(".gz")
except (HTTPError, URLError) as e:
raise ValueError("{} {}".format(get_message("Download XML file error."), e))
else:
try:
with open(path, "wb") as f_out:
with gzip.open(tfp.name, "rb") as f:
shutil.copyfileobj(f, f_out)
os.remove(tfp.name)
except Exception as e:
raise ValueError("{} {}".format(get_message("Unpacking data error."), e))
finally:
self._download_xml_is_active = False
self.update_active_header_elements(True)
try:
s_refs, info = ChannelsParser.get_refs_from_xml(path)
yield True
except Exception as e:
raise ValueError("{} {}".format(get_message("XML parsing error:"), e))
else:
if refs:
s_refs = filter(lambda x: x.num in refs, s_refs)
list(map(lambda s: self._services_model.append((s.name, s.data)), s_refs))
self.update_source_info(info)
self.update_source_count_info()
yield True
def on_key_release(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if ctrl and key is KeyboardKey.C:
self.on_copy_ref()
elif ctrl and key is KeyboardKey.V:
self.on_assign_ref()
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, options=self._options)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
services = []
iptv_types = (BqServiceType.IPTV.value, BqServiceType.MARKER.value)
for r in self._bouquet_model:
srv_type = r[Column.FAV_TYPE]
if srv_type in iptv_types:
srv = BouquetService(name=r[Column.FAV_SERVICE],
type=BqServiceType(srv_type),
data=r[Column.FAV_ID],
num=r[Column.FAV_NUM])
services.append(srv)
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
@run_idle
def on_auto_configuration(self, item):
""" Simple mapping of services by name. """
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
tr = None
if use_cyrillic:
# may be not entirely correct
symbols = (u"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯІÏҐЎЈЂЉЊЋЏTB",
u"ABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUAIEGUEDLNCJTV")
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
source = {}
for row in self._services_model:
name = re.sub("\\W+", "", str(row[0])).upper()
name = name.translate(tr) if use_cyrillic else name
source[name] = row[1]
success_count = 0
not_founded = {}
for r in self._bouquet_model:
if r[Column.FAV_TYPE] != BqServiceType.IPTV.value:
continue
name = re.sub("\\W+", "", str(r[Column.FAV_SERVICE])).upper()
if use_cyrillic:
name = name.translate(tr)
ref = source.get(name, None) # Not [pop], because the list may contain duplicates or similar names!
if ref:
self.assign_data(r, ref, True)
success_count += 1
else:
not_founded[name] = r
# Additional attempt to search in the remaining elements
for n in not_founded:
for k in source:
if k.startswith(n):
self.assign_data(not_founded[n], source[k], True)
success_count += 1
break
self.update_epg_count()
self.show_info_message("{} {} {}".format(get_message("Done!"),
get_message("Count of successfully configured services:"),
success_count), Gtk.MessageType.INFO)
def assign_data(self, row, ref, show_error=False):
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
if not show_error:
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
return
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = ref.split(":")
new_fav_id = ":".join(fav_id_data)
service = self._services.pop(fav_id, None)
if service:
self._services[new_fav_id] = service
row[Column.FAV_ID] = new_fav_id
row[Column.FAV_LOCKED] = EPG_ICON
row[Column.FAV_TOOLTIP] = ":".join(fav_id_data[:10]) if self._show_tooltips else None
def on_filter_toggled(self, button: Gtk.ToggleButton):
self._filter_bar.set_search_mode(button.get_active())
def on_filter_changed(self, entry):
self._services_filter_model.refilter()
def services_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return model is None or model == "None" or txt in model.get_value(itr, 0).upper()
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_copy_ref(self, item=None):
model, paths = self._source_view.get_selection().get_selected_rows()
self._current_ref.clear()
if paths:
self._current_ref.append(model[paths][1])
def on_assign_ref(self, item=None):
if self._current_ref:
model, paths = self._bouquet_view.get_selection().get_selected_rows()
self.assign_data(model[paths], self._current_ref.pop())
self.update_epg_count()
@run_idle
def on_reset(self, item):
model, paths = self._bouquet_view.get_selection().get_selected_rows()
if paths:
row = self._bouquet_model[paths]
self.reset_row_data(row)
self.update_epg_count()
@run_idle
def on_list_reset(self, item):
list(map(self.reset_row_data, self._bouquet_model))
self.update_epg_count()
def reset_row_data(self, row):
default_fav_id = self._services.pop(row[Column.FAV_ID], None)
if default_fav_id:
self._services[default_fav_id] = default_fav_id
row[Column.FAV_ID], row[Column.FAV_LOCKED], row[Column.FAV_TOOLTIP] = default_fav_id, None, None
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def update_source_info(self, info):
lines = info.split("\n")
self._source_info_label.set_text(lines[0] if lines else "")
self._source_view.set_tooltip_text(info)
@run_idle
def update_source_count_info(self):
source_count = len(self._services_model)
self._source_count_label.set_text(str(source_count))
if self._enable_dat_filter and source_count == 0:
msg = get_message("Current epg.dat file does not contains references for the services of this bouquet!")
self.show_info_message(msg, Gtk.MessageType.WARNING)
@run_idle
def update_epg_count(self):
count = len(list((filter(None, [r[Column.FAV_LOCKED] for r in self._bouquet_model]))))
self._bouquet_epg_count_label.set_text(str(count))
@run_idle
def update_active_header_elements(self, state):
self._left_header_box.set_sensitive(state)
self._xml_download_progress_bar.set_visible(not state)
self._source_info_label.set_text("" if state else "Downloading XML:")
@run_idle
def update_download_progress(self, value):
self._xml_download_progress_bar.set_fraction(value)
def on_bouquet_popup_menu(self, menu, event):
self._assign_ref_popup_item.set_sensitive(self._current_ref)
on_popup_menu(menu, event)
# ***************** Drag-and-drop *********************#
def init_drag_and_drop(self):
""" Enable drag-and-drop """
target = []
self._source_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
self._source_view.drag_source_add_text_targets()
self._bouquet_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._bouquet_view.drag_dest_add_text_targets()
def on_drag_begin(self, view, context):
""" Selects a row under the cursor in the view at the dragging beginning. """
selection = view.get_selection()
if selection.count_selected_rows() > 1:
view.do_toggle_cursor_row(view)
def on_drag_data_get(self, view: Gtk.TreeView, drag_context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if paths:
val = model.get_value(model.get_iter(paths), 1)
data.set_text(val, -1)
def on_drag_data_received(self, view: Gtk.TreeView, drag_context, x, y, data, info, time):
path, pos = view.get_dest_row_at_pos(x, y)
model = view.get_model()
self.assign_data(model[path], data.get_text())
self.update_epg_count()
return False
# ***************** Options *********************#
def init_options(self):
epg_dat_path = self._options.get("data_dir_path", "") + "epg/"
self._epg_dat_path_entry.set_text(epg_dat_path)
default_epg_data_stb_path = "/etc/enigma2"
epg_options = self._options.get("epg_options", None)
if epg_options:
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
self._use_web_source = epg_options.get("use_web_source", False)
self._use_web_source_switch.set_active(self._use_web_source)
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
self._enable_dat_filter = epg_options.get("enable_filtering", False)
self._enable_filtering_switch.set_active(self._enable_dat_filter)
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
self._epg_dat_path_entry.set_text(epg_dat_path)
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
self._update_epg_data_on_start = epg_options.get("epg_data_update_on_start", False)
self._update_on_start_switch.set_active(self._update_epg_data_on_start)
local_xml_path = epg_options.get("local_path_to_xml", None)
if local_xml_path:
self._xml_chooser_button.set_filename(local_xml_path)
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
def on_options_save(self, item=None):
epg_options = {"xml_source": self._xml_radiobutton.get_active(),
"use_web_source": self._use_web_source_switch.get_active(),
"local_path_to_xml": self._xml_chooser_button.get_filename(),
"url_to_xml": self._url_to_xml_entry.get_text(),
"enable_filtering": self._enable_filtering_switch.get_active(),
"epg_dat_path": self._epg_dat_path_entry.get_text(),
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
self._options["epg_options"] = epg_options
def on_resize(self, window):
if self._options:
self._options["epg_tool_window_size"] = window.get_size()
def on_names_source_changed(self, button):
self._refs_source = RefsSource.XML if button.get_active() else RefsSource.SERVICES
self._names_source_box.set_sensitive(button.get_active())
def on_enable_filtering_switch(self, switch, state):
self._epg_dat_source_box.set_sensitive(state)
self._update_on_start_switch.set_active(False if not state else self._update_epg_data_on_start)
def on_update_on_start_switch(self, switch, state):
pass
def on_use_web_source_switch(self, switch, state):
self._web_source_box.set_sensitive(state)
self._xml_chooser_button.set_sensitive(not state)
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._options)
# ***************** Downloads *********************#
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
download_data(properties=self._options, download_type=DownloadType.EPG, callback=print)
if __name__ == "__main__":
pass

388
app/ui/import_dialog.glade Normal file
View File

@@ -0,0 +1,388 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
The MIT License (MIT)
Copyright (c) 2018-2019 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-2019 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkImage" id="remove_selection_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-undo</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="select_all_popup_item">
<property name="label">gtk-select-all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
<property name="label" translatable="yes">Remove selection</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_selection_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">480</property>
<property name="default_height">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="gravity">center</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="subtitle" translatable="yes">Bouquets and services</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="import_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Import</property>
<signal name="clicked" handler="on_import" swapped="no"/>
<child>
<object class="GtkImage" id="import_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">480</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">main_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_name_column">
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
<property name="xalign">0.50999999046325684</property>
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_box">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquet details</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
<property name="width_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="services_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">services_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

224
app/ui/imports.py Normal file
View File

@@ -0,0 +1,224 @@
from contextlib import suppress
from pathlib import Path
from app.commons import run_idle
from app.eparser import get_bouquets, get_services
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.properties import Profile
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
def import_bouquet(transient, profile, model, path, options, services, appender):
""" Import of single bouquet """
itr = model.get_iter(path)
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
pattern, f_pattern = None, None
if profile is Profile.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "userbouquet.*{}".format(pattern)
elif profile is Profile.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
if bq_type is BqType.TV:
f_pattern = "ubouquets.xml"
elif bq_type is BqType.WEBTV:
f_pattern = "webtv.xml"
file_path = get_chooser_dialog(transient, options, f_pattern, "bouquet files")
if file_path == Gtk.ResponseType.CANCEL:
return
if not str(file_path).endswith(pattern):
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
if profile is Profile.ENIGMA_2:
bq = get_enigma2_bouquet(file_path)
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
if len(imported) == 0:
show_dialog(DialogType.ERROR, transient, text="The main list does not contain services for this bouquet!")
return
if model.iter_n_children(itr):
appender(bq, itr)
else:
p_itr = model.iter_parent(itr)
appender(bq, p_itr) if p_itr else appender(bq, itr)
elif profile is Profile.NEUTRINO_MP:
if bq_type is BqType.WEBTV:
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
else:
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
file_path = "{}/".format(Path(file_path).parent)
ImportDialog(transient, file_path, profile, services.keys(), lambda b, s: appender(b), (bqs,)).show()
def get_enigma2_bouquet(path):
path, sep, f_name = path.rpartition("userbouquet.")
name, sep, suf = f_name.rpartition(".")
bq = get_bouquet(path, name, suf)
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
return bouquet
class ImportDialog:
def __init__(self, transient, path, profile, service_ids, appender, bouquets=None):
handlers = {"on_import": self.on_import,
"on_cursor_changed": self.on_cursor_changed,
"on_info_button_toggled": self.on_info_button_toggled,
"on_selected_toggled": self.on_selected_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_popup_menu": on_popup_menu,
"on_key_press": self.on_key_press}
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "import_dialog.glade")
builder.connect_signals(handlers)
self._bq_services = {}
self._services = {}
self._service_ids = service_ids
self._append = appender
self._profile = profile
self._bouquets = bouquets
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
self._main_model = builder.get_object("main_list_store")
self._main_view = builder.get_object("main_view")
self._services_view = builder.get_object("services_view")
self._services_model = builder.get_object("services_list_store")
self._services_box = builder.get_object("services_box")
self._info_check_button = builder.get_object("info_check_button")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
self.init_data(path)
def show(self):
self._dialog_window.show()
@run_idle
def init_data(self, path):
self._main_model.clear()
self._services_model.clear()
try:
if not self._bouquets:
self._bouquets = get_bouquets(path, self._profile)
for bqs in self._bouquets:
for bq in bqs.bouquets:
self._main_model.append((bq.name, bq.type, True))
self._bq_services[(bq.name, bq.type)] = bq.services
# Note! Getting default format ver. 4
services = get_services(path, self._profile, 4 if self._profile is Profile.ENIGMA_2 else 0)
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_import(self, item):
if not any(r[-1] for r in self._main_model):
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
return
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return
services = set()
to_delete = set()
for row in self._main_model:
bq = (row[0], row[1])
if row[-1]:
for bq_srv in self._bq_services.get(bq, []):
srv = self._services.get(bq_srv.data, None)
if srv:
services.add(srv)
else:
to_delete.add(bq)
bqs_to_delete = []
for bqs in self._bouquets:
for bq in bqs.bouquets:
if (bq.name, bq.type) in to_delete:
bqs_to_delete.append(bq)
for bqs in self._bouquets:
bq = bqs.bouquets
for b in bqs_to_delete:
with suppress(ValueError):
bq.remove(b)
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services)))
self._dialog_window.destroy()
@run_idle
def on_cursor_changed(self, view):
if not self._info_check_button.get_active():
return
self._services_model.clear()
model, paths = view.get_selection().get_selected_rows()
if not paths:
return
bq_services = self._bq_services.get(model.get(model.get_iter(paths[0]), 0, 1))
for bq_srv in bq_services:
if bq_srv.type is BqServiceType.DEFAULT:
srv = self._services.get(bq_srv.data, None)
if srv:
self._services_model.append((srv.service, srv.service_type))
else:
self._services_model.append((bq_srv.name, bq_srv.type.value))
def on_info_button_toggled(self, button):
active = button.get_active()
self._services_box.set_visible(active)
def on_selected_toggled(self, toggle, path):
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE:
path, column = view.get_cursor()
itr = self._main_model.get_iter(path)
selected = self._main_model.get_value(itr, 2)
self._main_model.set_value(itr, 2, not selected)
if __name__ == "__main__":
pass

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018 Dmitriy Yefremov
Copyright (c) 2018-2019 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
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkDialog" id="search_unavailable_streams_dialog">
<property name="use-header-bar">1</property>
@@ -45,18 +45,9 @@ Author: Dmitriy Yefremov
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="decorated">False</property>
<property name="gravity">center</property>
<signal name="response" handler="on_response" swapped="no"/>
<child type="action">
<object class="GtkButton" id="search_unavailable_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
@@ -65,6 +56,16 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">1</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="search_unavailable_box_frame">
<property name="visible">True</property>
@@ -74,42 +75,17 @@ Author: Dmitriy Yefremov
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0</property>
<property name="label_yalign">1</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="search_unavailable_main_box">
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</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="label" translatable="yes">Please wait, streams testing in progress...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLevelBar" id="unavailable_streams_level_bar">
<property name="height_request">10</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="inverted">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<property name="column_spacing">10</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
@@ -157,11 +133,59 @@ Author: Dmitriy Yefremov
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLevelBar" id="unavailable_streams_level_bar">
<property name="height_request">10</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="inverted">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="search_unavailable_cancel_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Cancel</property>
<child>
<object class="GtkImage" id="cancel_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-cancel</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Please wait, streams testing in progress...</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label_item">
@@ -192,10 +216,16 @@ Author: Dmitriy Yefremov
<row>
<col id="0">non-TS</col>
</row>
<row>
<col id="0">none-REC1</col>
</row>
<row>
<col id="0">none-REC2</col>
</row>
</data>
</object>
<object class="GtkDialog" id="iptv_dialog">
<property name="use-header-bar">1</property>
<property name="use-header-bar">{use_header}</property>
<property name="width_request">480</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Stream data</property>
@@ -606,10 +636,10 @@ Author: Dmitriy Yefremov
</action-widgets>
</object>
<object class="GtkDialog" id="iptv_list_configuration_dialog">
<property name="use-header-bar">1</property>
<property name="use-header-bar">{use_header}</property>
<property name="width_request">400</property>
<property name="can_focus">False</property>
<property name="title"> IPTV streams list configuration</property>
<property name="title" translatable="yes">IPTV streams list configuration</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center</property>

View File

@@ -1,7 +1,6 @@
import re
import urllib
from urllib.error import HTTPError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
@@ -9,13 +8,14 @@ from app.commons import run_idle, run_task
from app.eparser.ecommons import BqServiceType, Service
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT
from app.properties import Profile
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON
from .dialogs import Action, show_dialog, DialogType
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string
from .main_helper import get_base_model, get_iptv_url
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\s]*$|\D)")
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
def is_data_correct(elems):
@@ -25,6 +25,17 @@ def is_data_correct(elems):
return True
def get_stream_type(box):
active = box.get_active()
if active == 0:
return StreamType.DVB_TS.value
elif active == 1:
return StreamType.NONE_TS.value
elif active == 2:
return StreamType.NONE_REC_1.value
return StreamType.NONE_REC_2.value
class IptvDialog:
def __init__(self, transient, view, services, bouquet, profile=Profile.ENIGMA_2, action=Action.ADD):
@@ -36,7 +47,8 @@ class IptvDialog:
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade", ("iptv_dialog", "stream_type_liststore"))
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_dialog", "stream_type_liststore"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("iptv_dialog")
@@ -116,7 +128,21 @@ class IptvDialog:
data = data.split(":")
if len(data) < 11:
return
self._stream_type_combobox.set_active(0 if StreamType(data[0].strip()) is StreamType.DVB_TS else 1)
s_type = data[0].strip()
try:
stream_type = StreamType(s_type)
if stream_type is StreamType.DVB_TS:
self._stream_type_combobox.set_active(0)
elif stream_type is StreamType.NONE_TS:
self._stream_type_combobox.set_active(1)
elif stream_type is StreamType.NONE_REC_1:
self._stream_type_combobox.set_active(2)
elif stream_type is StreamType.NONE_REC_2:
self._stream_type_combobox.set_active(3)
except ValueError:
show_dialog(DialogType.ERROR, "Unknown stream type {}".format(s_type))
self._srv_type_entry.set_text(data[2])
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
@@ -140,7 +166,7 @@ class IptvDialog:
int(self._namespace_entry.get_text())))
def get_type(self):
return 1 if self._stream_type_combobox.get_active() == 0 else 4097
return get_stream_type(self._stream_type_combobox)
def on_entry_changed(self, entry):
if _PATTERN.search(entry.get_text()):
@@ -183,11 +209,11 @@ class IptvDialog:
old_srv = self._services.pop(self._current_srv[7])
self._services[fav_id] = old_srv._replace(service=name, fav_id=fav_id)
self._bouquet[self._paths[0][0]] = fav_id
self._model.set(self._model.get_iter(self._paths), {2: name, 7: fav_id})
self._model.set(self._model.get_iter(self._paths), {Column.FAV_SERVICE: name, Column.FAV_ID: fav_id})
else:
aggr = [None] * 10
s_type = BqServiceType.IPTV.name
srv = (None, None, name, None, None, s_type, None, fav_id, None)
srv = (None, None, name, None, None, s_type, None, fav_id, *aggr[0:3])
itr = self._model.insert_after(self._model.get_iter(self._paths[0]),
srv) if self._paths else self._model.insert(0, srv)
self._model.set_value(itr, 1, IPTV_ICON)
@@ -281,7 +307,7 @@ class SearchUnavailableDialog:
class IptvListConfigurationDialog:
def __init__(self, transient, services, iptv_rows, bouquet, profile):
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, profile):
handlers = {"on_apply": self.on_apply,
"on_response": self.on_response,
"on_stream_type_default_togged": self.on_stream_type_default_togged,
@@ -297,13 +323,14 @@ class IptvListConfigurationDialog:
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade",
("iptv_list_configuration_dialog", "stream_type_liststore"))
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_list_configuration_dialog", "stream_type_liststore"))
builder.connect_signals(handlers)
self._rows = iptv_rows
self._services = services
self._bouquet = bouquet
self._fav_model = fav_model
self._profile = profile
self._dialog = builder.get_object("iptv_list_configuration_dialog")
@@ -392,9 +419,6 @@ class IptvListConfigurationDialog:
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if len(self._bouquet) != len(self._rows):
return
if self._profile is Profile.ENIGMA_2:
reset = self._reset_to_default_switch.get_active()
type_default = self._type_check_button.get_active()
@@ -403,35 +427,38 @@ class IptvListConfigurationDialog:
nid_default = self._nid_check_button.get_active()
namespace_default = self._namespace_check_button.get_active()
stream_type = StreamType.NONE_TS.value if reset else get_stream_type(self._stream_type_combobox)
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
tid = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
nid = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
namespace = "0" if namespace_default else "{:X}".format(int(self._list_namespace_entry.get_text()))
for index, row in enumerate(self._rows):
fav_id = row[7]
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if reset:
data[0] = " 4097"
data[2], data[3], data[4], data[5], data[6] = "10000"
else:
data[0] = " 4097" if self._stream_type_combobox.get_active() == 1 else "1"
data[2] = "1" if type_default else self._list_srv_type_entry.get_text()
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
data[3] = "{:X}".format(index) if sid_auto else "0"
data[4] = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
data[5] = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
data[6] = "0" if namespace_default else "{:X}".format(int(self._list_namespace_entry.get_text()))
data = ":".join(data)
new_fav_id = "{}{}{}".format(data, sep, desc)
row[7] = new_fav_id
self._bouquet[index] = new_fav_id
row[Column.FAV_ID] = new_fav_id
srv = self._services.pop(fav_id, None)
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id)
self._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
self._info_bar.set_visible(True)
@run_idle
def update_reference(self):
if is_data_correct(self._digit_elems):
stream_type = "4097" if self._stream_type_combobox.get_active() == 1 else "1"
stream_type = get_stream_type(self._stream_type_combobox)
self._reference_label.set_text(
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))

View File

@@ -3,28 +3,34 @@ import sys
from contextlib import suppress
from functools import lru_cache
from itertools import chain
from gi.repository import GLib
from gi.repository import GLib, Gio
from app.commons import run_idle, log, run_task, run_with_delay
from app.connections import http_request, HttpRequestType
from app.commons import run_idle, log, run_task, run_with_delay, init_logger
from app.connections import http_request, HttpRequestType, download_data, DownloadType, upload_data, test_http, \
TestException
from app.eparser import get_blacklist, write_blacklist, parse_m3u
from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service
from app.eparser.ecommons import CAS, Flag
from app.eparser.ecommons import CAS, Flag, BouquetService
from app.eparser.enigma.bouquets import BqServiceType
from app.eparser.iptv import export_to_m3u
from app.eparser.neutrino.bouquets import BqType
from app.properties import get_config, write_config, Profile
from app.tools.media import Player
from app.ui.backup import BackupDialog, backup_data, clear_data_path
from app.ui.epg_dialog import EpgDialog
from .backup import BackupDialog, backup_data, clear_data_path
from .imports import ImportDialog, import_bouquet
from .download_dialog import DownloadDialog
from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \
EXTRA_COLOR, NEW_COLOR
EXTRA_COLOR, NEW_COLOR, FavClickMode
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, get_message
from .main_helper import insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services, \
scroll_to, get_base_model, update_picons_data, copy_picon_reference, assign_picon, remove_picon, \
is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons, get_selection, get_model_data
is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons, get_selection, get_model_data, \
remove_all_unused_picons
from .picons_downloader import PiconsDialog
from .satellites_dialog import show_satellites_dialog
from .settings_dialog import show_settings_dialog
@@ -37,28 +43,33 @@ class Application(Gtk.Application):
_SERVICE_LIST_NAME = "services_list_store"
_FAV_LIST_NAME = "fav_list_store"
_BOUQUETS_LIST_NAME = "bouquets_tree_store"
# Dynamically active elements depending on the selected view
_SERVICE_ELEMENTS = ("services_popup_menu",)
_SERVICE_ELEMENTS = ("services_to_fav_end_move_popup_item", "services_to_fav_move_popup_item",
"services_create_bouquet_popup_item", "services_copy_popup_item", "services_edit_popup_item",
"services_add_new_popup_item", "services_picon_popup_item", "services_remove_popup_item")
_FAV_ELEMENTS = ("fav_cut_popup_item", "fav_paste_popup_item", "fav_locate_popup_item", "fav_iptv_popup_item",
"fav_insert_marker_popup_item", "fav_edit_sub_menu_popup_item", "fav_edit_popup_item",
"fav_picon_popup_item", "fav_copy_popup_item")
"fav_picon_popup_item", "fav_copy_popup_item", "fav_epg_configuration_popup_item")
_BOUQUET_ELEMENTS = ("bouquets_new_popup_item", "bouquets_edit_popup_item", "bouquets_cut_popup_item",
"bouquets_copy_popup_item", "bouquets_paste_popup_item", "edit_header_button",
"new_header_button")
_COMMONS_ELEMENTS = ("edit_header_button", "bouquets_remove_popup_item", "fav_remove_popup_item")
_FAV_ENIGMA_ELEMENTS = ("fav_insert_marker_popup_item",)
_FAV_IPTV_ELEMENTS = ("fav_iptv_popup_item",)
"bouquets_copy_popup_item", "bouquets_paste_popup_item", "new_header_button",
"bouquet_import_popup_item")
_COMMONS_ELEMENTS = ("bouquets_remove_popup_item", "fav_remove_popup_item", "import_bq_menu_button")
_FAV_ENIGMA_ELEMENTS = ("fav_insert_marker_popup_item", "fav_epg_configuration_popup_item",
"epg_configuration_header_button")
_FAV_IPTV_ELEMENTS = ("fav_iptv_popup_item", "import_m3u_header_button", "export_to_m3u_header_button",
"epg_configuration_header_button")
_LOCK_HIDE_ELEMENTS = ("locked_tool_button", "hide_tool_button")
_DYNAMIC_ELEMENTS = ("services_popup_menu", "new_header_button", "edit_header_button", "locked_tool_button",
"fav_cut_popup_item", "fav_paste_popup_item", "bouquets_new_popup_item", "hide_tool_button",
"bouquets_remove_popup_item", "fav_remove_popup_item", "bouquets_edit_popup_item",
"fav_insert_marker_popup_item", "fav_edit_popup_item", "fav_edit_sub_menu_popup_item",
"fav_locate_popup_item", "fav_picon_popup_item", "fav_iptv_popup_item", "fav_copy_popup_item",
"bouquets_cut_popup_item", "bouquets_copy_popup_item", "bouquets_paste_popup_item")
def __init__(self, **kwargs):
super().__init__(**kwargs)
super().__init__(flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, **kwargs)
# Adding command line options
self.add_main_option("log", ord("l"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "", None)
handlers = {"on_close_app": self.on_close_app,
"on_resize": self.on_resize,
@@ -95,12 +106,14 @@ class Application(Gtk.Application):
"on_bq_view_drag_data_received": self.on_bq_view_drag_data_received,
"on_view_press": self.on_view_press,
"on_view_popup_menu": self.on_view_popup_menu,
"on_popover_release": self.on_popover_release,
"on_view_focus": self.on_view_focus,
"on_hide": self.on_hide,
"on_locked": self.on_locked,
"on_model_changed": self.on_model_changed,
"on_import_m3u": self.on_import_m3u,
"on_export_to_m3u": self.on_export_to_m3u,
"on_import_bouquet": self.on_import_bouquet,
"on_import_bouquets": self.on_import_bouquets,
"on_backup_tool_show": self.on_backup_tool_show,
"on_insert_marker": self.on_insert_marker,
"on_fav_press": self.on_fav_press,
@@ -110,12 +123,14 @@ class Application(Gtk.Application):
"on_assign_picon": self.on_assign_picon,
"on_remove_picon": self.on_remove_picon,
"on_reference_picon": self.on_reference_picon,
"on_remove_unused_picons": self.on_remove_unused_picons,
"on_filter_toggled": self.on_filter_toggled,
"on_search_toggled": self.on_search_toggled,
"on_search_down": self.on_search_down,
"on_search_up": self.on_search_up,
"on_search": self.on_search,
"on_iptv": self.on_iptv,
"on_epg_list_configuration": self.on_epg_list_configuration,
"on_iptv_list_configuration": self.on_iptv_list_configuration,
"on_play_stream": self.on_play_stream,
"on_player_play": self.on_player_play,
@@ -161,6 +176,7 @@ class Application(Gtk.Application):
self._drawing_area_xid = None
# http api
self._http_api = None
self._fav_click_mode = None
self._monitor_signal = False
# Colors
self._use_colors = False
@@ -204,8 +220,6 @@ class Application(Gtk.Application):
self._signal_box = builder.get_object("signal_box")
self._service_name_label = builder.get_object("service_name_label")
self._signal_level_bar = builder.get_object("signal_level_bar")
# Dynamically active elements depending on the selected view
self._tool_elements = {k: builder.get_object(k) for k in self._DYNAMIC_ELEMENTS}
self._cas_label = builder.get_object("cas_label")
self._fav_count_label = builder.get_object("fav_count_label")
self._bouquets_count_label = builder.get_object("bouquets_count_label")
@@ -234,6 +248,10 @@ class Application(Gtk.Application):
self._search_provider = SearchProvider((self._services_view, self._fav_view, self._bouquets_view),
builder.get_object("search_down_button"),
builder.get_object("search_up_button"))
# Dynamically active elements depending on the selected view
d_elements = (self._SERVICE_ELEMENTS, self._BOUQUET_ELEMENTS, self._COMMONS_ELEMENTS, self._FAV_ELEMENTS,
self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS, self._LOCK_HIDE_ELEMENTS)
self._tool_elements = {k: builder.get_object(k) for k in set(chain.from_iterable(d_elements))}
def do_startup(self):
Gtk.Application.do_startup(self)
@@ -241,6 +259,7 @@ class Application(Gtk.Application):
self.init_drag_and_drop()
self.init_colors()
self.init_http_api()
self._services_view.grab_focus()
def do_activate(self):
self._main_window.set_application(self)
@@ -254,6 +273,16 @@ class Application(Gtk.Application):
self._player.release()
Gtk.Application.do_shutdown(self)
def do_command_line(self, command_line):
""" Processing command line parameters. """
options = command_line.get_options_dict()
options = options.end().unpack()
if "log" in options:
init_logger()
self.activate()
return 0
def init_drag_and_drop(self):
""" Enable drag-and-drop """
target = []
@@ -338,6 +367,7 @@ class Application(Gtk.Application):
move_items(key, self._fav_view if self._fav_view.is_focus() else self._bouquets_view)
# ***************** Copy - Cut - Paste *********************#
def on_services_copy(self, view):
self.on_copy(view, target=ViewTarget.FAV)
@@ -391,7 +421,7 @@ class Application(Gtk.Application):
self.fav_paste(selection)
elif target is ViewTarget.BOUQUET:
self.bouquet_paste(selection)
self.on_view_focus(view, None)
self.on_view_focus(view)
def fav_paste(self, selection):
dest_index = 0
@@ -408,7 +438,7 @@ class Application(Gtk.Application):
for row in self._rows_buffer:
dest_index += 1
model.insert(dest_index, row)
fav_bouquet.insert(dest_index, row[7])
fav_bouquet.insert(dest_index, row[Column.FAV_ID])
if model.get_name() == self._FAV_LIST_NAME:
self.update_fav_num_column(model)
@@ -418,7 +448,7 @@ class Application(Gtk.Application):
def bouquet_paste(self, selection):
model, paths = selection.get_selected_rows()
if len(paths) > 1:
show_dialog(DialogType.ERROR, self._main_window, "Please, select only one item!")
self.show_error_dialog("Please, select only one item!")
return
path = paths[0]
@@ -455,7 +485,7 @@ class Application(Gtk.Application):
elif model_name == self._SERVICE_LIST_NAME:
next(self.delete_services(itrs, model, rows), False)
self.on_view_focus(view, None)
self.on_view_focus(view)
return rows
@@ -499,7 +529,7 @@ class Application(Gtk.Application):
def delete_bouquets(self, itrs, model):
""" Deleting bouquets """
if len(itrs) == 1 and len(model.get_path(itrs[0])) < 2:
show_dialog(DialogType.ERROR, self._main_window, "This item is not allowed to be removed!")
self.show_error_dialog("This item is not allowed to be removed!")
return
for itr in itrs:
@@ -705,7 +735,7 @@ class Application(Gtk.Application):
model.remove(in_itr)
self.update_fav_num_column(model)
except ValueError as e:
show_dialog(DialogType.ERROR, self._main_window, str(e))
self.show_error_dialog(str(e))
def on_view_press(self, view, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
@@ -726,26 +756,64 @@ class Application(Gtk.Application):
name = Gtk.Buildable.get_name(menu)
if name == "services_popup_menu":
self.delete_selection(self._fav_view, self._bouquets_view)
self.on_view_focus(self._services_view, None)
self.on_view_focus(self._services_view)
elif name == "fav_popup_menu":
self.delete_selection(self._services_view, self._bouquets_view)
self.on_view_focus(self._fav_view, None)
self.on_view_focus(self._fav_view)
elif name == "bouquets_popup_menu":
self.delete_selection(self._services_view, self._fav_view)
self.on_view_focus(self._bouquets_view, None)
self.on_view_focus(self._bouquets_view)
menu.popup(None, None, None, None, event.button, event.time)
return True
def on_popover_release(self, menu, event):
""" Hides popover after mouse click. Used if element of Popover menu is Gtk.Button! """
menu.hide()
@run_idle
def on_satellite_editor_show(self, model):
""" Shows satellites editor dialog """
show_satellites_dialog(self._main_window, self._options.get(self._profile))
def on_download(self, item):
DownloadDialog(transient=self._main_window,
properties=self._options,
open_data_callback=self.open_data,
update_settings_callback=self.update_options,
profile=Profile(self._profile)).show()
@run_task
def on_download_data(self):
try:
download_data(properties=self._options.get(self._profile),
download_type=DownloadType.ALL,
callback=lambda x: print(x, end=""))
except Exception as e:
self.show_error_dialog(str(e))
else:
GLib.idle_add(self.open_data)
@run_task
def on_upload_data(self, download_type):
try:
profile = Profile(self._profile)
opts = self._options.get(self._profile)
use_http = profile is Profile.ENIGMA_2
if profile is Profile.ENIGMA_2:
host, port = opts.get("host", "127.0.0.1"), opts.get("http_port")
user, password = opts.get("http_user", "root"), opts.get("http_password", "")
try:
test_http(host, port, user, password, skip_message=True)
except TestException:
use_http = False
upload_data(properties=opts,
download_type=download_type,
remove_unused=True,
profile=profile,
callback=lambda x: print(x, end=""),
use_http=use_http)
except Exception as e:
self.show_error_dialog(str(e))
@run_idle
def on_data_open(self, model):
response = show_dialog(DialogType.CHOOSER, self._main_window, options=self._options.get(self._profile))
@@ -770,31 +838,40 @@ class Application(Gtk.Application):
update_picons_data(self._options.get(self._profile).get("picons_dir_path"), self._picons)
except FileNotFoundError as e:
self._wait_dialog.hide()
show_dialog(DialogType.ERROR, self._main_window, getattr(e, "message", str(e)) + "\n\n" +
get_message("Please, download files from receiver or setup your path for read data!"))
msg = get_message("Please, download files from receiver or setup your path for read data!")
self.show_error_dialog(getattr(e, "message", str(e)) + "\n\n" + msg)
except SyntaxError as e:
self._wait_dialog.hide()
show_dialog(DialogType.ERROR, self._main_window, str(e))
self.show_error_dialog(str(e))
except Exception as e:
self._wait_dialog.hide()
log("Append services error: " + str(e))
show_dialog(DialogType.ERROR, self._main_window, "Reading data error!\n" + str(e))
self.show_error_dialog(get_message("Reading data error!") + "\n" + str(e))
else:
self.append_blacklist(black_list)
self.append_bouquets(bouquets)
self.append_services(services)
self.update_sat_positions()
self.update_services_counts(len(self._services.values()))
def append_blacklist(self, black_list):
if black_list:
self._blacklist.update(black_list)
def append_bouquets(self, bqs):
for bouquet in bqs:
parent = self._bouquets_model.append(None, [bouquet.name, None, None, bouquet.type])
for bq in bouquet.bouquets:
self.append_bouquet(bq, parent)
if len(self._bouquets_model):
self.add_to_bouquets(bqs)
else:
for bouquet in bqs:
parent = self._bouquets_model.append(None, [bouquet.name, None, None, bouquet.type])
for bq in bouquet.bouquets:
self.append_bouquet(bq, parent)
def add_to_bouquets(self, bqs):
for bouquets in bqs:
for row in self._bouquets_model:
if row[Column.BQ_TYPE] == bouquets.type:
for bq in bouquets.bouquets:
self.append_bouquet(bq, row.iter)
def append_bouquet(self, bq, parent):
name, bt_type, locked, hidden = bq.name, bq.type, bq.locked, bq.hidden
@@ -829,6 +906,7 @@ class Application(Gtk.Application):
for srv in services:
# adding channels to dict with fav_id as keys
self._services[srv.fav_id] = srv
self.update_services_counts(len(self._services.values()))
gen = self.append_services_data(services)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@@ -844,7 +922,7 @@ class Application(Gtk.Application):
s = srv + (tooltip, background)
itr = self._services_model.append(s)
self._services_model.set_value(itr, 8, self._picons.get(srv.picon_id, None))
self._services_model.set_value(itr, Column.SRV_PICON, self._picons.get(srv.picon_id, None))
yield True
self._wait_dialog.hide()
@@ -865,7 +943,7 @@ class Application(Gtk.Application):
@run_idle
def on_data_save(self, *args):
if len(self._bouquets_model) == 0:
show_dialog(DialogType.ERROR, self._main_window, get_message("No data to save!"))
self.show_error_dialog("No data to save!")
return
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
@@ -887,7 +965,8 @@ class Application(Gtk.Application):
num_of_children = model.iter_n_children(itr)
for num in range(num_of_children):
bq_itr = model.iter_nth_child(itr, num)
bq_name, locked, hidden, bq_type = model.get(bq_itr, 0, 1, 2, 3)
bq_name, locked, hidden, bq_type = model.get(bq_itr, Column.BQ_NAME, Column.BQ_LOCKED,
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)
@@ -897,7 +976,7 @@ class Application(Gtk.Application):
bq = Bouquet(bq_name, bq_type, bq_s, locked, hidden)
bqs.append(bq)
if len(b_path) == 1:
bouquets.append(Bouquets(*model.get(itr, 0, 3), bqs if bqs else []))
bouquets.append(Bouquets(*model.get(itr, Column.BQ_NAME, Column.BQ_TYPE), bqs if bqs else []))
profile = Profile(self._profile)
# Getting bouquets
@@ -935,7 +1014,7 @@ class Application(Gtk.Application):
def update_service_bar(self, model, path):
def_val = "Unknown"
cas = model.get_value(model.get_iter(path), 0)
cas = model.get_value(model.get_iter(path), Column.SRV_CAS_FLAGS)
if not cas:
return
cas_values = list(filter(lambda val: val.startswith("C:"), cas.split(",")))
@@ -948,7 +1027,7 @@ class Application(Gtk.Application):
if self._current_bq_name:
ch_row = model[model.get_iter(path)][:]
self._bq_selected = "{}:{}".format(ch_row[0], ch_row[3])
self._bq_selected = "{}:{}".format(ch_row[Column.BQ_NAME], ch_row[Column.BQ_TYPE])
else:
self._bq_selected = ""
@@ -960,13 +1039,15 @@ class Application(Gtk.Application):
if len(path) > 1:
next(self.update_bouquet_services(model, path), False)
self.on_view_focus(self._bouquets_view)
def update_bouquet_services(self, model, path, bq_key=None):
""" Updates list of bouquet services """
tree_iter = None
if path:
tree_iter = model.get_iter(path)
key = bq_key if bq_key else "{}:{}".format(*model.get(tree_iter, 0, 3))
key = bq_key if bq_key else "{}:{}".format(*model.get(tree_iter, Column.BQ_NAME, Column.BQ_TYPE))
services = self._bouquets.get(key, None)
ex_services = self._extra_bouquets.get(key, None)
if not services:
@@ -987,11 +1068,11 @@ class Application(Gtk.Application):
def check_bouquet_selection(self):
""" checks and returns bouquet if selected """
if not self._bq_selected:
show_dialog(DialogType.ERROR, self._main_window, "Error. No bouquet is selected!")
self.show_error_dialog("Error. No bouquet is selected!")
return
if Profile(self._profile) is Profile.NEUTRINO_MP and self._bq_selected.endswith(BqType.WEBTV.value):
show_dialog(DialogType.ERROR, self._main_window, "Operation not allowed in this context!")
self.show_error_dialog("Operation not allowed in this context!")
return
return self._bq_selected
@@ -1004,11 +1085,11 @@ class Application(Gtk.Application):
if bqs_rows:
bq_type = row[-1]
for b_row in bqs_rows:
bq_id = "{}:{}".format(b_row[0], b_row[-1])
bq_id = "{}:{}".format(b_row[Column.BQ_NAME], b_row[Column.BQ_TYPE])
bq = self._bouquets.get(bq_id, None)
if bq:
b_row[-1] = bq_type
self._bouquets["{}:{}".format(b_row[0], b_row[-1])] = bq
b_row[Column.BQ_TYPE] = bq_type
self._bouquets["{}:{}".format(b_row[Column.BQ_NAME], b_row[Column.BQ_TYPE])] = bq
def delete_selection(self, view, *args):
""" Used for clear selection on given view(s) """
@@ -1019,17 +1100,18 @@ class Application(Gtk.Application):
def on_preferences(self, item):
response = show_settings_dialog(self._main_window, self._options)
if response != Gtk.ResponseType.CANCEL:
profile = self._options.get("profile")
self._ip_label.set_text(self._options.get(profile).get("host"))
self.update_options()
if profile != self._profile:
self._profile = profile
self.clear_current_data()
self.update_services_counts()
self.update_profile_label()
self.init_colors(True)
self.init_http_api()
def update_options(self):
profile = self._options.get("profile")
self._ip_label.set_text(self._options.get(profile).get("host"))
if profile != self._profile:
self._profile = profile
self.clear_current_data()
self.update_services_counts()
self.update_profile_label()
self.init_colors(True)
self.init_http_api()
def on_tree_view_key_press(self, view, event):
""" Handling keystrokes on press """
@@ -1041,7 +1123,11 @@ class Application(Gtk.Application):
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
model_name, model = get_model_data(view)
if ctrl and key in MOVE_KEYS:
if ctrl and key is KeyboardKey.O:
self.open_data()
elif ctrl and key is KeyboardKey.Q:
self.quit()
elif ctrl and key in MOVE_KEYS:
self.move_items(key)
elif ctrl and key is KeyboardKey.C:
if model_name == self._SERVICE_LIST_NAME:
@@ -1073,7 +1159,13 @@ class Application(Gtk.Application):
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
model_name, model = get_model_data(view)
if ctrl and key is KeyboardKey.INSERT:
if ctrl and key is KeyboardKey.D:
self.on_download_data()
elif ctrl and key is KeyboardKey.U:
self.on_upload_data(DownloadType.ALL)
elif ctrl and key is KeyboardKey.B:
self.on_upload_data(DownloadType.BOUQUETS)
elif ctrl and key is KeyboardKey.INSERT:
# Move items from app to fav list
if model_name == self._SERVICE_LIST_NAME:
self.on_to_fav_copy(view)
@@ -1107,16 +1199,11 @@ class Application(Gtk.Application):
self.update_fav_num_column(model)
self.update_bouquet_list()
def on_download(self, item):
DownloadDialog(transient=self._main_window,
properties=self._options,
open_data_callback=self.open_data,
profile=Profile(self._profile)).show()
def on_view_focus(self, view, focus_event):
def on_view_focus(self, view, focus_event=None):
profile = Profile(self._profile)
model_name, model = get_model_data(view)
not_empty = len(model) > 0 # if > 0 model has items
is_service = model_name == self._SERVICE_LIST_NAME
if model_name == self._BOUQUETS_LIST_NAME:
for elem in self._tool_elements:
@@ -1129,15 +1216,10 @@ class Application(Gtk.Application):
for elem in self._LOCK_HIDE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty)
else:
is_service = model_name == self._SERVICE_LIST_NAME
for elem in self._FAV_ELEMENTS:
if elem in ("paste_tool_button", "fav_paste_popup_item"):
self._tool_elements[elem].set_sensitive(not is_service and self._rows_buffer)
elif elem in self._FAV_ENIGMA_ELEMENTS:
if profile is Profile.ENIGMA_2:
self._tool_elements[elem].set_sensitive(self._bq_selected and not is_service)
elif elem in self._FAV_IPTV_ELEMENTS:
self._tool_elements[elem].set_sensitive(self._bq_selected and not is_service)
else:
self._tool_elements[elem].set_sensitive(not_empty and not is_service)
@@ -1148,9 +1230,15 @@ class Application(Gtk.Application):
for elem in self._LOCK_HIDE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty and profile is Profile.ENIGMA_2)
for elem in self._FAV_IPTV_ELEMENTS:
self._tool_elements[elem].set_sensitive(self._bq_selected and not is_service)
for elem in self._COMMONS_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty)
if profile is not Profile.ENIGMA_2:
for elem in self._FAV_ENIGMA_ELEMENTS:
self._tool_elements[elem].set_sensitive(False)
def on_hide(self, item):
self.set_service_flags(Flag.HIDE)
@@ -1207,8 +1295,14 @@ class Application(Gtk.Application):
def on_fav_press(self, menu, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_play_stream()
self.on_zap()
if self._fav_click_mode is FavClickMode.DISABLED:
return
elif self._fav_click_mode is FavClickMode.STREAM:
self.on_play_stream()
elif self._fav_click_mode is FavClickMode.PLAY:
self.on_zap(self.on_watch)
elif self._fav_click_mode is FavClickMode.ZAP:
self.on_zap()
else:
return self.on_view_popup_menu(menu, event)
@@ -1228,25 +1322,25 @@ class Application(Gtk.Application):
def on_iptv_list_configuration(self, item):
profile = Profile(self._profile)
if profile is Profile.NEUTRINO_MP:
show_dialog(DialogType.ERROR, transient=self._main_window, text="Neutrino at the moment not supported!")
self.show_error_dialog("Neutrino at the moment not supported!")
return
iptv_rows = list(filter(lambda r: r[Column.FAV_TYPE] == BqServiceType.IPTV.value, self._fav_model))
if not iptv_rows:
show_dialog(DialogType.ERROR, self._main_window, "This list does not contains IPTV streams!")
self.show_error_dialog("This list does not contains IPTV streams!")
return
if not self._bq_selected:
return
bouquet = self._bouquets.get(self._bq_selected, [])
IptvListConfigurationDialog(self._main_window, self._services, iptv_rows, bouquet, profile).show()
bq = self._bouquets.get(self._bq_selected, [])
IptvListConfigurationDialog(self._main_window, self._services, iptv_rows, bq, self._fav_model, profile).show()
@run_idle
def on_remove_all_unavailable(self, item):
iptv_rows = list(filter(lambda r: r[5] == BqServiceType.IPTV.value, self._fav_model))
iptv_rows = list(filter(lambda r: r[Column.FAV_TYPE] == BqServiceType.IPTV.value, self._fav_model))
if not iptv_rows:
show_dialog(DialogType.ERROR, self._main_window, "This list does not contains IPTV streams!")
self.show_error_dialog("This list does not contains IPTV streams!")
return
if not self._bq_selected:
@@ -1261,6 +1355,24 @@ class Application(Gtk.Application):
if response:
next(self.remove_favs(response, self._fav_model), False)
# ****************** EPG **********************#
@run_idle
def on_epg_list_configuration(self, item):
if Profile(self._profile) is not Profile.ENIGMA_2:
self.show_error_dialog("Only Enigma2 is supported!")
return
if not any(r[Column.FAV_TYPE] == BqServiceType.IPTV.value for r in self._fav_model):
self.show_error_dialog("This list does not contains IPTV streams!")
return
bq = self._bouquets.get(self._bq_selected)
profile = self._options.get(self._profile)
EpgDialog(self._main_window, profile, self._services, bq, self._fav_model, self._current_bq_name).show()
# ***************** Import ********************#
def on_import_m3u(self, item):
""" Imports iptv from m3u files. """
response = get_chooser_dialog(self._main_window, self._options.get(self._profile), "*.m3u", "m3u files")
@@ -1268,7 +1380,7 @@ class Application(Gtk.Application):
return
if not str(response).endswith("m3u"):
show_dialog(DialogType.ERROR, self._main_window, text="No m3u file is selected!")
self.show_error_dialog("No m3u file is selected!")
return
channels = parse_m3u(response, Profile(self._profile))
@@ -1281,6 +1393,50 @@ class Application(Gtk.Application):
bq_services.append(ch.fav_id)
next(self.update_bouquet_services(self._fav_model, None, self._bq_selected), False)
@run_idle
def on_export_to_m3u(self, item):
i_types = (BqServiceType.IPTV.value, BqServiceType.MARKER.value)
bq_services = [BouquetService(r[Column.FAV_SERVICE],
BqServiceType(r[Column.FAV_TYPE]),
r[Column.FAV_ID],
r[Column.FAV_NUM]) for r in self._fav_model if r[Column.FAV_TYPE] in i_types]
if not any(s.type is BqServiceType.IPTV for s in bq_services):
self.show_error_dialog("This list does not contains IPTV streams!")
return
response = show_dialog(DialogType.CHOOSER, self._main_window, options=self._options.get(self._profile))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
try:
bq = Bouquet(self._current_bq_name, None, bq_services, None, None)
export_to_m3u(response, bq, Profile(self._profile))
except Exception as e:
self.show_error_dialog(str(e))
else:
show_dialog(DialogType.INFO, self._main_window, "Done!")
def on_import_bouquet(self, item):
profile = Profile(self._profile)
model, paths = self._bouquets_view.get_selection().get_selected_rows()
if not paths:
self.show_error_dialog("No selected item!")
return
opts = self._options.get(self._profile)
appender = self.append_bouquet if profile is Profile.ENIGMA_2 else self.append_bouquets
import_bouquet(self._main_window, profile, model, paths[0], opts, self._services, appender)
def on_import_bouquets(self, item):
response = show_dialog(DialogType.CHOOSER, self._main_window, options=self._options.get(self._profile))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
ImportDialog(self._main_window, response, Profile(self._profile), self._services.keys(),
lambda b, s: (self._wait_dialog.show(), self.append_bouquets(b),
self.append_services(s), self.update_sat_positions())).show()
# ***************** Backup ********************#
def on_backup_tool_show(self, item):
@@ -1308,7 +1464,7 @@ class Application(Gtk.Application):
try:
self._player = Player()
except (NameError, AttributeError):
show_dialog(DialogType.ERROR, self._main_window, "No VLC is found. Check that it is installed!")
self.show_error_dialog("No VLC is found. Check that it is installed!")
return
else:
if self._drawing_area_xid:
@@ -1398,17 +1554,17 @@ class Application(Gtk.Application):
self._http_api = None
prp = self._options.get(self._profile)
self._fav_click_mode = FavClickMode(prp.get("fav_click_mode", FavClickMode.DISABLED))
if prp is Profile.NEUTRINO_MP or not prp.get("http_api_support", False):
self.update_info_boxes_visible(False)
return
self._http_api = http_request(prp.get("host", "127.0.0.1"), prp.get("http_port", "80"),
prp.get("http_user", ""), prp.get("http_password", ""))
next(self._http_api)
GLib.timeout_add_seconds(1, self.update_receiver_info)
@run_idle
def on_watch(self):
""" Switch to the channel and watch in the player """
m3u = self._http_api.send((HttpRequestType.STREAM, None))
@@ -1624,7 +1780,7 @@ class Application(Gtk.Application):
def on_bouquets_edit(self, view):
""" Rename bouquets """
if not self._bq_selected:
show_dialog(DialogType.ERROR, self._main_window, "This item is not allowed to edit!")
self.show_error_dialog("This item is not allowed to edit!")
return
model, paths = view.get_selection().get_selected_rows()
@@ -1638,6 +1794,9 @@ class Application(Gtk.Application):
model.set_value(itr, 0, response)
self._bouquets["{}:{}".format(response, bq_type)] = self._bouquets.pop("{}:{}".format(bq_name, bq_type))
self._current_bq_name = response
self._bq_name_label.set_text(self._current_bq_name)
self._bq_selected = "{}:{}".format(response, bq_type)
def on_rename(self, view):
name, model = get_model_data(view)
@@ -1659,7 +1818,7 @@ class Application(Gtk.Application):
cur_name, srv_type, fav_id = data[Column.FAV_SERVICE], data[Column.FAV_TYPE], data[Column.FAV_ID]
if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name:
show_dialog(DialogType.ERROR, self._main_window, "Not allowed in this context!")
self.show_error_dialog("Not allowed in this context!")
return
response = show_dialog(DialogType.INPUT, self._main_window, cur_name)
@@ -1693,11 +1852,11 @@ class Application(Gtk.Application):
ex_bq = self._extra_bouquets.get(self._bq_selected, None)
if not ex_bq:
show_dialog(DialogType.ERROR, self._main_window, "No changes required!")
self.show_error_dialog("No changes required!")
return
else:
if not ex_bq.pop(fav_id, None):
show_dialog(DialogType.ERROR, self._main_window, "No changes required!")
self.show_error_dialog("No changes required!")
return
if not ex_bq:
self._extra_bouquets.pop(self._bq_selected, None)
@@ -1746,6 +1905,12 @@ class Application(Gtk.Application):
""" Copying picon id to clipboard """
copy_picon_reference(self.get_target_view(view), view, self._services, self._clipboard, self._main_window)
def on_remove_unused_picons(self, item):
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
return
remove_all_unused_picons(self._options.get(self._profile), self._picons, self._services.values())
def get_target_view(self, view):
return ViewTarget.SERVICES if Gtk.Buildable.get_name(view) == "services_tree_view" else ViewTarget.FAV
@@ -1790,6 +1955,10 @@ class Application(Gtk.Application):
self._signal_box.set_visible(visible)
self._receiver_info_box.set_visible(visible)
@run_idle
def show_error_dialog(self, message):
show_dialog(DialogType.ERROR, self._main_window, message)
def start_app():
app = Application()

View File

@@ -435,15 +435,7 @@ def remove_picon(target, srv_view, fav_view, picons, options):
fav_view.get_model().foreach(remove) if target is ViewTarget.SERVICES else get_base_model(
srv_view.get_model()).foreach(remove)
pions_path = options.get("picons_dir_path")
backup_path = options.get("data_dir_path") + "backup/picons/"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
src = pions_path + p_id
if os.path.isfile(src):
shutil.move(src, backup_path + p_id)
remove_picons(options, picon_ids, picons)
def copy_picon_reference(target, view, services, clipboard, transient):
@@ -467,6 +459,23 @@ def copy_picon_reference(target, view, services, clipboard, transient):
show_dialog(DialogType.ERROR, transient, "No reference is present!")
def remove_all_unused_picons(options, picons, services):
ids = {s.picon_id for s in services}
pcs = list(filter(lambda x: x not in ids, picons))
remove_picons(options, pcs, picons)
def remove_picons(options, picon_ids, picons):
pions_path = options.get("picons_dir_path")
backup_path = options.get("backup_dir_path") + "picons/"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
src = pions_path + p_id
if os.path.isfile(src):
shutil.move(src, backup_path + p_id)
def is_only_one_item_selected(paths, transient):
if len(paths) > 1:
show_dialog(DialogType.ERROR, transient, "Please, select only one item!")

File diff suppressed because it is too large Load Diff

View File

@@ -425,6 +425,7 @@ Author: Dmitriy Yefremov
<object class="GtkHeaderBar" id="satellites_editor_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Satellites edit tool</property>
<property name="spacing">1</property>
<property name="show_close_button">True</property>
<child>
@@ -506,13 +507,6 @@ Author: Dmitriy Yefremov
</child>
</object>
</child>
<child type="title">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Satellites edit tool</property>
</object>
</child>
</object>
</child>
<child>
@@ -723,7 +717,8 @@ Author: Dmitriy Yefremov
</data>
</object>
<object class="GtkDialog" id="satellite_dialog">
<property name="use-header-bar">1</property>
<property name="use-header-bar">{use_header}</property>
<property name="title" translatable="yes">Satellite</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
@@ -732,41 +727,29 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="satellites_dialog_header">
<child type="action">
<object class="GtkButton" id="sat_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Satellite</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="sat_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="sat_ok_button">
<property name="label">gtk-ok</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="sat_ok_button">
<property name="label">gtk-ok</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox3">
<object class="GtkBox" id="satelitte_dialog_vbox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
@@ -792,7 +775,7 @@ Author: Dmitriy Yefremov
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="grid3">
<object class="GtkGrid" id="satellite_dialog_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
@@ -914,7 +897,8 @@ Author: Dmitriy Yefremov
</data>
</object>
<object class="GtkDialog" id="transponder_dialog">
<property name="use-header-bar">1</property>
<property name="use-header-bar">{use_header}</property>
<property name="title" translatable="yes">Transponder</property>
<property name="width_request">320</property>
<property name="can_focus">False</property>
<property name="resizable">False</property>
@@ -926,37 +910,25 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="transponder_dialog_header">
<child type="action">
<object class="GtkButton" id="tr_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Transponder</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="tr_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="tr_ok_button">
<property name="label">gtk-ok</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="tr_ok_button">
<property name="label">gtk-ok</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child internal-child="vbox">

View File

@@ -3,14 +3,19 @@ import time
import concurrent.futures
from math import fabs
from gi.repository import GLib
from app.commons import run_idle, run_task
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey
from .dialogs import show_dialog, DialogType, WaitDialog
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, get_dialogs_string
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
def show_satellites_dialog(transient, options):
SatellitesDialog(transient, options).show()
@@ -41,15 +46,14 @@ class SatellitesDialog:
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
builder.connect_signals(handlers)
self._window = builder.get_object("satellites_editor_window")
self._window.set_transient_for(transient)
self._sat_view = builder.get_object("satellites_editor_tree_view")
self._wait_dialog = WaitDialog(self._window)
# Setting the last size of the dialog window if it was saved
window_size = self._options.get("sat_editor_window_size", None)
if window_size:
@@ -59,7 +63,12 @@ class SatellitesDialog:
4: builder.get_object("fec_store"),
5: builder.get_object("system_store"),
6: builder.get_object("mod_store")}
self.on_satellites_list_load(self._sat_view.get_model())
self.load_satellites_list(self._sat_view.get_model())
def load_satellites_list(self, model):
gen = self.on_satellites_list_load(model)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def show(self):
self._window.show()
@@ -83,7 +92,7 @@ class SatellitesDialog:
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
return
self._data_path = response
self.on_satellites_list_load(model)
self.load_satellites_list(model)
def get_file_dialog_response(self, action: Gtk.FileChooserAction):
file_filter = Gtk.FileFilter()
@@ -132,25 +141,20 @@ class SatellitesDialog:
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
view.do_unselect_all(view)
@run_idle
def on_satellites_list_load(self, model):
""" Load satellites data into model """
try:
self._wait_dialog.show()
satellites = get_satellites(self._data_path)
yield True
except FileNotFoundError as e:
show_dialog(DialogType.ERROR, self._window, getattr(e, "message", str(e)) +
"\n\nPlease, download files from receiver or setup your path for read data!")
return
else:
model.clear()
self.append_data(model, satellites)
finally:
self._wait_dialog.hide()
@run_idle
def append_data(self, model, satellites):
for sat in satellites:
append_satellite(model, sat)
for sat in satellites:
append_satellite(model, sat)
yield True
def on_add(self, view):
""" Common adding """
@@ -315,9 +319,9 @@ class TransponderDialog:
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("transponder_dialog")
@@ -360,7 +364,7 @@ class TransponderDialog:
self._fec_box.set_active_id(transponder.fec_inner)
self._sys_box.set_active_id(transponder.system)
self._mod_box.set_active_id(transponder.modulation)
self._pls_mode_box.set_active_id(transponder.pls_mode)
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
@@ -371,7 +375,7 @@ class TransponderDialog:
fec_inner=self._fec_box.get_active_id(),
system=self._sys_box.get_active_id(),
modulation=self._mod_box.get_active_id(),
pls_mode=self._pls_mode_box.get_active_id(),
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text())
@@ -399,8 +403,8 @@ class SatelliteDialog:
def __init__(self, transient, satellite: Satellite = None):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellite_dialog", "side_store", "pos_adjustment"))
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("satellite_dialog", "side_store", "pos_adjustment"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)

View File

@@ -231,7 +231,7 @@
</child>
</object>
<object class="GtkDialog" id="service_details_dialog">
<property name="use-header-bar">1</property>
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Service data</property>
<property name="resizable">False</property>
@@ -1547,7 +1547,7 @@
</columns>
</object>
<object class="GtkDialog" id="tr_services_dialog">
<property name="use-header-bar">1</property>
<property name="use-header-bar">{use_header}</property>
<property name="width_request">480</property>
<property name="height_request">300</property>
<property name="can_focus">False</property>

View File

@@ -7,10 +7,12 @@ from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, P
get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION, TrType, \
SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC
from app.properties import Profile
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column
from .dialogs import show_dialog, DialogType, Action
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .main_helper import get_base_model
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
class ServiceDetailsDialog:
_ENIGMA2_DATA_ID = "{:04x}:{:08x}:{:04x}:{:04x}:{}:{}"
@@ -44,7 +46,7 @@ class ServiceDetailsDialog:
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "service_details_dialog.glade")
builder.add_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION))
builder.connect_signals(handlers)
self._builder = builder
@@ -65,9 +67,9 @@ class ServiceDetailsDialog:
self._current_model = None
self._current_itr = None
# Patterns
self._DIGIT_PATTERN = re.compile("\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\s]*$|\D)")
self._CAID_PATTERN = re.compile("(?:^[\s]*$)|(C:[0-9a-z]{4})(,C:[0-9a-z]{4})*")
self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-z]{4})(,C:[0-9a-z]{4})*")
# Buttons
self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button")
@@ -814,8 +816,8 @@ class TransponderServicesDialog:
def __init__(self, transient, model, transponder, tr_iters):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "service_details_dialog.glade",
("tr_services_dialog", "transponder_services_liststore"))
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("tr_services_dialog", "transponder_services_liststore"))
self._dialog = builder.get_object("tr_services_dialog")
self._dialog.set_transient_for(transient)
self._srv_model = builder.get_object("transponder_services_liststore")

View File

@@ -59,17 +59,15 @@ Author: Dmitriy Yefremov
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Options</property>
<property name="subtitle" translatable="yes">Profile: Enigma2</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="ok_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Ok</property>
<property name="valign">center</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="ok_button_image">
@@ -82,11 +80,11 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="apply_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save</property>
<property name="valign">center</property>
<signal name="clicked" handler="apply_settings" swapped="no"/>
<child>
<object class="GtkImage" id="apply_button_image">
@@ -100,6 +98,92 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child type="title">
<object class="GtkBox">
<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">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Options</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="GtkBox" id="settings_profile_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<child>
<object class="GtkLabel" id="active_profile_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">2</property>
<property name="label" translatable="yes">Profile:</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="enigma_radio_button">
<property name="label">Enigma2 </property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
<property name="group">neutrino_radio_button</property>
<signal name="toggled" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="neutrino_radio_button">
<property name="label">Neutrino-MP(experimental)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
<property name="group">enigma_radio_button</property>
</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">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="reset_button">
<property name="visible">True</property>
@@ -107,9 +191,7 @@ Author: Dmitriy Yefremov
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Reset profile</property>
<property name="halign">end</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="valign">center</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_reset" swapped="no"/>
<child>
@@ -177,7 +259,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="network_settings_frame">
@@ -467,6 +548,7 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="telnet_login_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">15</property>
<property name="primary_icon_name">avatar-default-symbolic</property>
@@ -491,6 +573,7 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="telnet_password_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">15</property>
<property name="primary_icon_name">emblem-readonly</property>
@@ -709,7 +792,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="stb_paths_frame">
@@ -840,6 +922,7 @@ Author: Dmitriy Yefremov
<object class="GtkFrame" id="local_file_paths_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
@@ -965,74 +1048,6 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="settings_profile_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="settings_profile_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_bottom">5</property>
<property name="spacing">5</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkRadioButton" id="enigma_radio_button">
<property name="label">Enigma2 </property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
<property name="group">neutrino_radio_button</property>
<signal name="toggled" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="neutrino_radio_button">
<property name="label">Neutrino-MP(experimental)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
<property name="group">enigma_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="active_profile_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Active profile:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="backup_frame">
<property name="visible">True</property>
@@ -1077,6 +1092,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">1</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Before saving</property>
<property name="xalign">0</property>
@@ -1111,22 +1127,22 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="program_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="program_frame">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
@@ -1183,7 +1199,6 @@ Author: Dmitriy Yefremov
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">5</property>
<property name="column_spacing">20</property>
<child>
@@ -1262,43 +1277,166 @@ Author: Dmitriy Yefremov
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="extra_support_grid">
<object class="GtkBox" id="parogram_extra_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_bottom">10</property>
<property name="row_spacing">5</property>
<property name="column_spacing">50</property>
<property name="column_homogeneous">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCheckButton" id="support_ver5_check_button">
<property name="label" translatable="yes">Ver. 5 support
(experimental)</property>
<object class="GtkGrid" id="extra_support_grid">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">5</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkSwitch" id="support_ver5_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="support_http_api_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
<signal name="state-set" handler="on_http_mode_switch_state" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Enable ver. 5 support (experimental)</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Enable HTTP API (experimental)</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="support_http_api_check_button">
<property name="label" translatable="yes">Enable HTTP API
(experimental)</property>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Double click on the service in the bouquet list:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="expand">True</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="double_click_mode_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_bottom">5</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkRadioButton" id="click_mode_zap_button">
<property name="label" translatable="yes">Zap</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="tooltip_text" translatable="yes">Switch(zap) the channel(Ctrl + Z)</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">click_mode_disabled_button</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="click_mode_play_button">
<property name="label" translatable="yes">Play</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="tooltip_text" translatable="yes">Switch the channel and watch in the program(Ctrl + W)</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">click_mode_stream_button</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="click_mode_stream_button">
<property name="label" translatable="yes">Play stream</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Play IPTV or other stream in the program(Ctrl + P)</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">click_mode_play_button</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="click_mode_disabled_button">
<property name="label" translatable="yes">Disabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Disabled</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">click_mode_play_button</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
@@ -1321,7 +1459,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">1</property>
</packing>
</child>
</object>

View File

@@ -1,12 +1,9 @@
from enum import Enum
from gi.repository import Gdk
from app.commons import run_task, run_idle
from app.connections import test_telnet, test_ftp, TestException, test_http
from app.properties import write_config, Profile, get_default_settings
from app.ui.dialogs import get_message
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, NEW_COLOR, EXTRA_COLOR
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, NEW_COLOR, EXTRA_COLOR, FavClickMode
from .main_helper import update_entry_data
@@ -29,7 +26,8 @@ class SettingsDialog:
"apply_settings": self.apply_settings,
"on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch_state": self.on_set_color_switch_state}
"on_set_color_switch_state": self.on_set_color_switch_state,
"on_http_mode_switch_state": self.on_http_mode_switch_state}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
@@ -72,11 +70,16 @@ class SettingsDialog:
# Program
self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._program_box = builder.get_object("program_box")
self._program_frame = builder.get_object("program_frame")
self._extra_support_grid = builder.get_object("extra_support_grid")
self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
self._click_mode_disabled_button = builder.get_object("click_mode_disabled_button")
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
self._click_mode_play_button = builder.get_object("click_mode_play_button")
self._click_mode_zap_button = builder.get_object("click_mode_zap_button")
# Options
self._options = options
self._active_profile = options.get("profile")
@@ -87,8 +90,11 @@ class SettingsDialog:
is_enigma_profile = profile is Profile.ENIGMA_2
self._neutrino_radio_button.set_active(profile is Profile.NEUTRINO_MP)
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
self._program_box.set_sensitive(is_enigma_profile)
self.update_subtitle(profile)
self._program_frame.set_sensitive(is_enigma_profile)
self._extra_support_grid.set_sensitive(is_enigma_profile)
http_active = self._support_http_api_check_button.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._click_mode_play_button.set_sensitive(is_enigma_profile and http_active)
def show(self):
response = self._dialog.run()
@@ -107,10 +113,6 @@ class SettingsDialog:
self.set_settings()
self.init_ui_elements(profile)
def update_subtitle(self, profile):
sub = "{} Enigma2" if profile is Profile.ENIGMA_2 else "{} Neutrino-MP"
self._header_bar.set_subtitle(sub.format(get_message("Profile:")))
def set_profile(self, profile):
self._active_profile = profile.value
self.set_settings()
@@ -129,6 +131,7 @@ class SettingsDialog:
def set_settings(self):
def_settings = get_default_settings().get(self._active_profile)
options = self._options.get(self._active_profile)
self._host_field.set_text(options.get("host", def_settings["host"]))
self._port_field.set_text(options.get("port", def_settings["port"]))
self._login_field.set_text(options.get("user", def_settings["user"]))
@@ -150,6 +153,7 @@ class SettingsDialog:
self._before_save_switch.set_active(options.get("backup_before_save", def_settings["backup_before_save"]))
self._before_downloading_switch.set_active(options.get("backup_before_downloading",
def_settings["backup_before_downloading"]))
self.set_fav_click_mode(options.get("fav_click_mode", def_settings["fav_click_mode"]))
if Profile(self._active_profile) is Profile.ENIGMA_2:
self._support_ver5_check_button.set_active(options.get("v5_support", False))
@@ -187,6 +191,7 @@ class SettingsDialog:
options["backup_dir_path"] = self._backup_dir_field.get_text()
options["backup_before_save"] = self._before_save_switch.get_active()
options["backup_before_downloading"] = self._before_downloading_switch.get_active()
options["fav_click_mode"] = self.get_fav_click_mode()
if profile is Profile.ENIGMA_2:
options["v5_support"] = self._support_ver5_check_button.get_active()
@@ -258,6 +263,30 @@ class SettingsDialog:
def on_set_color_switch_state(self, switch, state):
self._colors_grid.set_sensitive(state)
def on_http_mode_switch_state(self, switch, state):
self._click_mode_play_button.set_sensitive(state)
self._click_mode_zap_button.set_sensitive(state)
if self._click_mode_play_button.get_active() or self._click_mode_zap_button.get_active():
self._click_mode_disabled_button.set_active(True)
@run_idle
def set_fav_click_mode(self, mode):
mode = FavClickMode(mode)
self._click_mode_disabled_button.set_active(mode is FavClickMode.DISABLED)
self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM)
self._click_mode_play_button.set_active(mode is FavClickMode.PLAY)
self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP)
def get_fav_click_mode(self):
if self._click_mode_zap_button.get_active():
return FavClickMode.ZAP
if self._click_mode_play_button.get_active():
return FavClickMode.PLAY
if self._click_mode_stream_button.get_active():
return FavClickMode.STREAM
return FavClickMode.DISABLED
if __name__ == "__main__":
pass

View File

@@ -10,6 +10,8 @@ from gi.repository import Gtk, Gdk
# path to *.glade files
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
# translation
TEXT_DOMAIN = "demon-editor"
if UI_RESOURCES_PATH == "app/ui/":
@@ -24,7 +26,8 @@ LOCKED_ICON = theme.load_icon("changes-prevent-symbolic", 16, 0) if theme.lookup
"system-lock-screen", 16, 0) else _IMAGE_MISSING
HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING
TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.load_icon("emblem-shared", 16, 0) else None
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.lookup_icon("emblem-shared", 16, 0) else None
EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index", 16, 0) else None
# Colors
NEW_COLOR = "rgb(255,230,204)" # Color for new services in the main list
@@ -32,17 +35,22 @@ EXTRA_COLOR = "rgb(179,230,204)" # Color for services with a extra name for the
class KeyboardKey(Enum):
""" The raw(hardware) codes of the keyboard keys """
""" The raw(hardware) codes of the keyboard keys. """
Q = 24
E = 26
R = 27
T = 28
U = 30
O = 32
P = 33
S = 39
D = 40
H = 43
L = 46
X = 53
C = 54
V = 55
B = 56
W = 25
Z = 52
INSERT = 118
@@ -55,6 +63,7 @@ class KeyboardKey(Enum):
LEFT = 113
RIGHT = 114
F2 = 68
SPACE = 65
DELETE = 119
BACK_SPACE = 22
CTRL_L = 37
@@ -75,15 +84,23 @@ MOVE_KEYS = (KeyboardKey.UP, KeyboardKey.PAGE_UP, KeyboardKey.DOWN, KeyboardKey.
KeyboardKey.END, KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP)
class FavClickMode(IntEnum):
""" Double click mode on the service in the bouquet(FAV) list. """
DISABLED = 0
STREAM = 1
PLAY = 2
ZAP = 3
class ViewTarget(Enum):
""" Used for set target view """
""" Used for set target view. """
BOUQUET = 0
FAV = 1
SERVICES = 2
class BqGenType(Enum):
""" Bouquet generation type """
""" Bouquet generation type. """
SAT = 0
EACH_SAT = 1
PACKAGE = 2
@@ -129,6 +146,11 @@ class Column(IntEnum):
FAV_PICON = 8
FAV_TOOLTIP = 9
FAV_BACKGROUND = 10
# bouquets view
BQ_NAME = 0
BQ_LOCKED = 1
BQ_HIDDEN = 2
BQ_TYPE = 3
def __index__(self):
""" Overridden to get the index in slices directly """

View File

@@ -1,5 +1,5 @@
#!/bin/bash
VER="0.4.3_Pre-alpha"
VER="0.4.5_Pre-alpha"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"

View File

@@ -1,15 +1,13 @@
demon-editor for Debian
----------------------
DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc)
Keyboard shortcuts:
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning
or inserts (creates) a new bouquet.
Keyboard shortcuts:
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
Ctrl + BackSpace - copies the selected channels from the main list to the bouquet end.
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
Clipboard is "rubber". There is an accumulation before the insertion!
@@ -19,27 +17,32 @@ Keyboard shortcuts:
Ctrl + L - parental lock.
Ctrl + H - hide/skip.
Ctrl + P - start play IPTV or other stream in the bouquet list.
Ctrl + Z - switch(zap) the channel(works when the HTTP API is enabled, Enigma2 only).
Ctrl + Z - switch (zap) the channel (works when the HTTP API is enabled, Enigma2 only).
Ctrl + W - switch to the channel and watch in the program.
Space - select/deselect.
Left/Right - remove selection.
Ctrl + Up, Down, PageUp, PageDown, Home, End - move selected items in the list.
Ctrl + O - (re)load user data from current dir.
Ctrl + D - load data from receiver.
Ctrl + U/B upload data/bouquets to receiver.
Extra:
Import feature.
Multiple selections in lists only with Space key (as in file managers).
Ability to import IPTV into bouquet (Neutrino WEBTV) from m3u files.
Ability to download picons and update satellites (transponders) from web.
Preview (playing) IPTV or other streams directly from the bouquet list(should be installed VLC).
Ability to import into bouquet (Neutrino WEB TV) from m3u.
Ability to export bouquets with IPTV services to m3u.
Assignment EPG from DVB or XML for IPTV services(Enigma2 only).
Preview (playing) IPTV or other streams directly from the bouquet list (should be installed VLC).
Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings.
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings.
Launching
Note.
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
Terrestrial(DVB-T/T2) and cable channels are supported(Enigma2 only) with limitation!
Main supported lamedb format is version 4. Versions 3 and 5 has only experimental support!
For version 3 is only read mode available. When saving, version 4 format is used instead!
Important:
Main supported lamedb format is version 4. Versions 3 and 5 has only experimental support!
For version **3** is only read mode available. When saving, version **4** format is used instead!

View File

@@ -1,5 +1,5 @@
Package: DemonEditor
Version: 0.4.3-Pre-alpha
Version: 0.4.5-Pre-alpha
Section: utils
Priority: optional
Architecture: all

View File

@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018 Dmitriy Yefremov
Copyright (c) 2018-2019 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

View File

@@ -1,2 +1,2 @@
#!/bin/bash
python3 /usr/share/demoneditor/start.py
python3 /usr/share/demoneditor/start.py $1

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018 Frank Neirynck
# Copyright (C) 2018-2019 Frank Neirynck
# This file is distributed under the MIT license.
#
#Frank Neirynck <frank@insink.be>, 2018.
# Frank Neirynck <frank@insink.be>, 2018-2019.
#
msgid ""
msgstr ""
@@ -10,6 +10,11 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
msgid "translator-credits"
msgstr "Frank Neirynck <frank@insink.be>"
@@ -336,8 +341,13 @@ msgstr "Recibir picons para proovedor"
msgid "Load satellite providers."
msgstr "Cargar proovedores Satélite."
msgid "To automatically set the identifiers for picons,\nfirst load the required services list into the main application window."
msgstr "Para configurar automáticamente los identificadores para picons, \nprimero cargue la lista de serviços requeridos en la ventana principal."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Para configurar automáticamente los identificadores para picons, \n"
"primero cargue la lista de serviços requeridos en la ventana principal."
# Satellites editor
msgid "Satellites edit tool"
@@ -384,8 +394,12 @@ msgstr "Datos servicio"
msgid "Transponder details"
msgstr "Detalles Transpondedor"
msgid "Changes will be applied to all services of this transponder!\nContinue?"
msgstr "Los cambios se aplicarán a todos los servicios de este transpondedor!\nContinuar?"
msgid ""
"Changes will be applied to all services of this transponder!\n"
"Continue?"
msgstr ""
"Los cambios se aplicarán a todos los servicios de este transpondedor!\n"
"Continuar?"
msgid "Reference"
msgstr "Referencia"
@@ -434,7 +448,7 @@ msgstr "Restablecer a predeterminado"
msgid "IPTV streams list configuration"
msgstr "Configurar lista de Secuencias IPTV"
#Settings dialog
# Settings dialog
msgid "Preferences"
msgstr "Preferencias"
@@ -593,6 +607,9 @@ msgstr "Backup"
msgid "Backups"
msgstr "Backups"
msgid "Backup path:"
msgstr "Ruta del backup:"
msgid "Restore bouquets"
msgstr "Restaurar ramos"
@@ -614,8 +631,135 @@ msgstr "Marcado como nuevo:"
msgid "With an extra name in the bouquet:"
msgstr "Con nombre adicional en ramo:"
msgid "Select"
msgstr "Seleccione"
msgid "About"
msgstr "Sobre"
msgid "Exit"
msgstr "Salir"
msgid "Tools"
msgstr "Herramientas"
# Import
msgid "Import"
msgstr "Importar"
msgid "Bouquet"
msgstr "Ramo"
msgid "Bouquets and services"
msgstr "Ramos y servicios"
msgid "The main list does not contain services for this bouquet!"
msgstr "La lista principal no contiene servicios para este ramo!"
msgid "No bouquet file is selected!"
msgstr "Nigún fichero de ramo ha sido seleccionado!"
msgid "Remove all unused"
msgstr "Quite todos los"
msgid "Test"
msgstr "Prueba"
msgid "Test connection"
msgstr "Probar conexión"
msgid "Double click on the service in the bouquet list:"
msgstr "Haga doble clic en el servicio en la lista de bouquet:"
msgid "Zap"
msgstr "Zapear"
msgid "Play stream"
msgstr "Reproducir secuencia"
msgid "Disabled"
msgstr "Desactivado"
msgid "Enable ver. 5 support (experimental)"
msgstr "Soporte para ver. 5 (experimental)"
msgid "Enable HTTP API (experimental)"
msgstr "Habilitar API HTTP (experimental)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Cambiar (ZAP) el canal (Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Cambiar el canal y ver en el programa (Ctrl + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Reproducir IPTV u otro flujo en el programa (Ctrl + P)"
msgid "Export to m3u"
msgstr "Exportar hacia m3u"
msgid "EPG configuration"
msgstr "Configuración EPG"
msgid "Apply"
msgstr "Aplicar"
msgid "EPG source"
msgstr "Fuente EPG"
msgid "Service names source:"
msgstr "Nombre de servicio fuente:"
msgid "Main service list"
msgstr "Lista principal de servicios:"
msgid "XML file"
msgstr "Archivo XML"
msgid "Use web source"
msgstr "Usar fuente web"
msgid "Url to *.xml.gz file:"
msgstr "URL del archivo *.xml.gz:"
msgid "Enable filtering"
msgstr "Habilitar filtrar"
msgid "Filter by presence in the epg.dat file."
msgstr "Filtrar por presencia del archivo epg.dat."
msgid "Paths to the epg.dat file:"
msgstr "Ruta al archivo epg.dat:"
msgid "Local path:"
msgstr "Ruta local:"
msgid "STB path:"
msgstr "Ruta STB:"
msgid "Update on start"
msgstr "Actualisar al iniciar"
msgid "Auto configuration by service names."
msgstr "Auto configuración por nombres de servicios."
msgid "Save list to xml."
msgstr "Guardar como XML."
msgid "Download XML file error."
msgstr "Error bajando archivo XML."
msgid "Unsupported file type:"
msgstr "Archivo no supportado:"
msgid "Unpacking data error."
msgstr "Error abriende datos."
msgid "XML parsing error:"
msgstr "Error analisando XML:"
msgid "Count of successfully configured services:"
msgstr "Número de servicios configurados con éxito:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Archivo epg.dat actual no tiene referencias a servicios de este ramo!"

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018 Frank Neirynck
# Copyright (C) 2018-2019 Frank Neirynck
# This file is distributed under the MIT license.
#
#Frank Neirynck <frank@insink.be>, 2018.
# Frank Neirynck <frank@insink.be>, 2018-2019.
#
msgid ""
msgstr ""
@@ -10,6 +10,11 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
msgid "translator-credits"
msgstr "Frank Neirynck <frank@insink.be>"
@@ -336,8 +341,12 @@ msgstr "Ontvang picons voor leveranciers"
msgid "Load satellite providers."
msgstr "Laad satelliet leveranciers."
msgid "To automatically set the identifiers for picons,\nfirst load the required services list into the main application window."
msgstr "Om automatisch de ID in te stellen voor picons,\nlaad eerst de vereiste serviceslijst in via het hoofdvenster van het programma."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Om automatisch de ID in te stellen voor picons,\n"
"laad eerst de vereiste serviceslijst in via het hoofdvenster van het programma."
# Satellites editor
msgid "Satellites edit tool"
@@ -384,8 +393,12 @@ msgstr "Gegevens Dienst"
msgid "Transponder details"
msgstr "Details Transponder"
msgid "Changes will be applied to all services of this transponder!\nContinue?"
msgstr "Wijzigingen zullen worden doorgevoerd op alle diensten van deze transponder!\nDoorgaan?"
msgid ""
"Changes will be applied to all services of this transponder!\n"
"Continue?"
msgstr ""
"Wijzigingen zullen worden doorgevoerd op alle diensten van deze transponder!\n"
"Doorgaan?"
msgid "Reference"
msgstr "Referentie"
@@ -434,7 +447,7 @@ msgstr "Reset naar standaard"
msgid "IPTV streams list configuration"
msgstr "Configureren IPTV Streamlijst"
#Settings dialog
# Settings dialog
msgid "Preferences"
msgstr "Voorkeuren"
@@ -593,8 +606,11 @@ msgstr "Backup"
msgid "Backups"
msgstr "Backups"
msgid "Backup path:"
msgstr "Backup pad:"
msgid "Restore bouquets"
msgstr "Herstek boeketten"
msgstr "Herstel boeketten"
msgid "Restore all"
msgstr "Herstel alles"
@@ -614,8 +630,136 @@ msgstr "Gemarkeerd als nieuw:"
msgid "With an extra name in the bouquet:"
msgstr "Met een extra naam in het boeket:"
msgid "Select"
msgstr "Over"
msgid "About"
msgstr "Over"
msgid "Exit"
msgstr "Exit"
msgid "Tools"
msgstr "Tools"
# Import
msgid "Import"
msgstr "Importeer"
msgid "Bouquet"
msgstr "Boeket"
msgid "Bouquets and services"
msgstr "Boeketten en diensten"
msgid "The main list does not contain services for this bouquet!"
msgstr "De hoofdlijst bevat geen diensten voor dit boeket!"
msgid "No bouquet file is selected!"
msgstr "Geen boeket geselecteerd!"
msgid "Remove all unused"
msgstr "Verwijder alle ongebruikte"
msgid "Test"
msgstr "Test"
msgid "Test connection"
msgstr "Test verbinding"
msgid "Double click on the service in the bouquet list:"
msgstr "Dubbelklik op de dienst in de boeket lijst:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Speel stream af"
msgid "Disabled"
msgstr "Uitgeschakeld"
msgid "Enable ver. 5 support (experimental)"
msgstr "Ondersteuning voor ver. 5 inschakelen (experimenteel)"
msgid "Enable HTTP API (experimental)"
msgstr "HTTP API inschakelen (experimenteel)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Schakel het kanaal in en bekijk het programma (CTRL + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Speel IPTV of andere stream af en bekijk het programma (CTRL + P)"
msgid "Export to m3u"
msgstr "Uitvoeren naar m3u"
msgid "EPG configuration"
msgstr "Configureer EPG"
msgid "Apply"
msgstr "Toepassen"
msgid "EPG source"
msgstr "Bron EPG"
msgid "Service names source:"
msgstr "Naam van bron van de dienst:"
msgid "Main service list"
msgstr "Hoofdlijst diensten:"
msgid "XML file"
msgstr "XML file"
msgid "Use web source"
msgstr "Gebruik web bron"
msgid "Url to *.xml.gz file:"
msgstr "URL van de *.xml.gz file:"
msgid "Enable filtering"
msgstr "Zet filteren aan"
msgid "Filter by presence in the epg.dat file."
msgstr "Filter op aanwezigheid van epg.dat. file"
msgid "Paths to the epg.dat file:"
msgstr "Pad naar epg.dat:"
msgid "Local path:"
msgstr "Lokaal pad:"
msgid "STB path:"
msgstr "STB pad:"
msgid "Update on start"
msgstr "Actualiseer bij start"
msgid "Auto configuration by service names."
msgstr "Auto configuratie door dienst namen."
msgid "Save list to xml."
msgstr "Opslaan als XML."
msgid "Download XML file error."
msgstr "Fout bij downloaden XML."
msgid "Unsupported file type:"
msgstr "Ongesupporteerd archieftype:"
msgid "Unpacking data error."
msgstr "Fout bij uitpakken van de data."
msgid "XML parsing error:"
msgstr "XML parsingfout:"
msgid "Count of successfully configured services:"
msgstr "Aantal succesvol geconfigureerde diensten:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Huisige epg.dat bestand heeft geen referenties naar de diensten van dit boeket!"

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018 Frank Neirynck
# Copyright (C) 2018-2019 Frank Neirynck
# This file is distributed under the MIT license.
#
#Frank Neirynck <frank@insink.be>, 2018.
#Frank Neirynck <frank@insink.be>, 2018-2019.
#
msgid ""
msgstr ""
@@ -593,6 +593,9 @@ msgstr "Backup"
msgid "Backups"
msgstr "Backups"
msgid "Backup path:"
msgstr "Rota do backup:"
msgid "Restore bouquets"
msgstr "Restaurar ramos"
@@ -614,8 +617,135 @@ msgstr "Marcado como novo:"
msgid "With an extra name in the bouquet:"
msgstr "Com nome adicional em ramo:"
msgid "Select"
msgstr "Selecione"
msgid "About"
msgstr "Acerca"
msgid "Exit"
msgstr "Sair"
msgid "Tools"
msgstr "Tools"
#Import
msgid "Import"
msgstr "Importar"
msgid "Bouquet"
msgstr "Ramo"
msgid "Bouquets and services"
msgstr "Ramos e serviços"
msgid "The main list does not contain services for this bouquet!"
msgstr "A lista pricipal no tem serviços em esta ramo!"
msgid "No bouquet file is selected!"
msgstr "Nemhuma ficheiro de ramo foi selecionado!"
msgid "Remove all unused"
msgstr "Remova todos os não utilizados"
msgid "Test"
msgstr "Test"
msgid "Test connection"
msgstr "Testar a conexão"
msgid "Double click on the service in the bouquet list:"
msgstr "Clique duas vezes no serviço na lista de ramos:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Play stream"
msgid "Disabled"
msgstr "Desativado"
msgid "Enable ver. 5 support (experimental)"
msgstr "Ativar ver. 5 suporte (experimental)"
msgid "Enable HTTP API (experimental)"
msgstr "Ativar HTTP API (experimental)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Mudar(zap) o canal(Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Troque o canal e ver no programa(Ctrl + W)."
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Tocar IPTV ou outro fluxo no programa(Ctrl + P)"
msgid "Export to m3u"
msgstr "Exportar na m3u"
msgid "EPG configuration"
msgstr "Configuraçao EPG"
msgid "Apply"
msgstr "Aplicar"
msgid "EPG source"
msgstr "Fonte EPG"
msgid "Service names source:"
msgstr "Fonte de nomes de serviço:"
msgid "Main service list"
msgstr "Lista de serviço principal:"
msgid "XML file"
msgstr "Arquivo XML"
msgid "Use web source"
msgstr "Usar fonte web"
msgid "Url to *.xml.gz file:"
msgstr "Url para o arquivo *.xml.gz:"
msgid "Enable filtering"
msgstr "Ativar filtragem"
msgid "Filter by presence in the epg.dat file."
msgstr "Filtrar por presença no arquivo epg.dat."
msgid "Paths to the epg.dat file:"
msgstr "Ruta para o arquivo epg.dat:"
msgid "Local path:"
msgstr "Ruta local:"
msgid "STB path:"
msgstr "Ruta STB:"
msgid "Update on start"
msgstr "Atualizar no início"
msgid "Auto configuration by service names."
msgstr "Configuração automática por nomes de serviço."
msgid "Save list to xml."
msgstr "Salvar lista para XML."
msgid "Download XML file error."
msgstr "Baixe o erro de arquivo XML."
msgid "Unsupported file type:"
msgstr "Tipo de arquivo não suportado:"
msgid "Unpacking data error."
msgstr "Descompactando o erro de dados."
msgid "XML parsing error:"
msgstr "Erro de análise XML:"
msgid "Count of successfully configured services:"
msgstr "Contagem de serviços configurados com sucesso:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "O arquivo epg.dat não contém referências para os serviços deste buquê!"

View File

@@ -1,10 +1,10 @@
# Copyright (C) 2018 Dmitriy Yefremov
# Copyright (C) 2018-2019 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
msgid ""
msgstr ""
"Last-Translator: Dmitry Yefremov\n"
"Last-Translator: Dmitriy Yefremov\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -625,6 +625,138 @@ msgstr "О программе"
msgid "Exit"
msgstr "Выход"
msgid "Tools"
msgstr "Инструменты"
#Import
msgid "Import"
msgstr "Импорт"
msgid "Bouquet"
msgstr "Букета"
msgid "Bouquets and services"
msgstr "Букетов и сервисов"
msgid "The main list does not contain services for this bouquet!"
msgstr "Основной список не содержит сервисов для данного букета!"
msgid "No bouquet file is selected!"
msgstr "Не выбран файл букета!"
msgid "Remove all unused"
msgstr "Удалить все неиспользуемые"
msgid "Test"
msgstr "Тестировать"
msgid "Test connection"
msgstr "Тестировать соединение"
msgid "Double click on the service in the bouquet list:"
msgstr "Двойной клик по сервису в списке букетов:"
msgid "Zap"
msgstr "Переключить"
msgid "Play stream"
msgstr "Воспр. потока"
msgid "Disabled"
msgstr "Выкл."
msgid "Enable ver. 5 support (experimental)"
msgstr "Включить поддержку вер. 5 (экспериментально)"
msgid "Enable HTTP API (experimental)"
msgstr "Включить HTTP API (экспериментально)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Переключить канал(Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Переклють канал и просмотр в программе(Ctrl + W)."
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Воспроизведение IPTV или другого потока в программе(Ctrl + P)"
msgid "Export to m3u"
msgstr "Экспорт в m3u"
msgid "EPG configuration"
msgstr "Конфигурация EPG"
msgid "Apply"
msgstr "Применить"
msgid "EPG source"
msgstr "Источник EPG"
msgid "Service names source:"
msgstr "Источник имен сервисов:"
msgid "Main service list"
msgstr "Основной список сервисов:"
msgid "XML file"
msgstr "Файл XML"
msgid "Use web source"
msgstr "Использовать веб-источник"
msgid "Url to *.xml.gz file:"
msgstr "URL к файлу *.xml.gz:"
msgid "Enable filtering"
msgstr "Включить фильтрацию"
msgid "Filter by presence in the epg.dat file."
msgstr "Фильтровать по наличию в файле epg.dat."
msgid "Paths to the epg.dat file:"
msgstr "Пути к файлу epg.dat:"
msgid "Local path:"
msgstr "Локальный путь:"
msgid "STB path:"
msgstr "Путь в ресивере:"
msgid "Update on start"
msgstr "Обновлять при запуске"
msgid "Auto configuration by service names."
msgstr "Автонастройка по именам сервисов."
msgid "Save list to xml."
msgstr "Сохранить список в XML."
msgid "Download XML file error."
msgstr "Ошибка загрузки XML-файла."
msgid "Unsupported file type:"
msgstr "Неподдерживаемый тип файла:"
msgid "Unpacking data error."
msgstr "Ошибка распаковки данных."
msgid "XML parsing error:"
msgstr "Ошибка парсинга XML:"
msgid "Count of successfully configured services:"
msgstr "Количество успешно сконфигурированных сервисов:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Текущий файл epg.dat не содержит ссылок на сервисы данного букета!"