Compare commits

...

107 Commits
0.4.4 ... 0.4.6

Author SHA1 Message Date
DYefremov
408a4cfa32 fix of services counter reset 2019-10-14 00:17:06 +03:00
DYefremov
fe3fb1fefe updating of dutch, spanish and portuguese 2019-10-11 15:51:20 +03:00
DYefremov
95b5c7aa74 fix hide info box on new config creation 2019-10-10 12:55:58 +03:00
DYefremov
d2e80c6d44 translation update 2019-10-06 09:50:16 +03:00
DYefremov
57f157ef9b added app info box 2019-10-04 21:31:41 +03:00
DYefremov
f62f104c8d added rewind support to the player 2019-10-02 14:44:58 +03:00
DYefremov
8d6b1303dc slight refactoring of http api 2019-09-28 21:57:41 +03:00
DYefremov
de770169fa changed requests to compressed 2019-09-28 17:44:33 +03:00
DYefremov
7b2e467111 updated yt playlist parser 2019-09-22 16:54:20 +03:00
DYefremov
1c1dff2497 fix sensitivity for iptv elems in neutrino 2019-09-16 17:44:33 +03:00
DYefremov
309f960e1e minor clean 2019-09-10 00:37:48 +03:00
DYefremov
dadb73280c changed callbacks for loading picons 2019-09-10 00:28:38 +03:00
DYefremov
81260211a4 default callback set 2019-09-10 00:24:51 +03:00
DYefremov
d4d1dd397d slight refactoring of picons downloader 2019-09-09 23:48:36 +03:00
DYefremov
fe3e1ef30a fix apply default data for some iptv services 2019-09-08 18:45:12 +03:00
DYefremov
1f46eae6be changed counting of marker number 2019-09-04 10:39:46 +03:00
DYefremov
a398b0c6e9 revert default formats for youtube 2019-08-18 17:02:32 +03:00
DYefremov
272b6af3ad quality selection for a single yt link 2019-08-18 00:06:44 +03:00
DYefremov
d976e02cf6 added quality selection for yt playlist 2019-08-17 22:53:05 +03:00
DYefremov
fb6951896f changed getting yt link 2019-08-13 19:22:08 +03:00
DYefremov
9c62a49968 added min width 2019-08-11 21:24:51 +03:00
DYefremov
99bb508912 minor refactoring of data appending 2019-08-08 21:15:43 +03:00
DYefremov
e382a51f81 optimisation of data load/save 2019-08-01 01:05:30 +03:00
DYefremov
fdd30b2ac9 hiding header items during playback 2019-07-23 10:47:01 +03:00
DYefremov
f2b99f9eea fixed the order of links in the yt dialog 2019-07-19 16:02:00 +03:00
DYefremov
dea5723bb7 reworking of yt dialog 2019-06-30 22:13:26 +03:00
DYefremov
236a7a15d0 added keyboard shortcuts for the yt dialog 2019-06-28 23:25:38 +03:00
DYefremov
cfe281116a added logging for get yt link 2019-06-28 08:58:33 +03:00
DYefremov
984e8ca088 added popup menu for yt dialog 2019-06-28 00:02:34 +03:00
DYefremov
62eae7f029 minor changes in the yt dialog 2019-06-27 22:10:44 +03:00
DYefremov
204962b531 append yt list data 2019-06-26 15:57:22 +03:00
DYefremov
767c06a0ff merged yt dialog 2019-06-26 15:56:23 +03:00
DYefremov
d4ec28e9cd added getting max num of markers 2019-06-26 15:38:34 +03:00
DYefremov
8fee65cabb yt list import dialog skeleton 2019-06-24 00:36:54 +03:00
DYefremov
95069bbf24 refactoring of getting fav id 2019-06-23 23:25:03 +03:00
DYefremov
60f106bc2a added simple parser to handle yt playlists 2019-06-21 14:54:09 +03:00
DYefremov
4becdf1d6e added tooltip text for yt icon 2019-06-19 23:23:32 +03:00
DYefremov
f2f027c6f4 moved youtube logic to the extra module 2019-06-19 22:34:22 +03:00
DYefremov
edeab12b50 skeleton of basic support for youtube links 2019-06-18 18:46:27 +03:00
DYefremov
896aa7f66e added info bar for iptv dialog 2019-06-18 18:26:42 +03:00
DYefremov
6b6220a3ac youtube links detection skeleton 2019-06-15 23:50:42 +03:00
DYefremov
3fa46c6c6c version update 2019-06-15 22:06:41 +03:00
DYefremov
a0b322b188 set text for the question dialog 2019-06-15 21:43:24 +03:00
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
48 changed files with 5135 additions and 1708 deletions

View File

@@ -25,15 +25,18 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **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```
@@ -42,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):
def download_data(*, properties, download_type=DownloadType.ALL, callback=print):
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):
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,13 +71,26 @@ def download_data(*, properties, download_type=DownloadType.ALL, callback):
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")
callback("\nDone.\n")
def upload_data(*, properties, download_type=DownloadType.ALL, remove_unused=False, profile=Profile.ENIGMA_2,
callback=None, done_callback=None, use_http=False):
callback=print, done_callback=None, use_http=False):
data_path = properties["data_dir_path"]
host = properties["host"]
base_url = "http://{}:{}/api/".format(host, properties.get("http_port", "80"))
@@ -137,7 +151,7 @@ def upload_data(*, properties, download_type=DownloadType.ALL, remove_unused=Fal
upload_files(ftp, data_path, _DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS:
upload_picons(ftp, properties.get("picons_dir_path"), properties.get("picons_path"))
upload_picons(ftp, properties.get("picons_dir_path"), properties.get("picons_path"), callback)
if tn and not use_http:
# resume enigma or restart neutrino
@@ -188,7 +202,7 @@ def upload_xml(ftp, data_path, xml_path, xml_file, callback):
send_file(xml_file, data_path, ftp, callback)
def upload_picons(ftp, src, dest):
def upload_picons(ftp, src, dest, callback):
try:
ftp.cwd(dest)
except error_perm as e:
@@ -205,7 +219,7 @@ def upload_picons(ftp, src, dest):
ftp.delete(name)
for file_name in os.listdir(src):
if file_name.endswith(picons_suf):
send_file(file_name, src, ftp)
send_file(file_name, src, ftp, callback)
def download_file(ftp, name, save_path, callback):
@@ -259,6 +273,7 @@ def telnet(host, port=23, user="", password="", timeout=5):
def http_request(host, port, user, password):
base_url = "http://{}:{}/api/".format(host, port)
init_auth(user, password, base_url)
while True:
req_type, ref = yield
url = base_url
@@ -271,19 +286,22 @@ def http_request(host, port, user, password):
elif req_type is HttpRequestType.STREAM:
url = base_url + HttpRequestType.STREAM.value
try:
with urlopen(url, timeout=5) as f:
if req_type is HttpRequestType.STREAM:
yield f.read().decode("utf-8")
else:
yield json.loads(f.read().decode("utf-8"))
except (URLError, HTTPError):
yield None
yield from get_json(req_type, url)
def get_json(req_type, url):
try:
with urlopen(url, timeout=5) as f:
if req_type is HttpRequestType.STREAM:
yield f.read().decode("utf-8")
else:
yield json.loads(f.read().decode("utf-8"))
except (URLError, HTTPError):
yield None
# ***************** Connections testing *******************#
def test_ftp(host, port, user, password, timeout=5):
try:
with FTP(host=host, user=user, passwd=password, timeout=timeout) as ftp:

View File

@@ -14,9 +14,10 @@ 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_()]+")
current_marker = [0]
for bqs in bouquets:
line.clear()
@@ -28,23 +29,29 @@ 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))
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
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, current_marker)
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(path, name, channels):
def write_bouquet(path, name, services, current_marker):
bouquet = ["#NAME {}\n".format(name)]
marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
for ch in channels:
if ch.service_type == BqServiceType.IPTV.name or ch.service_type == BqServiceType.MARKER.name:
bouquet.append("#SERVICE {}\n".format(ch.fav_id.strip()))
for srv in services:
if srv.service_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif srv.service_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = current_marker[0]
current_marker[0] += 1
bouquet.append(marker.format(m_data[2], m_data[-1]))
else:
data = to_bouquet_id(ch)
if ch.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, ch.service, ch.service))
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
else:
bouquet.append("#SERVICE {}\n".format(data))
@@ -52,13 +59,13 @@ def write_bouquet(path, name, channels):
file.writelines(bouquet)
def to_bouquet_id(ch):
def to_bouquet_id(srv):
""" Creates bouquet channel id """
data_type = ch.data_id
data_type = srv.data_id
if data_type and len(data_type) > 4:
data_type = int(ch.data_id.split(":")[4])
data_type = int(srv.data_id.split(":")[4])
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, ch.fav_id)
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
def get_bouquet(path, name, bq_type):
@@ -99,7 +106,7 @@ def parse_bouquets(path, bq_name, bq_type):
if bouquets and "#SERVICE" in line:
name = re.match(bq_pattern, line)
if name:
b_name, services = get_bouquet(path, name.group(1), bq_type)
b_name, services = get_bouquet(path, name.group(1), bq_type)
bouquets[2].append(Bouquet(name=b_name,
type=bq_type,
services=services,

View File

@@ -1,4 +1,5 @@
""" Module for IPTV and streams support """
import re
import urllib.request
from enum import Enum
@@ -26,7 +27,7 @@ def parse_m3u(path, profile):
groups = set()
counter = 0
name = None
fav_id = None
for line in file.readlines():
if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip()
@@ -40,12 +41,7 @@ def parse_m3u(path, profile):
services.append(mr)
elif not line.startswith("#"):
url = line.strip()
if profile is Profile.ENIGMA_2:
url = urllib.request.quote(line.strip())
stream_type = StreamType.NONE_TS.value
fav_id = ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, name, name, None)
elif profile is Profile.NEUTRINO_MP:
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
fav_id = get_fav_id(url, name, profile)
if name and url:
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
services.append(srv)
@@ -53,5 +49,38 @@ 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)
def get_fav_id(url, service_name, profile):
""" Returns fav id depending on the profile. """
if profile is Profile.ENIGMA_2:
url = urllib.request.quote(url)
stream_type = StreamType.NONE_TS.value
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, service_name, service_name, None)
elif profile is Profile.NEUTRINO_MP:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
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):
@@ -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 = []

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

@@ -1,13 +1,25 @@
from app.commons import run_task
from app.tools import vlc
from app.tools.vlc import EventType
class Player:
_VLC_INSTANCE = None
def __init__(self):
def __init__(self, rewind_callback=None, position_callback=None):
self._is_playing = False
self._player = self.get_vlc_instance()
ev_mgr = self._player.event_manager()
if rewind_callback:
# TODO look other EventType options
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
lambda e, p: rewind_callback(p.get_media().get_duration()),
self._player)
if position_callback:
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda e, p: position_callback(p.get_time()),
self._player)
@staticmethod
def get_vlc_instance():
@@ -32,6 +44,9 @@ class Player:
def pause(self):
self._player.pause()
def set_time(self, time):
self._player.set_time(time)
@run_task
def release(self):
if self._player:

View File

@@ -7,6 +7,7 @@ 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
@@ -81,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:
@@ -104,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()
@@ -125,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]))
@@ -207,7 +210,7 @@ class SatellitesParser(HTMLParser):
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat """
frq_pol_pattern = re.compile("(\\d{4,5}).*([RLHV])(.*\\d$)")
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)
@@ -215,7 +218,10 @@ class SatellitesParser(HTMLParser):
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)

167
app/tools/yt.py Normal file
View File

@@ -0,0 +1,167 @@
""" Module for working with YouTube service """
import gzip
import json
import re
import urllib
from html.parser import HTMLParser
from json import JSONDecodeError
from urllib.request import Request
from app.commons import log
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{23,})?.*")
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0",
"DNT": "1",
"Accept-Encoding": "gzip, deflate"}
Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
class YouTube:
""" Helper class for working with YouTube service. """
@staticmethod
def is_yt_video_link(url):
return re.match(_YT_VIDEO_PATTERN, url)
@staticmethod
def get_yt_id(url):
""" Returns video id or None """
yt = re.search(_YT_PATTERN, url)
if yt:
return yt.group(1)
@staticmethod
def get_yt_list_id(url):
""" Returns playlist id or None """
yt = re.search(_YT_LIST_PATTERN, url)
if yt:
return yt.group(1)
@staticmethod
def get_yt_link(video_id):
""" Getting link to YouTube video by id.
returns tuple from the video links dict and title
"""
req = Request("https://youtube.com/get_video_info?video_id={}&hl=en".format(video_id), headers=_HEADERS)
with urllib.request.urlopen(req, timeout=2) as resp:
data = urllib.request.unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(urllib.request.unquote, data))}
player_resp = out.get("player_response", None)
if player_resp:
try:
resp = json.loads(player_resp)
except JSONDecodeError as e:
log("{}: Parsing player response error: {}".format(__class__.__name__, e))
else:
det = resp.get("videoDetails", None)
title = det.get("title", None) if det else None
streaming_data = resp.get("streamingData", None)
fmts = streaming_data.get("formats", None) if streaming_data else None
if fmts:
urls = {Quality[i["itag"]]: i["url"] for i in
filter(lambda i: i.get("itag", -1) in Quality, fmts)}
if urls and title:
return urls, title.replace("+", " ")
stream_map = out.get("url_encoded_fmt_stream_map", None)
if stream_map:
s_map = {k: v for k, sep, v in (str(d).partition("=") for d in stream_map.split("&"))}
url, title = s_map.get("url", None), out.get("title", None)
url, title = urllib.request.unquote(url) if url else "", title.replace("+", " ") if title else ""
if url and title:
return {Quality[0]: url}, title.replace("+", " ")
rsn = out.get("reason", None)
rsn = rsn.replace("+", " ") if rsn else ""
log("{}: Getting link to video with id {} filed! Cause: {}".format(__class__.__name__, video_id, rsn))
return None, rsn
class PlayListParser(HTMLParser):
""" Very simple parser to handle YouTube playlist pages. """
def __init__(self):
super().__init__()
self._is_header = False
self._header = ""
self._playlist = []
self._is_script = False
def handle_starttag(self, tag, attrs):
if tag == "script":
self._is_script = True
def handle_data(self, data):
if self._is_script:
data = data.lstrip()
if data.startswith('window["ytInitialData"] = '):
data = data.split(";")[0].lstrip('window["ytInitialData"] = ')
try:
resp = json.loads(data)
except JSONDecodeError as e:
log("{}: Parsing data error: {}".format(__class__.__name__, e))
else:
sb = resp.get("sidebar", None)
if sb:
for t in [t["runs"][0] for t in flat("title", sb) if "runs" in t]:
txt = t.get("text", None)
if txt:
self._header = txt
break
ct = resp.get("contents", None)
if ct:
for d in [(d["title"]["simpleText"], d["videoId"]) for d in flat("playlistVideoRenderer", ct)]:
self._playlist.append(d)
self._is_script = False
def error(self, message):
log("{} Parsing error: {}".format(__class__.__name__, message))
@property
def header(self):
return self._header
@property
def playlist(self):
return self._playlist
@staticmethod
def get_yt_playlist(play_list_id):
""" Getting YouTube playlist by id.
returns tuple from the playlist header and list of tuples (title, video id)
"""
request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS)
with urllib.request.urlopen(request, timeout=2) as resp:
data = gzip.decompress(resp.read()).decode("utf-8")
parser = PlayListParser()
parser.feed(data)
return parser.header, parser.playlist
def flat(key, d):
for k, v in d.items():
if k == key:
yield v
elif isinstance(v, dict):
yield from flat(key, v)
elif isinstance(v, list):
for el in v:
if isinstance(el, dict):
yield from flat(key, el)
if __name__ == "__main__":
pass

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,7 +40,7 @@ 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.4 Pre-alpha</property>
<property name="version">0.4.6 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>
@@ -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, text or "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

View File

@@ -173,9 +173,12 @@ class ImportDialog:
bq_services = self._bq_services.get(model.get(model.get_iter(paths[0]), 0, 1))
for bq_srv in bq_services:
srv = self._services.get(bq_srv.data, None)
if srv:
self._services_model.append((srv.service, srv.service_type))
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()

View File

@@ -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">
@@ -201,7 +225,7 @@ Author: Dmitriy Yefremov
</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>
@@ -406,14 +430,49 @@ Author: Dmitriy Yefremov
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkEntry" id="url_entry">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_url_changed" swapped="no"/>
<property name="can_focus">False</property>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Link to YouTube resource.</property>
<signal name="changed" handler="on_url_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="yt_iptv_quality_combobox">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Desired video quality</property>
<property name="model">yt_quality_liststore</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<signal name="changed" handler="on_yt_quality_changed" swapped="no"/>
<property name="active">0</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="yt_quality_renderer"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
@@ -598,6 +657,54 @@ Author: Dmitriy Yefremov
<property name="position">2</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="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">label</property>
</object>
<packing>
<property name="expand">True</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">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -612,10 +719,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>
@@ -1114,4 +1221,379 @@ Author: Dmitriy Yefremov
<action-widget response="-6">close_config_list_button</action-widget>
</action-widgets>
</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="yt_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="yt_list_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="yt_list_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="yt_liststore">
<columns>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name id -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
<!-- column-name tooltip -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="yt_quality_liststore">
<columns>
<!-- column-name quality -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Auto</col>
</row>
<row>
<col id="0">720p</col>
</row>
<row>
<col id="0">360p</col>
</row>
</data>
</object>
<object class="GtkWindow" id="yt_import_dialog_window">
<property name="width_request">480</property>
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">480</property>
<property name="destroy_with_parent">True</property>
<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="yt_header_bar">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="title" translatable="yes">YouTube</property>
<property name="subtitle" translatable="yes">Playlist import</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="yt_receive_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Receive</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
<child>
<object class="GtkImage" id="yt_receive_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-bottom</property>
</object>
</child>
<accelerator key="d" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="yt_import_button">
<property name="visible">False</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="yt_import_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">insert-link</property>
</object>
</child>
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="yt_quality_combobox">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Desired video quality</property>
<property name="model">yt_quality_liststore</property>
<property name="active">0</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="yt_quality_renderer"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="yt_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEntry" id="yt_url_entry">
<property name="visible">True</property>
<property name="can_focus">True</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="primary_icon_stock">gtk-edit</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Link to YouTube resource.</property>
<property name="placeholder_text" translatable="yes">YouTube playlist URL:</property>
<signal name="changed" handler="on_yt_url_entry_changed" swapped="no"/>
</object>
<packing>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="yt_list_view_scrolled_window">
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_height">150</property>
<child>
<object class="GtkTreeView" id="yt_list_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">yt_liststore</property>
<property name="search_column">0</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">3</property>
<signal name="button-press-event" handler="on_popup_menu" object="yt_popup_menu" 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"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="yt_title_column">
<property name="resizable">True</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Title</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="yt_title_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="yt_id_column">
<property name="visible">False</property>
<property name="title" translatable="yes">ID</property>
<child>
<object class="GtkCellRendererText" id="yt_id_renderer"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="yt_selected_column">
<property name="min_width">50</property>
<property name="max_width">100</property>
<property name="title" translatable="yes">Selected</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererToggle" id="yt_selected_renderer">
<property name="width">50</property>
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="yt_tooltip_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Tooltip</property>
<child>
<object class="GtkCellRendererText" id="yt_tooltip_renderer"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="yt_info_bar_box">
<property name="height_request">24</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">10</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="yt_cout_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage" id="yt_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="yt_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkProgressBar" id="yt_progress_bar">
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="pulse_step">0.01</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">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="yt_info_bar">
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_yt_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>
<child>
<placeholder/>
</child>
</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="yt_info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">info</property>
</object>
<packing>
<property name="expand">True</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>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -1,21 +1,28 @@
import concurrent.futures
import glob
import os
import re
import urllib
from functools import lru_cache
from urllib.error import HTTPError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
from app.commons import run_idle, run_task
from gi.repository import GLib
from app.commons import run_idle, run_task, log
from app.eparser.ecommons import BqServiceType, Service
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
from app.properties import Profile
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column
from .dialogs import Action, show_dialog, DialogType
from .main_helper import get_base_model, get_iptv_url
from app.tools.yt import YouTube, PlayListParser
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
from .main_helper import get_base_model, get_iptv_url, on_popup_menu
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey
_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):
@@ -36,6 +43,22 @@ def get_stream_type(box):
return StreamType.NONE_REC_2.value
@lru_cache(maxsize=1)
def get_yt_icon(icon_name, size=24):
""" Getting YouTube icon. If the icon is not found in the icon themes, the "Info" icon is returned by default! """
default_theme = Gtk.IconTheme.get_default()
if default_theme.has_icon(icon_name):
return default_theme.load_icon(icon_name, size, 0)
theme = Gtk.IconTheme.new()
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
theme.set_custom_theme(theme_name)
if theme.has_icon(icon_name):
return theme.load_icon(icon_name, size, 0)
return default_theme.load_icon("info", size, 0)
class IptvDialog:
def __init__(self, transient, view, services, bouquet, profile=Profile.ENIGMA_2, action=Action.ADD):
@@ -43,13 +66,22 @@ class IptvDialog:
"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
"on_save": self.on_save,
"on_stream_type_changed": self.on_stream_type_changed}
"on_stream_type_changed": self.on_stream_type_changed,
"on_yt_quality_changed": self.on_yt_quality_changed,
"on_info_bar_close": self.on_info_bar_close}
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", "yt_quality_liststore"))
builder.connect_signals(handlers)
self._action = action
self._profile = profile
self._bouquet = bouquet
self._services = services
self._yt_links = None
self._dialog = builder.get_object("iptv_dialog")
self._dialog.set_transient_for(transient)
self._name_entry = builder.get_object("name_entry")
@@ -65,10 +97,9 @@ class IptvDialog:
self._add_button = builder.get_object("iptv_dialog_add_button")
self._save_button = builder.get_object("iptv_dialog_save_button")
self._stream_type_combobox = builder.get_object("stream_type_combobox")
self._action = action
self._profile = profile
self._bouquet = bouquet
self._services = services
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._yt_quality_box = builder.get_object("yt_iptv_quality_combobox")
self._model, self._paths = view.get_selection().get_selected_rows()
# style
self._style_provider = Gtk.CssProvider()
@@ -93,6 +124,7 @@ class IptvDialog:
self._add_button.set_visible(True)
if self._profile is Profile.ENIGMA_2:
self._update_reference_entry()
self._stream_type_combobox.set_active(1)
elif self._action is Action.EDIT:
self._current_srv = get_base_model(self._model)[self._paths][:]
self.init_data(self._current_srv)
@@ -105,9 +137,11 @@ class IptvDialog:
self._dialog.destroy()
def on_save(self, item):
self.on_url_changed(self._url_entry)
if self._action is Action.ADD:
self.on_url_changed(self._url_entry)
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
return
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
@@ -140,7 +174,7 @@ class IptvDialog:
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.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
self._srv_type_entry.set_text(data[2])
self._sid_entry.set_text(str(int(data[3], 16)))
@@ -175,12 +209,55 @@ class IptvDialog:
self._update_reference_entry()
def on_url_changed(self, entry):
url = urlparse(entry.get_text())
url_str = entry.get_text()
url = urlparse(url_str)
entry.set_name("GtkEntry" if all([url.scheme, url.netloc, url.path]) else _DIGIT_ENTRY_NAME)
yt_id = YouTube.get_yt_id(url_str)
if yt_id:
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
text = "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
entry.set_sensitive(False)
gen = self.set_yt_url(entry, yt_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
elif YouTube.is_yt_video_link(url_str):
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
self._yt_quality_box.set_visible(False)
def set_yt_url(self, entry, video_id):
try:
links, title = YouTube.get_yt_link(video_id)
except urllib.error.URLError as e:
self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR)
return
else:
if self._action is Action.ADD:
self._name_entry.set_text(title)
if links:
if len(links) > 1:
self._yt_quality_box.set_visible(True)
entry.set_text(links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]])
self._yt_links = links
else:
msg = get_message("Getting link error:") + " No link received for id: {}".format(video_id)
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
entry.set_sensitive(True)
yield True
def on_stream_type_changed(self, item):
self._update_reference_entry()
def on_yt_quality_changed(self, box):
model = box.get_model()
active = model.get_value(box.get_active_iter(), 0)
if self._yt_links and active in self._yt_links:
self._url_entry.set_text(self._yt_links[active])
def save_enigma2_data(self):
name = self._name_entry.get_text().strip()
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
@@ -219,6 +296,16 @@ class IptvDialog:
self._bouquet.insert(self._model.get_path(itr)[0], fav_id)
self._services[fav_id] = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, *aggr, fav_id, None)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@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)
class SearchUnavailableDialog:
@@ -322,8 +409,8 @@ 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
@@ -447,7 +534,9 @@ class IptvListConfigurationDialog:
new_fav_id = "{}{}{}".format(data, sep, desc)
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)
if srv:
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))
@@ -469,5 +558,210 @@ class IptvListConfigurationDialog:
self.update_reference()
class YtListImportDialog:
def __init__(self, transient, profile, appender):
handlers = {"on_import": self.on_import,
"on_receive": self.on_receive,
"on_yt_url_entry_changed": self.on_url_entry_changed,
"on_yt_info_bar_close": self.on_info_bar_close,
"on_popup_menu": on_popup_menu,
"on_selected_toggled": self.on_selected_toggled,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_key_press": self.on_key_press,
"on_close": self.on_close}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
"yt_popup_menu", "remove_selection_image"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("yt_import_dialog_window")
self._dialog.set_transient_for(transient)
self._list_view_scrolled_window = builder.get_object("yt_list_view_scrolled_window")
self._model = builder.get_object("yt_liststore")
self._progress_bar = builder.get_object("yt_progress_bar")
self._info_bar_box = builder.get_object("yt_info_bar_box")
self._message_label = builder.get_object("yt_info_bar_message_label")
self._info_bar = builder.get_object("yt_info_bar")
self._yt_count_label = builder.get_object("yt_count_label")
self._url_entry = builder.get_object("yt_url_entry")
self._receive_button = builder.get_object("yt_receive_button")
self._import_button = builder.get_object("yt_import_button")
self._quality_box = builder.get_object("yt_quality_combobox")
self._quality_model = builder.get_object("yt_quality_liststore")
self._import_button.bind_property("visible", self._quality_box, "visible")
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.appender = appender
self._profile = profile
self._download_task = False
self._yt_list_id = None
self._yt_list_title = None
def show(self):
self._dialog.show()
@run_task
def on_import(self, item):
self.on_info_bar_close()
self.update_active_elements(False)
self._download_task = True
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
done_links = {}
rows = list(filter(lambda r: r[2], self._model))
futures = {executor.submit(YouTube.get_yt_link, r[1]): r for r in rows}
size = len(futures)
counter = 0
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
return
done_links[futures[future]] = future.result()
counter += 1
self.update_progress_bar(counter / size)
except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
if self._download_task:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
self.append_services([done_links[r] for r in rows])
finally:
self._download_task = False
self.update_active_elements(True)
def on_receive(self, item):
self.show_invisible_elements()
self.update_active_elements(False)
self._model.clear()
self._yt_count_label.set_text("0")
self.on_info_bar_close()
self.update_refs_list()
@run_task
def update_refs_list(self):
if self._yt_list_id:
try:
self._yt_list_title, links = PlayListParser.get_yt_playlist(self._yt_list_id)
except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
return
else:
gen = self.update_links(links)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
finally:
self.update_active_elements(True)
def update_links(self, links):
for l in links:
yield self._model.append((l[0], l[1], True, None))
size = len(self._model)
self._yt_count_label.set_text(str(size))
self._import_button.set_visible(size)
yield True
@run_idle
def append_services(self, links):
aggr = [None] * 9
srvs = []
if self._yt_list_title:
title = self._yt_list_title
fav_id = MARKER_FORMAT.format(0, title, title)
mk = Service(None, None, None, title, *aggr[0:3], BqServiceType.MARKER.name, *aggr, 0, fav_id, None)
srvs.append(mk)
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
for link in links:
lnk, title = link
if not lnk:
continue
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
fav_id = get_fav_id(ln, title, self._profile)
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
srvs.append(srv)
self.appender(srvs)
@run_idle
def update_active_elements(self, sensitive):
self._url_entry.set_sensitive(sensitive)
self._receive_button.set_sensitive(sensitive)
def show_invisible_elements(self):
self._list_view_scrolled_window.set_visible(True)
self._info_bar_box.set_visible(True)
self._dialog.set_resizable(True)
def on_url_entry_changed(self, entry):
url_str = entry.get_text()
yt_id = YouTube.get_yt_list_id(url_str)
entry.set_name("GtkEntry" if yt_id else _DIGIT_ENTRY_NAME)
self._receive_button.set_sensitive(bool(yt_id))
self._import_button.set_sensitive(bool(yt_id))
self._yt_list_id = yt_id
if yt_id:
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def update_progress_bar(self, value):
self._progress_bar.set_visible(value < 1)
self._progress_bar.set_fraction(value)
@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)
def on_selected_toggled(self, toggle, path):
self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active())
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):
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._model.get_iter(path)
selected = self._model.get_value(itr, 2)
self._model.set_value(itr, 2, not selected)
def on_close(self, window, event):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self._download_task = False
if __name__ == "__main__":
pass

View File

@@ -3,23 +3,26 @@ 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.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.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 .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog
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, FavClickMode
@@ -40,37 +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", "bouquet_import_popup_item")
"bouquets_copy_popup_item", "bouquets_paste_popup_item", "new_header_button",
"bouquet_import_popup_item")
_COMMONS_ELEMENTS = ("edit_header_button", "bouquets_remove_popup_item",
"fav_remove_popup_item", "import_bq_menu_button")
_COMMONS_ELEMENTS = ("bouquets_remove_popup_item", "fav_remove_popup_item", "import_bq_menu_button")
_FAV_ENIGMA_ELEMENTS = ("fav_insert_marker_popup_item",)
_FAV_ENIGMA_ELEMENTS = ("fav_insert_marker_popup_item", "fav_epg_configuration_popup_item",
"epg_configuration_header_button")
_FAV_IPTV_ELEMENTS = ("fav_iptv_popup_item",)
_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",
"bouquet_import_popup_item", "import_bq_menu_button")
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,
@@ -107,12 +106,13 @@ 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_yt_list": self.on_import_yt_list,
"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,
@@ -131,12 +131,14 @@ class Application(Gtk.Application):
"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,
"on_player_stop": self.on_player_stop,
"on_player_previous": self.on_player_previous,
"on_player_next": self.on_player_next,
"on_player_rewind": self.on_player_rewind,
"on_player_close": self.on_player_close,
"on_player_press": self.on_player_press,
"on_full_screen": self.on_full_screen,
@@ -177,7 +179,6 @@ class Application(Gtk.Application):
# http api
self._http_api = None
self._fav_click_mode = None
self._monitor_signal = False
# Colors
self._use_colors = False
self._NEW_COLOR = None # Color for new services in the main list
@@ -199,19 +200,17 @@ class Application(Gtk.Application):
self._services_model = builder.get_object("services_list_store")
self._bouquets_model = builder.get_object("bouquets_tree_store")
self._main_data_box = builder.get_object("main_data_box")
self._player_drawing_area = builder.get_object("player_drawing_area")
self._player_box = builder.get_object("player_box")
self._player_tool_bar = builder.get_object("player_tool_bar")
self._player_prev_button = builder.get_object("player_prev_button")
self._player_next_button = builder.get_object("player_next_button")
self._status_bar_box = builder.get_object("status_bar_box")
self._services_main_box = builder.get_object("services_main_box")
self._bouquets_main_box = builder.get_object("bouquets_main_box")
# Enabling events for the drawing area
self._player_drawing_area.set_events(Gdk.ModifierType.BUTTON1_MASK)
self._player_frame = builder.get_object("player_frame")
self._header_bar = builder.get_object("header_bar")
self._bq_name_label = builder.get_object("bq_name_label")
# App info
self._app_info_box = builder.get_object("app_info_box")
self._app_info_box.bind_property("visible", self._status_bar_box, "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("main_paned"), "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("right_header_box"), "sensitive", 4)
self._app_info_box.bind_property("visible", builder.get_object("left_header_box"), "sensitive", 4)
# Status bar
self._ip_label = builder.get_object("ip_label")
self._ip_label.set_text(self._options.get(self._profile).get("host"))
@@ -220,14 +219,14 @@ 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")
self._tv_count_label = builder.get_object("tv_count_label")
self._radio_count_label = builder.get_object("radio_count_label")
self._data_count_label = builder.get_object("data_count_label")
self._save_header_button = builder.get_object("save_header_button")
self._save_header_button.bind_property("sensitive", builder.get_object("save_menu_button"), "sensitive")
# Force ctrl press event for view. Multiple selections in lists only with Space key(as in file managers)!!!
self._services_view.connect("key-press-event", self.force_ctrl)
self._fav_view.connect("key-press-event", self.force_ctrl)
@@ -245,11 +244,34 @@ class Application(Gtk.Application):
self._filter_types_model = builder.get_object("filter_types_list_store")
self._filter_sat_positions_model = builder.get_object("filter_sat_positions_list_store")
self._filter_only_free_button = builder.get_object("filter_only_free_button")
# Player
self._player_box = builder.get_object("player_box")
self._player_scale = builder.get_object("player_scale")
self._player_full_time_label = builder.get_object("player_full_time_label")
self._player_current_time_label = builder.get_object("player_current_time_label")
self._player_rewind_box = builder.get_object("player_rewind_box")
self._player_drawing_area = builder.get_object("player_drawing_area")
self._player_tool_bar = builder.get_object("player_tool_bar")
self._player_prev_button = builder.get_object("player_prev_button")
self._player_next_button = builder.get_object("player_next_button")
self._player_box.bind_property("visible", self._services_main_box, "visible", 4)
self._player_box.bind_property("visible", self._bouquets_main_box, "visible", 4)
self._player_box.bind_property("visible", builder.get_object("close_player_menu_button"), "visible")
self._player_box.bind_property("visible", builder.get_object("left_header_box"), "visible", 4)
self._player_box.bind_property("visible", builder.get_object("right_header_box"), "visible", 4)
self._player_box.bind_property("visible", builder.get_object("main_popover_menu_box"), "visible", 4)
# Enabling events for the drawing area
self._player_drawing_area.set_events(Gdk.ModifierType.BUTTON1_MASK)
self._player_frame = builder.get_object("player_frame")
# Search
self._search_bar = builder.get_object("search_bar")
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)
@@ -270,6 +292,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 = []
@@ -408,7 +440,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
@@ -472,7 +504,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
@@ -743,21 +775,17 @@ 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 """
@@ -767,6 +795,7 @@ class Application(Gtk.Application):
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
@@ -804,7 +833,6 @@ class Application(Gtk.Application):
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))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
@@ -813,35 +841,56 @@ class Application(Gtk.Application):
def open_data(self, data_path=None):
""" Opening data and fill views. """
self._wait_dialog.show()
self.clear_current_data()
GLib.idle_add(self.append_data, data_path, priority=GLib.PRIORITY_LOW)
gen = self.update_data(data_path)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_DEFAULT_IDLE)
def update_data(self, data_path):
self._wait_dialog.show()
yield True
def append_data(self, data_path):
profile = Profile(self._profile)
data_path = self._options.get(self._profile).get("data_dir_path") if data_path is None else data_path
yield from self.clear_current_data()
try:
black_list = get_blacklist(data_path)
bouquets = get_bouquets(data_path, Profile(self._profile))
yield True
services = get_services(data_path, profile, self.get_format_version() if profile is Profile.ENIGMA_2 else 0)
yield True
update_picons_data(self._options.get(self._profile).get("picons_dir_path"), self._picons)
yield True
except FileNotFoundError as e:
self._wait_dialog.hide()
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)
return
except SyntaxError as e:
self._wait_dialog.hide()
self.show_error_dialog(str(e))
return
except Exception as e:
self._wait_dialog.hide()
log("Append services error: " + str(e))
self.show_error_dialog(get_message("Reading data error!") + "\n" + str(e))
return
else:
self.append_blacklist(black_list)
self.append_bouquets(bouquets)
self.append_services(services)
self.update_sat_positions()
yield from self.append_data(bouquets, services)
finally:
self._wait_dialog.hide()
yield True
def append_data(self, bouquets, services):
if self._app_info_box.get_visible():
yield from self.show_app_info(False)
self.append_bouquets(bouquets)
yield from self.append_services(services)
self.update_sat_positions()
yield True
def show_app_info(self, visible):
self._app_info_box.set_visible(visible)
self._app_info_box.grab_focus() if visible else self._services_view.grab_focus()
yield True
def append_blacklist(self, black_list):
if black_list:
@@ -897,10 +946,7 @@ class Application(Gtk.Application):
# 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)
def append_services_data(self, services):
for srv in services:
tooltip, background = None, None
if self._use_colors:
@@ -914,13 +960,22 @@ class Application(Gtk.Application):
itr = self._services_model.append(s)
self._services_model.set_value(itr, Column.SRV_PICON, self._picons.get(srv.picon_id, None))
yield True
self._wait_dialog.hide()
def clear_current_data(self):
""" Clearing current data from lists """
self._bouquets_model.clear()
yield True
self._fav_model.clear()
self._services_model.clear()
yield True
s_model = self._services_view.get_model()
self._services_view.set_model(None)
yield True
for index, itr in enumerate([row.iter for row in self._services_model]):
self._services_model.remove(itr)
if index % 25 == 0:
yield True
yield True
self._services_view.set_model(s_model)
self._blacklist.clear()
self._services.clear()
self._rows_buffer.clear()
@@ -929,8 +984,9 @@ class Application(Gtk.Application):
self._current_bq_name = None
self._bq_name_label.set_text("")
self.init_sat_positions()
self.update_services_counts()
yield True
@run_idle
def on_data_save(self, *args):
if len(self._bouquets_model) == 0:
self.show_error_dialog("No data to save!")
@@ -939,12 +995,18 @@ class Application(Gtk.Application):
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
return
gen = self.save_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def save_data(self):
self._save_header_button.set_sensitive(False)
profile = Profile(self._profile)
options = self._options.get(self._profile)
path = options.get("data_dir_path")
backup_path = options.get("backup_dir_path", path + "backup/")
# Backup data or clearing data path
backup_data(path, backup_path) if options.get("backup_before_save", True) else clear_data_path(path)
yield True
bouquets = []
@@ -968,27 +1030,38 @@ class Application(Gtk.Application):
if len(b_path) == 1:
bouquets.append(Bouquets(*model.get(itr, Column.BQ_NAME, Column.BQ_TYPE), bqs if bqs else []))
profile = Profile(self._profile)
# Getting bouquets
self._bouquets_view.get_model().foreach(parse_bouquets)
write_bouquets(path, bouquets, profile)
yield True
# Getting services
services_model = get_base_model(self._services_view.get_model())
services = [Service(*row[: Column.SRV_TOOLTIP]) for row in services_model]
write_services(path, services, profile, self.get_format_version() if profile is Profile.ENIGMA_2 else 0)
yield True
# removing bouquet files
if profile is Profile.ENIGMA_2:
# blacklist
write_blacklist(path, self._blacklist)
self._save_header_button.set_sensitive(True)
yield True
def on_new_configuration(self, item):
""" Creates new empty configuration """
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
return
self.clear_current_data()
gen = self.create_new_configuration(Profile(self._profile))
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def create_new_configuration(self, profile):
if self._app_info_box.get_visible():
yield from self.show_app_info(False)
c_gen = self.clear_current_data()
yield from c_gen
profile = Profile(self._profile)
if profile is Profile.ENIGMA_2:
parent = self._bouquets_model.append(None, ["Favourites (TV)", None, None, BqType.TV.value])
self.append_bouquet(Bouquet("Favourites (TV)", BqType.TV.value, [], None, None), parent)
@@ -998,6 +1071,7 @@ class Application(Gtk.Application):
self._bouquets_model.append(None, ["Providers", None, None, BqType.BOUQUET.value])
self._bouquets_model.append(None, ["FAV", None, None, BqType.TV.value])
self._bouquets_model.append(None, ["WEBTV", None, None, BqType.WEBTV.value])
yield True
def on_services_selection(self, model, path, column):
self.update_service_bar(model, path)
@@ -1029,6 +1103,8 @@ 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
@@ -1084,21 +1160,25 @@ class Application(Gtk.Application):
for v in [view, *args]:
v.get_selection().unselect_all()
@run_idle
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"))
gen = self.update_options()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
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:
yield from self.show_app_info(True)
self._profile = profile
c_gen = self.clear_current_data()
yield from c_gen
self.update_profile_label()
self.init_colors(True)
yield True
self.init_http_api()
yield True
def on_tree_view_key_press(self, view, event):
""" Handling keystrokes on press """
@@ -1160,8 +1240,6 @@ class Application(Gtk.Application):
self.on_new_bouquet(view)
elif ctrl and key is KeyboardKey.BACK_SPACE and model_name == self._SERVICE_LIST_NAME:
self.on_to_fav_end_copy(view)
elif ctrl and key is KeyboardKey.S:
self.on_data_save()
elif ctrl and key is KeyboardKey.L:
self.on_locked(None)
elif ctrl and key is KeyboardKey.H:
@@ -1186,10 +1264,11 @@ class Application(Gtk.Application):
self.update_fav_num_column(model)
self.update_bouquet_list()
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:
@@ -1202,15 +1281,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)
@@ -1221,9 +1295,18 @@ 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:
is_iptv = self._bq_selected and not is_service
if profile is Profile.NEUTRINO_MP:
is_iptv = is_iptv and BqType(self._bq_selected.split(":")[1]) is BqType.WEBTV
self._tool_elements[elem].set_sensitive(is_iptv)
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)
@@ -1340,8 +1423,31 @@ 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_yt_list(self, item):
""" Import playlist from YouTube """
if not self._bq_selected:
return
YtListImportDialog(self._main_window, Profile(self._profile), self.append_imported_services).show()
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")
@@ -1355,12 +1461,39 @@ class Application(Gtk.Application):
channels = parse_m3u(response, Profile(self._profile))
if channels and self._bq_selected:
bq_services = self._bouquets.get(self._bq_selected)
self._fav_model.clear()
for ch in channels:
self._services[ch.fav_id] = ch
bq_services.append(ch.fav_id)
next(self.update_bouquet_services(self._fav_model, None, self._bq_selected), False)
self.append_imported_services(channels)
def append_imported_services(self, services):
bq_services = self._bouquets.get(self._bq_selected)
self._fav_model.clear()
for srv in services:
self._services[srv.fav_id] = srv
bq_services.append(srv.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)
@@ -1378,9 +1511,18 @@ class Application(Gtk.Application):
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()
def append(b, s):
gen = self.append_imported_data(b, s)
GLib.idle_add(lambda: next(gen, False))
ImportDialog(self._main_window, response, Profile(self._profile), self._services.keys(), append).show()
def append_imported_data(self, bouquets, services):
try:
self._wait_dialog.show()
yield from self.append_data(bouquets, services)
finally:
self._wait_dialog.hide()
# ***************** Backup ********************#
@@ -1407,15 +1549,14 @@ class Application(Gtk.Application):
def play(self, url):
if not self._player:
try:
self._player = Player()
self._player = Player(rewind_callback=self.on_player_duration_changed,
position_callback=self.on_player_time_changed)
except (NameError, AttributeError):
self.show_error_dialog("No VLC is found. Check that it is installed!")
return
else:
if self._drawing_area_xid:
self._player.set_xwindow(self._drawing_area_xid)
self._services_main_box.set_visible(False)
self._bouquets_main_box.set_visible(False)
w, h = self._main_window.get_size()
self._player_box.set_size_request(w * 0.6, -1)
@@ -1441,6 +1582,9 @@ class Application(Gtk.Application):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
self.on_play_stream()
def on_player_rewind(self, scale, scroll_type, value):
self._player.set_time(int(value))
def update_player_buttons(self):
if self._player:
path, column = self._fav_view.get_cursor()
@@ -1453,8 +1597,24 @@ class Application(Gtk.Application):
self._player.release()
self._player = None
GLib.idle_add(self._player_box.set_visible, False, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._services_main_box.set_visible, True, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._bouquets_main_box.set_visible, True, priority=GLib.PRIORITY_LOW)
@lru_cache(maxsize=1)
def on_player_duration_changed(self, duration):
self._player_scale.set_value(0)
self._player_scale.get_adjustment().set_upper(duration)
GLib.idle_add(self._player_rewind_box.set_visible, duration > 0)
GLib.idle_add(self._player_current_time_label.set_text, "0")
GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration))
def on_player_time_changed(self, t):
if not self._full_screen and self._player_rewind_box.get_visible():
GLib.idle_add(self._player_current_time_label.set_text, self.get_time_str(t), priority=GLib.PRIORITY_LOW)
def get_time_str(self, duration):
""" returns a string representation of time from duration in milliseconds """
m, s = divmod(duration // 1000, 60)
h, m = divmod(m, 60)
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
def on_drawing_area_realize(self, widget):
self._drawing_area_xid = widget.get_window().get_xid()
@@ -1488,27 +1648,28 @@ class Application(Gtk.Application):
def on_main_window_state(self, window, event):
full = not event.new_window_state & Gdk.WindowState.FULLSCREEN
self._main_data_box.set_visible(full)
self._status_bar_box.set_visible(full)
self._player_tool_bar.set_visible(full)
self._status_bar_box.set_visible(full and not self._app_info_box.get_visible())
# ************************ HTTP API ****************************#
@run_task
def init_http_api(self):
if self._http_api:
self._http_api.close()
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)
if self._http_api:
self._http_api.close()
self._http_api = None
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)
if not self._http_api:
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)
def on_watch(self):
""" Switch to the channel and watch in the player """
@@ -1537,7 +1698,6 @@ class Application(Gtk.Application):
req = self._http_api.send((HttpRequestType.ZAP, ref))
next(self._http_api)
if req and req.get("result", False):
GLib.timeout_add_seconds(2, self.update_service_info)
GLib.idle_add(scroll_to, path, self._fav_view)
if callback is not None:
callback()
@@ -1564,27 +1724,31 @@ class Application(Gtk.Application):
GLib.idle_add(self._receiver_info_box.set_visible, res_info)
if service_info:
GLib.idle_add(self._service_name_label.set_text, service_info.get("name", ""))
GLib.timeout_add_seconds(2, self.update_signal)
gen = self.update_service_info()
GLib.timeout_add_seconds(3, lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._signal_box.set_visible, service_info)
def update_signal(self):
if not self._http_api:
return
sig = self._http_api.send((HttpRequestType.SIGNAL, None))
next(self._http_api)
val = sig.get("snr", 0)
self._signal_level_bar.set_value(val if val else 0)
self._signal_level_bar.set_visible(val)
return self._monitor_signal
def update_service_info(self):
info = self._http_api.send((HttpRequestType.INFO, None))
next(self._http_api)
if info:
service_info = info.get("service", None)
if service_info:
GLib.idle_add(self._service_name_label.set_text, service_info.get("name", ""))
GLib.timeout_add_seconds(1, self.update_signal)
while self._http_api:
info = self._http_api.send((HttpRequestType.INFO, None))
next(self._http_api)
if info:
service_info = info.get("service", None)
if service_info:
GLib.idle_add(self._service_name_label.set_text, service_info.get("name", ""))
if service_info.get("onid", None):
self.update_signal()
yield self._http_api
# ***************** Filter and search *********************#
@@ -1650,10 +1814,10 @@ class Application(Gtk.Application):
if self._services_model_filter is None or self._services_model_filter == "None":
return True
else:
txt = self._filter_entry.get_text() in str(model.get(itr, Column.SRV_SERVICE, Column.SRV_PACKAGE,
Column.SRV_TYPE, Column.SRV_SSID, Column.SRV_FREQ,
Column.SRV_RATE, Column.SRV_POL, Column.SRV_FEC,
Column.SRV_SYSTEM, Column.SRV_POS))
r_txt = str(model.get(itr, Column.SRV_SERVICE, Column.SRV_PACKAGE, Column.SRV_TYPE, Column.SRV_SSID,
Column.SRV_FREQ, Column.SRV_RATE, Column.SRV_POL, Column.SRV_FEC, Column.SRV_SYSTEM,
Column.SRV_POS)).upper()
txt = self._filter_entry.get_text().upper() in r_txt
type_active = self._filter_types_box.get_active() > 0
pos_active = self._filter_sat_positions_box.get_active() > 0
free = not model.get(itr, Column.SRV_CODED)[0] if self._filter_only_free_button.get_active() else True
@@ -1739,6 +1903,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)

View File

@@ -16,7 +16,7 @@ from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
# ***************** Markers *******************#
def insert_marker(view, bouquets, selected_bouquet, channels, parent_window):
def insert_marker(view, bouquets, selected_bouquet, services, parent_window):
"""" Inserts marker into bouquet services list. """
response = show_dialog(DialogType.INPUT, parent_window)
if response == Gtk.ResponseType.CANCEL:
@@ -26,17 +26,13 @@ def insert_marker(view, bouquets, selected_bouquet, channels, parent_window):
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
# Searching for max num value in all marker services (if empty default = 0)
max_num = max(map(lambda num: int(num.data_id, 16),
filter(lambda ch: ch.service_type == BqServiceType.MARKER.name, channels.values())), default=0)
max_num = '{:X}'.format(max_num + 1)
fav_id = "1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(max_num, response, response)
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
s_type = BqServiceType.MARKER.name
model, paths = view.get_selection().get_selected_rows()
marker = (None, None, response, None, None, s_type, None, fav_id, None, None, None)
itr = model.insert_before(model.get_iter(paths[0]), marker) if paths else model.insert(0, marker)
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
channels[fav_id] = Service(None, None, None, response, None, None, None, s_type, *[None] * 9, max_num, fav_id, None)
services[fav_id] = Service(None, None, None, response, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
# ***************** Movement *******************#

File diff suppressed because it is too large Load Diff

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,28 +31,8 @@ Author: Dmitriy Yefremov
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-bottom</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-server-symbolic</property>
</object>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-top</property>
</object>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-execute</property>
</object>
<object class="GtkListStore" id="providers_list_store">
<columns>
<!-- column-name logo -->
@@ -119,157 +99,76 @@ Author: Dmitriy Yefremov
<property name="destroy_with_parent">True</property>
<property name="icon_name">emblem-photos</property>
<property name="type_hint">dialog</property>
<signal name="destroy" handler="on_close" swapped="no"/>
<signal name="delete-event" handler="on_close" swapped="no"/>
<child type="titlebar">
<object class="GtkHeaderBar" id="header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Picons download tool</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child type="title">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Cancel</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_cancel" swapped="no"/>
<child>
<object class="GtkLabel">
<object class="GtkImage" id="cancel_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="label" translatable="yes">Picons download tool</property>
<property name="xalign">0.52999997138977051</property>
<property name="stock">gtk-cancel</property>
</object>
</child>
<accelerator key="z" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkBox" id="header_download_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="load_providers_button">
<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">Load providers</property>
<property name="halign">center</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_load_providers" swapped="no"/>
<child>
<object class="GtkImage" id="load_providers_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-server-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkButton" id="receive_button">
<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">2</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Receive picons</property>
<property name="halign">center</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
<child>
<object class="GtkButton">
<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="tooltip_text" translatable="yes">Cancel</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_cancel" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment">
<object class="GtkImage" id="receive_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<property name="stock">gtk-goto-bottom</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="receive_button">
<property name="label" translatable="yes">Receive picons</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Receive picons for providers</property>
<property name="halign">center</property>
<property name="image">image1</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="convert_button">
<property name="label" translatable="yes">Convert</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="image">image4</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="load_providers_button">
<property name="label" translatable="yes">Load providers</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Load satellite providers.</property>
<property name="halign">center</property>
<property name="image">image2</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_load_providers" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="label" translatable="yes">Send</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
<property name="image">image3</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_send" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
</object>
<packing>
@@ -278,7 +177,64 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_send" swapped="no"/>
<child>
<object class="GtkImage" id="send_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-top</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkButton" id="convert_button">
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Convert</property>
<property name="halign">center</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_convert" swapped="no"/>
<child>
<object class="GtkImage" id="convert_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-execute</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
@@ -286,15 +242,11 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="picons_dialog_vbox">
<property name="width_request">640</property>
<property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog_action_area">
<property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="homogeneous">True</property>
<property name="layout_style">spread</property>
</object>
@@ -304,17 +256,6 @@ Author: Dmitriy Yefremov
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="separator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkNotebook" id="notebook">
<property name="visible">True</property>
@@ -326,8 +267,8 @@ 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">2</property>
<property name="margin_bottom">2</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
@@ -404,10 +345,47 @@ Author: Dmitriy Yefremov
</object>
</child>
<child type="label">
<object class="GtkLabel">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Satellite</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="satellite_label">
<property name="can_focus">False</property>
<property name="label" translatable="yes">Satellite</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="loading_data_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="label" translatable="yes">Loading data...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="loading_data_spinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">2</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
@@ -658,7 +636,7 @@ Author: Dmitriy Yefremov
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="secondary_icon_name">folder-open-symbolic</property>
<property name="secondary_icon_name">folder-open</property>
<property name="primary_icon_activatable">False</property>
<signal name="icon-press" handler="on_picons_dir_open" swapped="no"/>
</object>
@@ -988,7 +966,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">8</property>
<property name="position">0</property>
</packing>
</child>
<child>
@@ -1025,7 +1003,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">13</property>
<property name="position">1</property>
</packing>
</child>
<child>
@@ -1079,7 +1057,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">14</property>
<property name="position">2</property>
</packing>
</child>
</object>

View File

@@ -3,7 +3,6 @@ import re
import shutil
import subprocess
import tempfile
import time
from gi.repository import GLib, GdkPixbuf
@@ -23,8 +22,8 @@ class PiconsDialog:
self._sat_positions = sat_positions
self._TMP_DIR = tempfile.gettempdir() + "/"
self._BASE_URL = "www.lyngsat.com/packages/"
self._PATTERN = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html$")
self._POS_PATTERN = re.compile("^\d+\.\d+[EW]?$")
self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
self._current_process = None
self._terminate = False
@@ -71,11 +70,16 @@ class PiconsDialog:
self._enigma2_path_button = builder.get_object("enigma2_path_button")
self._save_to_button = builder.get_object("save_to_button")
self._send_button = builder.get_object("send_button")
self._cancel_button = builder.get_object("cancel_button")
self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
self._satellite_label = builder.get_object("satellite_label")
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
self._cancel_button.bind_property("visible", builder.get_object("header_download_box"), "visible", 4)
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -88,16 +92,17 @@ class PiconsDialog:
self._picons_path = self._properties.get("picons_dir_path", "")
self._picons_dir_entry.set_text(self._picons_path)
self._enigma2_picons_path = self._picons_path
if profile is Profile.NEUTRINO_MP:
self._enigma2_picons_path = options.get(Profile.ENIGMA_2.value).get("picons_dir_path", "")
if not len(self._picon_ids) and self._profile is Profile.ENIGMA_2:
message = get_message("To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window.")
self.show_info_message(message, Gtk.MessageType.WARNING)
self._satellite_label.show()
def show(self):
self._dialog.run()
self._dialog.destroy()
def on_satellites_view_realize(self, view):
self.get_satellites(view)
@@ -105,18 +110,22 @@ class PiconsDialog:
@run_task
def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not sats:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
gen = self.append_satellites(view.get_model(), sats)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def append_satellites(self, model, sats):
for sat in sats:
pos = sat[1]
name = "{} ({})".format(sat[0], pos)
pos = "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
if not self._terminate and model:
if pos in self._sat_positions:
model.append((name, sat[3], pos))
yield True
try:
for sat in sats:
pos = sat[1]
name, pos = "{} ({})".format(sat[0], pos), "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
if not self._terminate and model:
if pos in self._sat_positions:
yield model.append((name, sat[3], pos))
finally:
self._satellite_label.show()
def on_satellite_selection(self, view, path, column):
model = view.get_model()
@@ -125,7 +134,10 @@ class PiconsDialog:
@run_idle
def on_load_providers(self, item):
self._expander.set_expanded(True)
self.on_info_bar_close()
self._cancel_button.show()
url = self._url_entry.get_text()
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -133,24 +145,33 @@ class PiconsDialog:
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
model = self._providers_tree_view.get_model()
model.clear()
self.update_receive_button_state()
self.append_providers(url, model)
@run_task
def append_providers(self, url, model):
self._current_process.wait()
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
if providers:
for p in providers:
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
self.update_receive_button_state()
try:
self._terminate = False
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
except FileNotFoundError:
pass # NOP
else:
if providers:
for p in providers:
if self._terminate:
return
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
self.update_receive_button_state()
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def get_pixbuf(self, img_url):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
width=48, height=48, preserve_aspect_ratio=True)
@run_idle
def on_receive(self, item):
self._cancel_button.show()
self.start_download()
@run_task
@@ -170,13 +191,19 @@ class PiconsDialog:
scroll_to(prv.path, self._providers_tree_view)
return
for prv in providers:
if self._terminate:
break
self.process_provider(Provider(*prv))
self.resize(self._picons_path)
if not self._terminate:
try:
for prv in providers:
if self._terminate:
return
self.process_provider(Provider(*prv))
if self._resize_no_radio_button.get_active():
self.resize(self._picons_path)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def process_provider(self, prv):
url = prv.url
@@ -195,17 +222,13 @@ class PiconsDialog:
char = fd.read(1)
self.append_output(char)
return True
else:
return False
return False
@run_idle
def append_output(self, char):
append_text_to_tview(char, self._text_view)
def resize(self, path):
if self._resize_no_radio_button.get_active():
return
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
command = "mogrify -resize {}! *.png".format(
"320x240" if self._resize_220_132_radio_button.get_active() else "100x60").split()
@@ -214,18 +237,30 @@ class PiconsDialog:
self._current_process.wait()
except FileNotFoundError as e:
self.show_info_message("Conversion error. " + str(e), Gtk.MessageType.ERROR)
self.on_cancel()
def on_cancel(self, item=None):
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self.terminate_task()
@run_task
def on_cancel(self, item=None):
if self._current_process:
self._terminate = True
self._current_process.terminate()
time.sleep(1)
def terminate_task(self):
self._terminate = True
@run_idle
def on_close(self, item):
self.on_cancel(item)
if self._current_process:
self._current_process.terminate()
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
def on_close(self, window, event):
if self.on_cancel():
return True
self.clean_data()
GLib.idle_add(self._dialog.destroy)
@run_task
def clean_data(self):
path = self._TMP_DIR + "www.lyngsat.com"
if os.path.exists(path):
shutil.rmtree(path)
@@ -239,15 +274,17 @@ class PiconsDialog:
@run_task
def upload_picons(self):
if self._current_process is not None and self._current_process.poll() is None:
if self.is_task_running():
self.show_dialog("The task is already running!", DialogType.ERROR)
return
try:
GLib.idle_add(self._expander.set_expanded, True)
upload_data(properties=self._properties,
download_type=DownloadType.PICONS,
profile=self._profile,
callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
@@ -276,7 +313,8 @@ class PiconsDialog:
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
self.update_receive_button_state()
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
@@ -317,7 +355,10 @@ class PiconsDialog:
@run_idle
def update_receive_button_state(self):
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
try:
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
except TypeError:
pass # NOP
def get_selected_providers(self):
""" returns selected providers """
@@ -335,6 +376,9 @@ class PiconsDialog:
return picon_format
def is_task_running(self):
return self._current_process and self._current_process.poll() is None
if __name__ == "__main__":
pass

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>
@@ -547,6 +541,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="satellite_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">Satellite</property>
<property name="expand">True</property>
<child>
@@ -560,6 +555,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="freq_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">Freq</property>
<property name="expand">True</property>
<child>
@@ -573,6 +569,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="rate_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">Rate</property>
<property name="expand">True</property>
<child>
@@ -586,6 +583,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="pol_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">Pol</property>
<property name="expand">True</property>
<child>
@@ -599,6 +597,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="fec_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">FEC</property>
<property name="expand">True</property>
<child>
@@ -612,6 +611,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="sys_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">System</property>
<property name="expand">True</property>
<child>
@@ -625,6 +625,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="mod_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="title" translatable="yes">Mod</property>
<property name="expand">True</property>
<child>
@@ -723,7 +724,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 +734,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 +782,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 +904,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 +917,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,15 +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()
@@ -42,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:
@@ -60,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()
@@ -84,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()
@@ -133,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 """
@@ -316,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")
@@ -400,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

@@ -67,7 +67,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Ok</property>
<property name="margin_top">10</property>
<property name="valign">center</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="ok_button_image">
@@ -84,7 +84,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save</property>
<property name="margin_top">10</property>
<property name="valign">center</property>
<signal name="clicked" handler="apply_settings" swapped="no"/>
<child>
<object class="GtkImage" id="apply_button_image">
@@ -191,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">2</property>
<property name="margin_top">10</property>
<property name="valign">center</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_reset" swapped="no"/>
<child>
@@ -550,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>
@@ -574,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>

View File

@@ -3,7 +3,6 @@ from enum import Enum
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, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, NEW_COLOR, EXTRA_COLOR, FavClickMode
from .main_helper import update_entry_data

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

View File

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

View File

@@ -1,6 +1,5 @@
demon-editor for Debian
----------------------
DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
@@ -8,8 +7,7 @@ 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.
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,7 +17,7 @@ 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.
@@ -29,17 +27,22 @@ Keyboard shortcuts:
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.
Terrestrial(DVB-T/T2) and cable channels are supported(Enigma2 only) with limitation!
Note.
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

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

View File

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

View File

@@ -74,9 +74,6 @@ msgstr "Descargar"
msgid "Edit"
msgstr "Editar"
msgid "Edit "
msgstr "Editar "
msgid "Edit mаrker text"
msgstr "Editar texto del mаrcador"
@@ -694,3 +691,90 @@ 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!"
msgid "Use HTTP"
msgstr "Utilizar HTTP"
msgid "Close playback"
msgstr "Terminar reproducción"
msgid "Import YouTube playlist"
msgstr "Importar lista de reproducción de YouTube"
msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
msgstr "Encontré un enlace al recurso de YouTube!\nIntentar obtener un enlace directo al video?"
msgid "Playlist import"
msgstr "Importar lista de reproducción"
msgid "Getting link error:"
msgstr "Recibido Link error:"

View File

@@ -74,9 +74,6 @@ msgstr "Download"
msgid "Edit"
msgstr "Wijzig"
msgid "Edit "
msgstr "Wijzig "
msgid "Edit mаrker text"
msgstr "Wijzig mаrker tekst"
@@ -693,3 +690,90 @@ 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!"
msgid "Use HTTP"
msgstr "Gebruik HTTP"
msgid "Close playback"
msgstr "Stop afspelen"
msgid "Import YouTube playlist"
msgstr "Importeer YouTube playlist"
msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
msgstr "Een link gevonden naar de YouTube bron!\nProberen een rechtstreekse link naar de video te vonden?"
msgid "Playlist import"
msgstr "Importeer playlist"
msgid "Getting link error:"
msgstr "Volgende Link error gekregen:"

View File

@@ -69,9 +69,6 @@ msgstr "Descarregar"
msgid "Edit"
msgstr "Editar"
msgid "Edit "
msgstr "Editar "
msgid "Edit mаrker text"
msgstr "Editar texto do mаrcador"
@@ -679,4 +676,91 @@ 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)"
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ê!"
msgid "Use HTTP"
msgstr "Use HTTP"
msgid "Close playback"
msgstr "Fechar reprodução"
msgid "Import YouTube playlist"
msgstr "Importar YouTube playlist"
msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
msgstr "Encontrou um link para o recurso do YouTube!\nTentar obter um link direto para o vídeo?"
msgid "Playlist import"
msgstr "Importação de lista de reprodução"
msgid "Getting link error:"
msgstr "Obtendo erro de link:"

View File

@@ -68,9 +68,6 @@ msgstr "Загрузить"
msgid "Edit"
msgstr "Изменить"
msgid "Edit "
msgstr "Изменить"
msgid "Edit mаrker text"
msgstr "Изменить текст маркера"
@@ -680,25 +677,89 @@ msgstr "Переклють канал и просмотр в программе(
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 не содержит ссылок на сервисы данного букета!"
msgid "Use HTTP"
msgstr "Использовать HTTP"
msgid "Close playback"
msgstr "Закрыть воспроизведение"
msgid "Import YouTube playlist"
msgstr "Импорт плейлиста YouTube"
msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
msgstr "Найдена ссылка на ресурс YouTube!\nПопробовать получить прямую ссылку на видео?"
msgid "Playlist import"
msgstr "Импорт плейлиста"
msgid "Getting link error:"
msgstr "Ошибка получения ссылки:"