Compare commits

..

57 Commits

Author SHA1 Message Date
DYefremov
9a8b1e871d bump version 2022-02-05 15:20:28 +03:00
DYefremov
3a53a95f86 fixed current channel recording on Windows 2022-02-05 15:11:29 +03:00
DYefremov
24a94cfe9a improved MPV support 2022-02-05 13:50:59 +03:00
DYefremov
0ca08e3a1d fixed bouquets DND for macOS (#64) 2022-02-04 02:12:48 +03:00
DYefremov
db4e9d2696 fixed single bouquets import on macOS (#63) 2022-02-04 01:16:37 +03:00
DYefremov
3be9b374c8 bump version 2022-01-29 18:43:36 +03:00
DYefremov
0b9fd37ee9 minor revision and improvement of the FTP client 2022-01-28 16:31:12 +03:00
DYefremov
be90d9694a fixed removal of unused picons (#62) 2022-01-28 00:37:02 +03:00
DYefremov
a5144e8e34 fixed multiple selection with the Shift key (#61) 2022-01-27 22:46:34 +03:00
DYefremov
04e0a25956 minor fixes for picons 2022-01-26 23:07:41 +03:00
DYefremov
dc24c899af version update 2022-01-25 18:16:00 +03:00
DYefremov
a55495fd7c reworked built-in function for getting yt links 2022-01-25 18:07:34 +03:00
DYefremov
ed3aea42f5 minor corrections for EPG dialog 2022-01-24 19:17:11 +03:00
DYefremov
a6904360f9 improvement of EPG refs mapping (#59) 2022-01-24 16:39:59 +03:00
DYefremov
1e42d693cc added info panel auto closing for playback (#58) 2022-01-23 14:52:34 +03:00
DYefremov
5e64605be6 minor changes in saving the EPG link mapping list (#59) 2022-01-23 14:24:50 +03:00
DYefremov
63c55ea2ed Russian, Belarusian and German translations update 2022-01-19 22:32:47 +03:00
DYefremov
c7f85b027d bump version 2022-01-05 15:10:10 +03:00
DYefremov
b24910a9a5 fixed command to open file for FTP 2022-01-05 14:57:39 +03:00
DYefremov
1bdb4f123f minor data cleaning optimization 2022-01-05 12:16:16 +03:00
DYefremov
8f8d7633b8 added display picons option 2022-01-05 00:41:06 +03:00
DYefremov
c997724300 refactoring of picon assignment 2022-01-04 20:39:59 +03:00
DYefremov
727a3fa8a2 data rendering optimization 2022-01-03 00:08:31 +03:00
DYefremov
4009f5c2a2 multi-stream support for services web import 2022-01-01 23:09:31 +03:00
DYefremov
ca5d648032 multi-stream transponders support for LyngSat 2021-12-30 15:54:09 +03:00
DYefremov
938fe297c5 corrected satellite update from KingOfSat 2021-12-30 00:02:32 +03:00
DYefremov
db29b78fd7 basic editing support for FTP client 2021-12-29 00:21:11 +03:00
DYefremov
e599ea04c7 improved bouquet export to *.m3u (#56) 2021-12-28 15:17:39 +03:00
DYefremov
50a2f66fc3 bump version 2021-12-24 11:42:33 +03:00
DYefremov
34056b1006 minor rework of recordings tab 2021-12-24 00:15:17 +03:00
DYefremov
6188fecda9 reduced waiting time for web import 2021-12-23 22:07:06 +03:00
DYefremov
eb41b9629e minor revision of satellite update dialog 2021-12-22 14:19:10 +03:00
DYefremov
7694754919 fixed satellites web update from FlySat (#55) 2021-12-21 22:08:54 +03:00
DYefremov
11f240a81f minor rework of ftp client 2021-12-19 20:37:23 +03:00
DYefremov
1b75034317 basic alternate layout support 2021-12-19 13:15:58 +03:00
DYefremov
5b6900cae7 tooltips support for the bouquets list 2021-12-11 16:48:13 +03:00
DYefremov
eeeca881e8 added default id value for iptv service 2021-12-10 12:09:27 +03:00
DYefremov
ed16fb0195 minor correction for settings dialog 2021-12-07 15:05:58 +03:00
DYefremov
833b386356 win style correction 2021-12-07 14:54:12 +03:00
DYefremov
83424124d3 fix saving sub-bouquets 2021-12-07 14:39:31 +03:00
DYefremov
d77aa68a39 Russian, Belarusian and German translations update 2021-12-04 14:53:47 +03:00
DYefremov
94266e13b8 bump version 2021-12-03 21:11:00 +03:00
DYefremov
8db6fb1b0b improved picons filtering and assignment (#49) 2021-12-03 20:05:46 +03:00
DYefremov
e07c5d4bf7 minor code cleanup 2021-12-02 17:28:25 +03:00
DYefremov
6f3090a7e1 sub-bouquets creation support 2021-12-02 15:39:33 +03:00
DYefremov
b73c1d1118 sub bouquets saving support 2021-12-01 11:37:23 +03:00
DYefremov
df127c05f3 services marking not presented in bouquets 2021-11-28 23:41:16 +03:00
DYefremov
9b579af528 added filter option "not in bouquets" 2021-11-28 18:55:37 +03:00
DYefremov
8290e723c9 state saving for the main paned widgets 2021-11-27 10:20:10 +03:00
DYefremov
b12c29be84 minor correction for tid and nid values 2021-11-24 23:54:16 +03:00
DYefremov
1780fbadbd fixed signal update for control tab 2021-11-22 19:41:30 +03:00
DYefremov
4fe4e92442 fixed some folders display for the recording tab 2021-11-22 16:33:24 +03:00
DYefremov
bfad5cf9ac bump version 2021-11-22 14:19:52 +03:00
DYefremov
5d285f61c0 fixed a typo in the Italian translation 2021-11-18 19:52:40 +03:00
DYefremov
f61d9a1f61 fix tid and nid order for KingOfSat web source 2021-11-18 11:31:01 +03:00
DYefremov
8d115677d1 minor fix for picon downloader 2021-11-17 22:42:09 +03:00
DYefremov
f7c6cd6908 preventing gen bouquets for an empty config 2021-11-17 13:05:15 +03:00
50 changed files with 3165 additions and 1531 deletions

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import re
import socket
@@ -181,7 +209,7 @@ class UtfFTP(FTP):
self.send_file(file_name, src, callback)
def remove_unused_bouquets(self, callback):
bq_files = ("userbouquet.", "bouquets.xml", "ubouquets.xml")
bq_files = ("userbouquet.", "subbouquet.", "bouquets.xml", "ubouquets.xml")
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
self.delete_file(file, callback)

View File

@@ -62,7 +62,7 @@ def get_bouquets(path, s_type):
def write_bouquet(path, bq, s_type):
if s_type is SettingsType.ENIGMA_2:
writer = BouquetsWriter(path, None)
writer.write_bouquet(path + "userbouquet.{}.{}".format(bq.name, bq.type), bq.name, bq.services)
writer.write_bouquet(f"{path}userbouquet.{bq.name}.{bq.type}", bq.name, bq.services)
elif s_type is SettingsType.NEUTRINO_MP:
from .neutrino.bouquets import write_bouquet
write_bouquet(path, bq)

View File

@@ -47,6 +47,10 @@ class BqServiceType(Enum):
ALT = "ALT" # Service with alternatives
BOUQUET = "BOUQUET" # Sub bouquet.
@classmethod
def _missing_(cls, value):
return cls.DEFAULT
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden", "file"])
Bouquet.__new__.__defaults__ = (None, BqServiceType.DEFAULT, [], None, None, None) # For Python3 < 3.7

View File

@@ -60,14 +60,14 @@ class BouquetsWriter:
self._marker_index = 1
self._space_index = 0
self._alt_names = set()
self._NAME_PATTERN = re.compile("[^\\w_()]+")
def write(self):
line = []
pattern = re.compile("[^\\w_()]+")
for bqs in self._bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
line.append(f"#NAME {bqs.name}\n")
bq_file_names = {b.file for b in bqs.bouquets}
count = 1
m_count = 0
@@ -76,24 +76,29 @@ class BouquetsWriter:
bq_name = bq.file
if not bq_name:
if self._force_bq_names:
bq_name = re.sub(pattern, "_", bq.name)
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
else:
bq_name = "de{0:02d}".format(count)
bq_name = f"de{count:02d}"
while bq_name in bq_file_names:
count += 1
bq_name = "de{0:02d}".format(count)
bq_name = f"de{count:02d}"
bq_file_names.add(bq_name)
if BqType(bq.type) is BqType.MARKER:
bq_type = BqType(bq.type)
if bq_type is BqType.MARKER:
m_data = bq.file.split(":") if bq.file else None
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
line.append(self._MARKER.format(m_count, b_name))
m_count += 1
else:
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bq.type}", bq.name, bq.services)
if bq_type is BqType.BOUQUET:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
else:
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(self, path, name, services):
@@ -134,6 +139,18 @@ class BouquetsWriter:
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def write_sub_bouquet(self, path, file_name, bq, bq_type):
bouquet = [f"#NAME {bq.name}\n"]
sb_type = 2 if bq_type == BqType.RADIO.value else 1
for sb in bq.services:
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8") as file:
file.writelines(bouquet)
class ServiceType(Enum):
SERVICE = "0"
@@ -199,7 +216,7 @@ class BouquetsReader:
else:
s_data = line.split(":")
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
b_name = "{}{}".format(_MARKER_PREFIX, s_data[-1].strip())
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
else:
log(f"Unsupported or invalid data format: [{line}].")
@@ -211,14 +228,14 @@ class BouquetsReader:
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
""" Parsing services ids from bouquet file. """
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file:
with open(f"{path}{prefix}.{bq_name}.{bq_type}", encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
log(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
return f"{bq_name} [empty]", services
bq_name = srvs.pop(0)
@@ -226,7 +243,7 @@ class BouquetsReader:
srv_data = srv.strip().split(":")
data_len = len(srv_data)
if data_len < 10:
log("The bouquet [{}] service [{}] has the wrong data format: [{}]".format(bq_name, num, srv))
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
continue
s_type = ServiceType(srv_data[1])
@@ -254,7 +271,7 @@ class BouquetsReader:
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
name = None
if data_len == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")

View File

@@ -52,7 +52,7 @@ class LameDbReader:
""" Lamedb parser class.
Reads and parses the Enigma2 lamedb[5] file.
Supports versions 3, 4 and 5..
Supports versions 3, 4 and 5.
"""
__slots__ = ["_path", "_fmt"]
@@ -99,7 +99,7 @@ class LameDbReader:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
log(f"lamedb parse error: {e}")
else:
return self.get_services_list(data)
@@ -115,7 +115,7 @@ class LameDbReader:
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
srv_data[1] = srv_data[1].strip("\"\n")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
@@ -129,7 +129,7 @@ class LameDbReader:
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
trs[tr] = srv
else:
log("Error while parsing transponder data [ver. 5] for line: {}".format(line))
log(f"Error while parsing transponder data [ver. 5] for line: {line}")
return self.parse_services(srvs, trs)
@@ -164,9 +164,9 @@ class LameDbReader:
srv_type = int(data[4])
transponder_id = f"{data[1]}:{tid}:{nid}"
transponder = transponders.get(transponder_id, None)
tid = tid.lstrip(sp).upper()
nid = nid.lstrip(sp).upper()
# The tid and nid values can be 0.
tid = tid.lstrip(sp).upper() or "0"
nid = nid.lstrip(sp).upper() or "0"
ssid = str(data[0]).lstrip(sp).upper()
onid = str(data[1]).lstrip(sp).upper()
# For comparison in bouquets. Needed in upper case!!!
@@ -188,7 +188,7 @@ class LameDbReader:
tr_type = TrType(tr_type)
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
# Removing all non printable symbols!
# Removing all non-printable symbols!
srv_name = "".join(c for c in srv[1] if c.isprintable())
freq = tr[0]
rate = tr[1]
@@ -258,18 +258,17 @@ class LameDbReader:
tr_set = set()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
if tr_id not in tr_set:
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
tr_lines.append(transponder)
tr_lines.append(f"{tr_id}\n\t{srv.transponder}\n/\n")
tr_set.add(tr_id)
# Services
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
services_lines.append(f"{srv.data_id}\n{srv.service}\n{srv.flags_cas}\n")
tr_lines.sort()
lines.extend(tr_lines)
lines.extend(services_lines)
lines.append("end\n" + _END_LINE)
lines.append(f"end\n{_END_LINE}")
return lines
@@ -324,13 +323,13 @@ class LameDbWriter:
for srv in self._services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
tr_set.add(f"t:{tr_id},{srv.transponder.replace(' ', ':', 1)}\n")
# Removing empty packages
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
flags = ",".join(flags)
flags = "," + flags if flags else ""
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
services_lines.append(f"s:{srv.data_id},\"{srv.service}\"{flags}\n")
lines.extend(sorted(tr_set))
lines.extend(services_lines)

View File

@@ -112,12 +112,12 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
services.append(srv)
else:
log("*.m3u* parse error ['{}']: name[{}], url[{}], fav id[{}]".format(path, name, url, fav_id))
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
return services
def export_to_m3u(path, bouquet, s_type):
def export_to_m3u(path, bouquet, s_type, url=None):
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
@@ -128,15 +128,17 @@ def export_to_m3u(path, bouquet, s_type):
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(unquote(data.strip())))
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{unquote(res.group(1).strip())}\n")
elif s_type is BqServiceType.MARKER:
current_grp = "#EXTGRP:{}\n".format(s.name)
current_grp = f"#EXTGRP:{s.name}\n"
elif s_type is BqServiceType.DEFAULT and url:
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{url}{s.data}\n")
with open(path + "{}.m3u".format(bouquet.name), "w", encoding="utf-8") as file:
with open(f"{path}{bouquet.name}.m3u", "w", encoding="utf-8") as file:
file.writelines(lines)

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -676,6 +676,14 @@ class Settings:
def dark_mode(self, value):
self._settings["dark_mode"] = value
@property
def display_picons(self):
return self._settings.get("display_picons", True)
@display_picons.setter
def display_picons(self, value):
self._settings["display_picons"] = value
@property
def alternate_layout(self):
return self._settings.get("alternate_layout", IS_DARWIN)
@@ -711,7 +719,7 @@ class Settings:
@property
@lru_cache(1)
def themes_path(self):
return "{}{}.themes{}".format(HOME_PATH, SEP, SEP)
return f"{HOME_PATH}{SEP}.themes{SEP}"
@property
def icon_theme(self):
@@ -724,7 +732,7 @@ class Settings:
@property
@lru_cache(1)
def icon_themes_path(self):
return "{}{}.icons{}".format(HOME_PATH, SEP, SEP)
return f"{HOME_PATH}{SEP}.icons{SEP}"
@property
def is_darwin(self):

View File

@@ -1,4 +1,32 @@
""" Module for working with epg.dat file """
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with epg.dat file. """
import struct
from datetime import datetime
from xml.dom.minidom import parse, Node, Document
@@ -11,7 +39,7 @@ 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
https://github.com/OpenPLi/enigma2/blob/develop/lib/dvb/epgcache.cpp#L955
"""
refs = set()
@@ -21,17 +49,23 @@ class EPG:
raise ValueError("Epg file has incorrect byte order!")
header = f.read(13).decode()
if header != "ENIGMA_EPG_V7":
if header == "ENIGMA_EPG_V7":
epg_ver = 7
elif header == "ENIGMA_EPG_V8":
epg_ver = 8
else:
raise ValueError("Unsupported format of epd.dat file!")
channels_count = struct.unpack("<I", f.read(4))[0]
_len_read_size = 3 if epg_ver == 8 else 2
_type_read_str = f"<{'H' if epg_ver == 8 else 'B'}B"
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)
service_id = f"{sid:X}:{tsid:X}:{nid:X}"
for j in range(events_size):
_type, _len = struct.unpack("<BB", f.read(2))
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
f.read(10)
n_crc = (_len - 10) // 4
if n_crc > 0:
@@ -90,14 +124,14 @@ class ChannelsParser:
srv_type = srv.type
if srv_type is BqServiceType.IPTV:
channel_child = doc.createElement("channel")
channel_child.setAttribute("id", str(srv.num))
channel_child.setAttribute("id", srv.name)
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())))
lines.append(f"{channel_child.toxml()} {comment.toxml()}\n")
elif srv_type is BqServiceType.MARKER:
comment = doc.createComment(srv.name)
lines.append("{}\n".format(str(comment.toxml())))
lines.append(f"{comment.toxml()}\n")
lines.append("</channels>")
doc.unlink()

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,6 +33,7 @@ from datetime import datetime
from gi.repository import Gdk, Gtk, GObject
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
class Player(Gtk.DrawingArea):
@@ -115,22 +116,21 @@ class Player(Gtk.DrawingArea):
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
if sys.platform == "linux":
if IS_LINUX:
return self.get_window().get_xid()
else:
is_darwin = sys.platform == "darwin"
try:
import ctypes
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if is_darwin else "libgdk-3-0.dll")
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if IS_DARWIN else "libgdk-3-0.dll")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
else:
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None)
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
get_pointer = libgdk.gdk_quartz_window_get_nsview if IS_DARWIN else libgdk.gdk_win32_window_get_handle
get_pointer.restype = ctypes.c_void_p
get_pointer.argtypes = [ctypes.c_void_p]
@@ -171,7 +171,7 @@ class Player(Gtk.DrawingArea):
elif name == "vlc":
return VlcPlayer.get_instance(mode, widget)
else:
raise NameError("There is no such [{}] implementation.".format(name))
raise NameError(f"There is no such [{name}] implementation.")
class MpvPlayer(Player):
@@ -191,7 +191,7 @@ class MpvPlayer(Player):
input_cursor=False,
cursor_autohide="no")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No libmpv is found. Check that it is installed!")
else:
self._mode = mode
@@ -202,11 +202,22 @@ class MpvPlayer(Player):
log("Starting playback...")
self.emit("played", 0)
t_list = self._player._get_property("track-list")
if t_list:
# Audio tracks.
a_tracks = filter(lambda t: t.get("type", "") == "audio", t_list)
self.emit("audio-track", ((t.get("id", 1), t.get("lang", "Unknown")) for t in a_tracks))
# Subtitle.
sub_tracks = [(0, "no")]
tracks = filter(lambda t: t.get("type", "") == "sub", t_list)
[sub_tracks.append((t.get("id", 1), t.get("lang", "Unknown"))) for t in tracks]
self.emit("subtitle-track", sub_tracks)
@self._player.event_callback(mpv.MpvEventID.END_FILE)
def on_end(event):
event = event.get("event", {})
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
log(f"Stream playback error: {event.get('error', mpv.ErrorCode.GENERIC)}")
self.emit("error", "Can't Playback!")
@classmethod
@@ -243,6 +254,15 @@ class MpvPlayer(Player):
def is_playing(self):
return self._is_playing
def set_audio_track(self, track):
self._player._set_property("aid", track)
def set_subtitle_track(self, track):
self._player._set_property("sub", track)
def set_aspect_ratio(self, ratio):
self._player._set_property("aspect", ratio or "-1.0")
class GstPlayer(Player):
""" Simple wrapper for GStreamer playbin. """
@@ -260,7 +280,7 @@ class GstPlayer(Player):
# Initialization of GStreamer.
Gst.init(sys.argv)
except (OSError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No GStreamer is found. Check that it is installed!")
else:
self.STATE = Gst.State
@@ -293,11 +313,11 @@ class GstPlayer(Player):
self._player.set_property("uri", mrl)
log("Setting the URL for playback: {}".format(mrl))
log(f"Setting the URL for playback: {mrl}")
ret = self._player.set_state(self.STATE.PLAYING)
if ret == self.STAT_RETURN.FAILURE:
msg = "ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl)
msg = f"ERROR: Unable to set the 'PLAYING' state for '{mrl}'."
log(msg)
self.emit("error", msg)
else:
@@ -356,7 +376,7 @@ class GstPlayer(Player):
tags = self._player.emit("get-video-tags", i)
if tags:
_, cod = tags.get_string("video-codec")
log("Video codec: {}".format(cod or "unknown"))
log(f"Video codec: {cod or 'unknown'}")
nr_audio = self._player.get_property("n-audio")
for i in range(nr_audio):
@@ -364,7 +384,7 @@ class GstPlayer(Player):
tags = self._player.emit("get-audio-tags", i)
if tags:
_, cod = tags.get_string("audio-codec")
log("Audio codec: {}".format(cod or "unknown"))
log(f"Audio codec: {cod or 'unknown'}")
class VlcPlayer(Player):
@@ -378,18 +398,18 @@ class VlcPlayer(Player):
def __init__(self, mode, widget):
super().__init__(mode, widget)
try:
if sys.platform == "win32":
if IS_WIN:
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
from app.tools import vlc
from app.tools.vlc import EventType
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
self._player = vlc.Instance(args).media_player_new()
vlc.libvlc_video_set_key_input(self._player, False)
vlc.libvlc_video_set_mouse_input(self._player, False)
except (OSError, AttributeError, NameError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No VLC is found. Check that it is installed!")
else:
self._mode = mode
@@ -457,17 +477,17 @@ class VlcPlayer(Player):
def on_playback_start(self, event):
self.emit("played", self._player.get_media().get_duration())
# Audio tracks
# Audio tracks.
a_desc = self._player.audio_get_track_description()
self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc])
# Subtitle
# Subtitle.
s_desc = self._player.video_get_spu_description()
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
def init_video_widget(self, widget):
if sys.platform == "linux":
if IS_LINUX:
self._player.set_xwindow(self.get_window_handle())
elif sys.platform == "darwin":
elif IS_DARWIN:
self._player.set_nsobject(self.get_window_handle())
else:
self._player.set_hwnd(self.get_window_handle())
@@ -481,15 +501,18 @@ class Recorder:
def __init__(self, settings):
try:
if IS_WIN:
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError
else:
self._settings = settings
self._is_record = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
self._recorder = vlc.Instance(args).media_player_new()
@classmethod
@@ -506,7 +529,8 @@ class Recorder:
path = self._settings.records_path
os.makedirs(os.path.dirname(path), exist_ok=True)
d_now = datetime.now().strftime(_DATE_FORMAT)
path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_"))
d_now = d_now.replace(" ", "_").replace(":", "-") if IS_WIN else d_now.replace(" ", "_")
path = f"{path}{name.replace(' ', '_')}_{d_now}"
cmd = self.get_transcoding_cmd(path) if self._settings.activate_transcoding else self._CMD.format(path)
media = self._recorder.get_instance().media_new(url, cmd)
media.get_mrl()
@@ -514,7 +538,7 @@ class Recorder:
self._recorder.set_media(media)
self._is_record = True
self._recorder.play()
log("Record started {}".format(d_now))
log(f"Record started {d_now}")
@run_task
def stop(self):
@@ -536,7 +560,7 @@ class Recorder:
def get_transcoding_cmd(self, path):
presets = self._settings.transcoding_presets
prs = presets.get(self._settings.active_preset)
return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path)
return self._TR_CMD.format(",".join(f"{k}={v}" for k, v in prs.items()), path)
if __name__ == "__main__":

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -57,7 +57,7 @@ class PiconsCzDownloader:
_PERM_URL = "https://picon.cz/download/7337"
_BASE_URL = "https://picon.cz/download/"
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
_HEADER = {"User-Agent": "DemonEditor/2.0.0", "Referer": ""}
_HEADER = {"User-Agent": "DemonEditor/2.1.2", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
@@ -130,7 +130,7 @@ class PiconsCzDownloader:
# TODO: think about https://github.com/miurahr/py7zr
exe = "7z"
if IS_DARWIN and GTK_PATH:
exe = "./7z"
exe = "./7zr"
if IS_LINUX and not os.path.isfile(f"/usr/bin/{exe}"):
raise PiconsError("7-zip [7z] archiver not found!")
@@ -219,7 +219,8 @@ class PiconsCzDownloader:
"picontransparentdark": "td220",
"piconoled": "o96",
"piconblack80": "b50",
"piconblack3d": "b50"
"piconblack3d": "b50",
"piconwin11": "win11220"
}
def get_name_map(self):

View File

@@ -1,6 +1,34 @@
""" Module for downloading satellites, transponders ans services from the web.
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
Sources: www.flysat.com, www.lyngsat.com.
""" Module for downloading satellites, transponders and services from the Web.
Sources: www.flysat.com, www.lyngsat.com, www.kingofsat.net.
Replaces or updates the current satellites.xml file.
"""
import re
@@ -15,10 +43,11 @@ from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLAR
Service, CAS)
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0"}
_TIMEOUT = 10
class SatelliteSource(Enum):
FLYSAT = ("https://www.flysat.com/satlist.php",)
FLYSAT = ("https://www.flysat.com/en/satellitelist",)
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
@@ -38,10 +67,10 @@ class Cell:
self._img = img
def __repr__(self):
return "Cell({}, {}, {})".format(self._text, self._url, self._img)
return f"Cell({self._text}, {self._url}, {self._img})"
def __str__(self):
return "<Cell(text={}, link={}, img={})>".format(self._text, self._url, self._img)
return f"<Cell(text={self._text}, link={self._url}, img={self._img})>"
def __iter__(self):
return (x for x in (self._text, self._url, self._img))
@@ -92,6 +121,7 @@ class SatellitesParser(HTMLParser):
self._current_cell = []
self._rows = []
self._source = source
self.pls_modes = {v: k for k, v in PLS_MODE.items()}
def handle_starttag(self, tag, attrs):
if tag == "td":
@@ -134,9 +164,9 @@ class SatellitesParser(HTMLParser):
for src in SatelliteSource.get_sources(self._source):
try:
request = requests.get(url=src, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log(repr(e))
request = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
log(f"Getting satellite list error: {repr(e)}")
return []
else:
reason = request.reason
@@ -147,63 +177,99 @@ class SatellitesParser(HTMLParser):
if self._rows:
if self._source is SatelliteSource.FLYSAT:
def get_sat(r):
return r[1], self.parse_position(r[2]), r[3], r[0], False
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
return self.get_satellites_for_fly_sat()
elif self._source is SatelliteSource.LYNGSAT:
base_url = "https://www.lyngsat.com/"
sats = []
cur_pos = "0"
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
if not row[0]:
row = row[1:]
pos = self.parse_position(row[1])
if not self.POS_PAT.match(pos):
if len(row) == 4 and row[0].endswith(".html"):
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
continue
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
cur_pos = pos
return sats
return self.get_satellites_for_lyng_sat()
elif source is SatelliteSource.KINGOFSAT:
def get_sat(r):
return r[3], self.parse_position(r[1]), None, r[2], False
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
return self.get_satellites_for_king_of_sat()
def get_satellite(self, sat):
pos = sat[1]
return Satellite(name="{} {}".format(pos, sat[0]),
flags="0",
return Satellite(name=f"{pos} {sat[0]}", flags="0",
position=self.get_position(pos.replace(".", "")),
transponders=self.get_transponders(sat[3]))
def get_satellites_for_fly_sat(self):
sat_pat = re.compile(r"https://.*/satellite/.+")
pos_pat = re.compile(r"https://.*/satellite/position/.+")
names = []
pos = ""
pos_url = ""
def normalize_pos(p):
return f"{float(p[:-1])}{p[-1]}" if "." not in p else p
def get_sat(r):
nonlocal pos
nonlocal pos_url
# Uniting satellites in position.
if re.match(pos_pat, r[2]):
pos_url = r[2]
name = r[1]
pos = normalize_pos(self.parse_position(r[3]))
names.append(name)
return name, pos, r[5], r[0], False
r_size = len(r)
if r_size == 5:
name = r[1]
names.append(name)
return name, pos, r[3], r[0], False
if r_size == 6:
if names:
name = "/".join(names)
names.clear()
return name, pos, None, pos_url, False
return r[1], normalize_pos(self.parse_position(r[2])), r[4], r[0], False
return list(filter(None, map(get_sat, filter(lambda row: row and re.match(sat_pat, row[0]), self._rows))))
def get_satellites_for_lyng_sat(self):
base_url = "https://www.lyngsat.com/"
sats = []
cur_pos = "0"
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
if not row[0]:
row = row[1:]
pos = self.parse_position(row[1])
if not self.POS_PAT.match(pos):
if len(row) == 4 and row[0].endswith(".html"):
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
continue
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
cur_pos = pos
return sats
def get_satellites_for_king_of_sat(self):
def get_sat(r):
return r[3], self.parse_position(r[1]), None, r[2], False
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
@staticmethod
def parse_position(pos_str):
return "".join(c for c in pos_str if c.isdigit() or c.isalpha() or c == ".")
@staticmethod
def get_position(pos):
return "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
return f"{'-' if pos[-1] == 'W' else ''}{pos[:-1]}"
def get_transponders(self, sat_url):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
trs = []
url = sat_url
if self._source is SatelliteSource.FLYSAT:
url = "https://www.flysat.com/" + sat_url
elif self._source is SatelliteSource.KINGOFSAT:
url = "https://en.kingofsat.net/" + sat_url
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
try:
request = requests.get(url=url, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log("Getting transponders error: {}".format(e))
request = requests.get(url=sat_url, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
log(f"Getting transponders error: {e}")
else:
if request.status_code == 200:
self.feed(request.text)
@@ -214,87 +280,89 @@ class SatellitesParser(HTMLParser):
elif self._source is SatelliteSource.KINGOFSAT:
self.get_transponders_for_king_of_sat(trs)
else:
log("SatellitesParser [get transponders] error: {} {}".format(url, request.reason))
log(f"SatellitesParser [get transponders] error: {sat_url} {request.reason}")
return sorted(trs, key=lambda x: int(x.frequency))
def get_transponders_for_fly_sat(self, trs):
""" Parsing transponders for FlySat """
pls_pattern = re.compile("(PLS:)+ (Root|Gold|Combo)+ (\\d+)?")
is_id_pattern = re.compile("(Stream) (\\d+)")
pls_modes = {v: k for k, v in PLS_MODE.items()}
""" Parsing transponders for FlySat. """
frq_pol_pattern = re.compile(r"(\d{4,5})+\s+([RLHV]).*(DVB-S[2]?)/(.+PSK)?.*")
pls_pattern = re.compile(r".*PLS\s+(Root|Gold|Combo)+\s(\d+)?")
is_id_pattern = re.compile(r"Stream\s(\d+)")
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
n_trs = []
if self._rows:
zeros = "000"
is_ids = []
for r in self._rows:
if len(r) == 1:
row_len = len(r)
if row_len == 1:
is_ids.extend(re.findall(is_id_pattern, r[0]))
continue
if len(r) < 3:
if row_len < 12:
continue
data = r[2].split(" ")
if len(data) != 2:
continue
sr, fec = data
data = r[1].split(" ")
if len(data) < 3:
continue
freq, pol, sys = data[0], data[1], data[2]
sys = sys.split("/")
if len(sys) != 2:
continue
sys, mod = sys
mod = "QPSK" if sys == "DVB-S" else mod
pls = re.findall(pls_pattern, r[1])
freq = re.findall(frq_pol_pattern, r[2])
if not freq:
continue
freq, pol, sys, mod = freq[0]
sr_fec = re.match(sr_fec_pattern, r[3])
if not sr_fec:
continue
sr, fec = sr_fec.group(1), sr_fec.group(2)
pls = re.match(pls_pattern, r[2])
pls_code = None
pls_mode = None
if pls:
pls_code = pls[0][2]
pls_mode = pls_modes.get(pls[0][1], None)
pls_mode = self.pls_modes.get(pls.group(1), None)
pls_code = pls.group(2)
if is_ids:
tr = trs.pop()
for index, is_id in enumerate(is_ids):
tr = tr._replace(is_id=is_id[1])
tr = tr._replace(is_id=is_id)
if is_transponder_valid(tr):
n_trs.append(tr)
else:
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, None)
if is_transponder_valid(tr):
trs.append(tr)
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, None)
if is_transponder_valid(tr):
trs.append(tr)
is_ids.clear()
trs.extend(n_trs)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat. """
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
sr_fec_pattern = re.compile(r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s*(?:T2-MI\s+PLP\s+(\d+))?.*")
zeros = "000"
pls_mode, pls_code, pls_id = None, None, None
frq_pol_pattern = re.compile(r"(\d{4,5})\s+([RLHV]).*")
sr_fec_pattern = re.compile((r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s?"
r"(?:T2-MI\s+PLP\s+(\d+))?.*"
r"?(?:PLS\s+(Root|Gold|Combo)\s+(\d+))?"
r"(?:.*Stream\s+(\d+))?.*"))
for row in filter(lambda x: len(x) > 8, self._rows):
for frq in row[1], row[2], row[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
for freq in row[1], row[2], row[3]:
res = re.match(frq_pol_pattern, freq)
if res:
break
if not freq:
if not res:
continue
frq, pol = freq.group(1), freq.group(2)
srf = " ".join(row[3:5])
sr_fec = re.search(sr_fec_pattern, srf)
if not sr_fec:
freq, pol = res.group(1), res.group(2)
res = re.search(sr_fec_pattern, row[3])
if not res:
continue
sys, mod, sr, fec = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3), sr_fec.group(4)
sys, mod, sr, fec = res.group(1), res.group(2), res.group(3), res.group(4)
mod = mod.strip() if mod else "Auto"
pls_id = sr_fec.group(5)
plp, pls_mode, pls_code, is_id = res.group(5), res.group(6), res.group(7), res.group(8)
pls_mode = self.pls_modes.get(pls_mode, None)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
if plp is not None:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, is_id)
if is_transponder_valid(tr):
trs.append(tr)
@@ -303,19 +371,37 @@ class SatellitesParser(HTMLParser):
Since the *.ini file contains incomplete information, it is not used.
"""
zeros = "000"
pat = re.compile(
r"(\d+).00\s+([RLHV])\s+(DVB-S[2]?)\s+(?:T2-MI, PLP (\d+)\s+)?(.*PSK).*?(?:Stream\s+(\d+))?\s+(\d+)\s+(\d+/\d+)$")
sys_pat = re.compile(r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?")
mod_pat = re.compile(r"(.*PSK).*?(?:.*Stream\s+(\d+))?.*")
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
res = pat.search(" ".join((row[0], row[2], row[3], row[8], row[9], row[10])))
if res:
freq, sr, pol, fec, sys = res.group(1), res.group(7), res.group(2), res.group(8), res.group(3)
mod, pls_id, pls_code = res.group(5), res.group(4), res.group(6)
freq, pol = row[2].replace(".", "0"), row[3]
if not freq.isdigit() or pol not in "VHLR":
continue
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, pls_code, pls_id)
if is_transponder_valid(tr):
trs.append(tr)
res = re.match(sys_pat, row[8])
if not res:
continue
sys, t2_mi, pls_id, pls_code = res.group(1), res.group(2), res.group(3), res.group(4)
pls_id = self.pls_modes.get(pls_id, None)
res = re.match(mod_pat, row[9])
if not res:
continue
mod, is_id = res.group(1), res.group(2)
res = re.match(sr_fec_pattern, row[10])
if not res:
continue
sr, fec = res.group(1), res.group(2)
if t2_mi:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(freq, f"{sr}000", pol, fec, sys, mod, pls_id, pls_code, is_id)
if is_transponder_valid(tr):
trs.append(tr)
class ServicesParser(HTMLParser):
@@ -329,16 +415,27 @@ class ServicesParser(HTMLParser):
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
"HEVC UHD": "31", "HEVC UHD 4K": "31", "3": "Data"}
self._TR_PAT = re.compile(
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
self._S2_TR = "{}:{}:{}:{}"
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
# LyngSat.
self._TR_PAT = re.compile((r".*?(\d+)\.?\d?\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s"
r"?(T2-MI)?\s?(PLS\s+Multistream)?\s?"
r"SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*"))
self._MULTI_PAT = re.compile(r"PLS\s+(Root|Gold|Combo)+\s(\d+)?\s+(?:Stream\s(\d+))")
# KingOfSat.
self._KING_TR_PAT = re.compile((r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*"
r"?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?"
r"\s+(.*PSK).*?(?:.*Stream\s+(\d+))?.*"))
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._is_mux_div = False
self._current_row = []
self._current_cell_text = []
self._current_cell = Cell()
@@ -346,6 +443,7 @@ class ServicesParser(HTMLParser):
self._source = source
self._t_url = ""
self._use_short_names = True
self._pls_modes = {v: k for k, v in PLS_MODE.items()}
@property
def source(self):
@@ -389,12 +487,18 @@ class ServicesParser(HTMLParser):
self._current_cell.img = img_link
elif self._source is SatelliteSource.KINGOFSAT:
self._current_cell.img = img_link
elif tag == "div" and self._source is SatelliteSource.LYNGSAT:
self._is_mux_div = bool(list(filter(lambda at: at[-1] == "mux-header", attrs)))
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell_text.append(data.strip())
if self._is_mux_div:
self._current_cell.url = data.strip()
self._is_mux_div = False
def handle_endtag(self, tag):
if tag == "td":
self._is_td = False
@@ -414,21 +518,24 @@ class ServicesParser(HTMLParser):
self._current_row = []
def error(self, message):
log("ServicesParser error: {}".format(message))
log(f"ServicesParser error: {message}")
def init_data(self, url):
""" Initializes data for the given URL. """
if self._source not in (SatelliteSource.LYNGSAT, SatelliteSource.KINGOFSAT):
raise ValueError("Unsupported source: {}!".format(self._source.name))
raise ValueError(f"Unsupported source: {self._source.name}!")
self._rows.clear()
request = requests.get(url=url, headers=_HEADERS)
reason = request.reason
if reason == "OK":
self.feed(request.text)
try:
request = requests.get(url=url, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
raise ValueError(e)
else:
raise ValueError(reason)
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
raise ValueError(reason)
def get_transponders_links(self, sat_url):
""" Returns transponder links. """
@@ -467,13 +574,13 @@ class ServicesParser(HTMLParser):
self.init_data(tr_url)
except ValueError as e:
log(e)
return []
else:
if self._source is SatelliteSource.LYNGSAT:
return self.get_lyngsat_services(sat_position, use_pids)
elif self._source is SatelliteSource.KINGOFSAT:
return self.get_kingofsat_services(sat_position, use_pids)
else:
return []
return []
def get_lyngsat_services(self, sat_position=None, use_pids=False):
services = []
@@ -481,7 +588,10 @@ class ServicesParser(HTMLParser):
sys = "DVB-S"
pos_found = False
tr = None
# Transponder
# Multi-stream.
multi_tr = None
multi = False
# Transponder.
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
if not pos_found:
pos_tr = re.match(self._POS_PAT, r[0].text)
@@ -490,7 +600,6 @@ class ServicesParser(HTMLParser):
if not sat_position:
pos = self.get_position(pos_tr.group(1))
pos_found = True
if pos_found:
@@ -498,34 +607,46 @@ class ServicesParser(HTMLParser):
td = re.match(self._TR_PAT, text)
if td:
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
if td.group(5):
log("Detected T2-MI transponder!")
continue
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
nid, tid = td.group(8), td.group(9)
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(7), td.group(8)
nid, tid = td.group(9), td.group(10)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
nid, tid = int(nid), int(tid)
if td.group(5):
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}]")
if td.group(6):
log(f"Detected multi-stream transponder! [{freq} {sr} {pol}]")
multi = True
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
if not tr:
er = f"Transponder [{freq}] not found or its type [T2-MI, etc] not supported yet."
er = f"Transponder [{self._t_url}] not found or its type [T2-MI, etc] not supported yet."
log(f"ServicesParser error [get transponder services]: {er}")
return services
# Services
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
try:
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
v_pid, a_pid, cas, use_pids)
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
except ValueError as e:
log(f"ServicesParser error [get transponder services]: {e}")
# Services.
for r in filter(None, self._rows):
if multi and r[0].url:
res = re.match(self._MULTI_PAT, r[0].url)
if res:
pls_mode, is_code, is_id = self._pls_modes.get(res.group(1), None), res.group(2), res.group(3)
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
tid = int(is_id) if multi_tr else tid
if len(r) == 12 and r[0].text.isdigit():
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
try:
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
v_pid, a_pid, cas, use_pids)
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
except ValueError as e:
log(f"ServicesParser error [get transponder services]: {e}")
return services
@@ -537,40 +658,60 @@ class ServicesParser(HTMLParser):
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
return services
tr = tr[0]
s_pos, freq, pol, sys, mod, sr_fec = tr[0].text, tr[2].text, tr[3].text, tr[6].text, tr[7].text, tr[8].text
tid, nid = tr[10].text, tr[11].text
tr, multi_tr, tid, nid, nsp = None, None, None, None, None
freq, sr, pol, fec, sys, pos = None, None, None, None, None, None
pos = sat_position
if not sat_position:
pos_tr = re.match(self._POS_PAT, s_pos)
if pos_tr:
pos = self.get_position(pos_tr.group(1))
for r in filter(lambda x: len(x) > 12, self._rows):
r_size = len(r)
if r_size == 13 and r[4].url and r[4].url.startswith("tp.php?tp="):
res = re.match(self._KING_TR_PAT, f"{r[6].text} {r[7].text}")
if not res:
continue
sr, fec = sr_fec.split()
pol = get_key_by_value(POLARIZATION, pol)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
freq, nid, tid = int(float(freq)), int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
sys, mod = res.group(1), res.group(5)
s_pos, freq, pol, sr_fec = r[0].text, r[2].text, r[3].text, r[8].text
nid, tid = r[10].text, r[11].text
for r in filter(lambda x: len(x) == 14 and not x[1].text and x[7].text and x[7].text.isdigit(), self._rows):
if r[1].img == "/radio.gif":
s_type = ""
elif r[8].img == "/hd.gif":
s_type = "HEVC HD"
elif r[1].img == "/data.gif":
s_type = "Data"
else:
s_type = "SD"
pos = sat_position
if not sat_position:
pos_tr = re.match(self._POS_PAT, s_pos)
if pos_tr:
pos = self.get_position(pos_tr.group(1))
s_type = self._S_TYPES.get(s_type, "3")
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
sr, fec = sr_fec.split()
pol = get_key_by_value(POLARIZATION, pol)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
freq, nid, tid = int(float(freq)), int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
v_pid, a_pid, cas, use_pids)
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, tr))
pls_mode, is_code, is_id = self._pls_modes.get(res.group(3), None), res.group(4), res.group(6)
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
tid = int(is_id) if multi_tr else tid
if res.group(2):
log(f"Detected T2-MI transponder! [{freq} {sr}]")
if multi_tr:
log(f"Detected multi-stream transponder! [{freq} {sr}]")
if tr and r_size == 14 and not r[1].text and r[7].text and r[7].text.isdigit():
if r[1].img == "/radio.gif":
s_type = ""
elif r[8].img == "/hd.gif":
s_type = "HEVC HD"
elif r[1].img == "/data.gif":
s_type = "Data"
else:
s_type = "SD"
s_type = self._S_TYPES.get(s_type, "3")
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
v_pid, a_pid, cas, use_pids)
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
return services
@@ -580,7 +721,7 @@ class ServicesParser(HTMLParser):
mod = get_key_by_value(MODULATION, mod)
fec = get_key_by_value(FEC, fec)
# For negative (West) positions: 3600 - numeric position value!!!
namespace = "{:04x}0000".format(3600 - pos if pos < 0 else pos)
namespace = f"{3600 - pos if pos < 0 else pos:04x}0000"
tr_flag = 1
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto
@@ -592,15 +733,15 @@ class ServicesParser(HTMLParser):
@staticmethod
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
sid = int(sid)
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
data_id = f"{sid:04x}:{namespace}:{tid:04x}:{nid:04x}:{s_type}:0:0"
fav_id = f"{sid}:{tid}:{nid}:{namespace}"
picon_id = f"1_0_{int(s_type):X}_{sid}_{tid}_{nid}_{namespace}_0_0_0.png"
# Flags.
flags = "p:{}".format(pkg)
flags = f"p:{pkg}"
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
if use_pids:
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
v_pid = f"c:00{int(v_pid):04x}" if v_pid else None
a_pid = ",".join([f"c:01{int(p):04x}" for p in a_pid]) if a_pid else None
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
else:
flags = ",".join(filter(None, (flags, cas)))

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +26,7 @@
#
""" Module for working with YouTube service """
""" Module for working with YouTube service. """
import gzip
import json
import os
@@ -35,20 +35,21 @@ import shutil
import sys
from html.parser import HTMLParser
from json import JSONDecodeError
from urllib import parse
from urllib.error import URLError
from urllib.parse import unquote
from urllib.request import Request, urlopen, urlretrieve
from app.commons import log
from app.settings import SEP
from app.ui.uicommons import show_notification
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*")
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
_TIMEOUT = 5
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0",
"DNT": "1",
"Accept-Encoding": "gzip, deflate"}
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*")
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
@@ -121,44 +122,27 @@ class YouTube:
Returns tuple from the video links dict and title.
"""
req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS)
info = InnerTube().player(video_id)
det = info.get("videoDetails", None)
title = det.get("title", None) if det else None
streaming_data = info.get("streamingData", None)
fmts = streaming_data.get("formats", None) if streaming_data else None
with urlopen(req, timeout=2) as resp:
data = unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(unquote, data))}
player_resp = out.get("player_response", None)
if fmts:
links = {Quality[i["itag"]]: i["url"] for i in filter(
lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
if player_resp:
try:
resp = json.loads(player_resp)
except JSONDecodeError as e:
log(f"{__class__.__name__}: Parsing player response error: {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 links and title:
return links, title.replace("+", " ")
if fmts:
urls = {Quality[i["itag"]]: i["url"] for i in
filter(lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
cause = None
status = info.get("playabilityStatus", None)
if status:
cause = f"[{status.get('status', '')}] {status.get('reason', '')}"
if urls and title:
return urls, title.replace("+", " ")
log(f"{__class__.__name__}: Getting link to video with id '{video_id}' filed! Cause: {cause}")
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 = 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
return None, cause
def get_yt_playlist(self, list_id, url=None):
""" Returns tuple from the playlist header and list of tuples (title, video id). """
@@ -181,6 +165,74 @@ class YouTube:
return PlayListParser.get_yt_playlist(list_id)
class InnerTube:
""" Object for interacting with the innertube API.
Based on InnerTube class from pytube [https://github.com/pytube/pytube] project!
"""
_BASE_URI = "https://www.youtube.com/youtubei/v1"
_DEFAULT_CLIENTS = {
"ANDROID": {
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20"}},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
},
"ANDROID_EMBED": {
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20", "clientScreen": "EMBED"}},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
}
}
def __init__(self, client="ANDROID"):
""" Initialize an InnerTube object.
@param client: Client to use for the object. Default to web because it returns the most playback types.
"""
self.context = self._DEFAULT_CLIENTS[client]["context"]
self.api_key = self._DEFAULT_CLIENTS[client]["api_key"]
@property
def base_data(self):
"""Return the base json data to transmit to the innertube API."""
return {"context": self.context}
@property
def base_params(self):
"""Return the base query parameters to transmit to the innertube API."""
return {"key": self.api_key, "contentCheckOk": True, "racyCheckOk": True}
def player(self, video_id):
""" Make a request to the player endpoint. Returns raw player info results. """
endpoint = f"{self._BASE_URI}/player"
query = {"videoId": video_id}
query.update(self.base_params)
return self._call_api(endpoint, query, self.base_data) or {}
@staticmethod
def _call_api(endpoint, query, data):
""" Make a request to a given endpoint with the provided query parameters and data."""
headers = {"Content-Type": "application/json", }
response = InnerTube._execute(f"{endpoint}?{parse.urlencode(query)}", "POST", headers=headers, data=data)
try:
resp = json.loads(response.read())
except JSONDecodeError as e:
log(f"{__class__.__name__}: Parsing response error: {e}")
else:
return resp
@staticmethod
def _execute(url, method=None, headers=None, data=None, timeout=_TIMEOUT):
base_headers = {"User-Agent": "Mozilla/5.0", "accept-language": "en-US,en"}
if headers:
base_headers.update(headers)
if data:
# Encoding data for request.
if not isinstance(data, bytes):
data = bytes(json.dumps(data), encoding="utf-8")
return urlopen(Request(url, headers=base_headers, method=method, data=data), timeout=timeout)
class PlayListParser(HTMLParser):
""" Very simple parser to handle YouTube playlist pages. """
@@ -207,7 +259,7 @@ class PlayListParser(HTMLParser):
try:
resp = json.loads(data)
except JSONDecodeError as e:
log("{}: Parsing data error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Parsing data error: {e}")
else:
sb = resp.get("sidebar", None)
if sb:
@@ -241,9 +293,9 @@ class PlayListParser(HTMLParser):
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)
request = Request(f"https://www.youtube.com/playlist?list={play_list_id}&hl=en", headers=_HEADERS)
with urlopen(request, timeout=2) as resp:
with urlopen(request, timeout=_TIMEOUT) as resp:
data = gzip.decompress(resp.read()).decode("utf-8")
parser = PlayListParser()
parser.feed(data)
@@ -295,10 +347,10 @@ class YouTubeDL:
try:
import youtube_dl
except ModuleNotFoundError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
log(f"YouTubeDLHelper error: {e}")
raise YouTubeException(e)
except ImportError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
log(f"YouTubeDLHelper error: {e}")
else:
if self._path not in youtube_dl.__file__:
msg = "Another version of youtube-dl was found on your system!"

View File

@@ -133,6 +133,16 @@
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Display picons</attribute>
<attribute name="action">app.display_picons</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Alternate layout</attribute>
<attribute name="action">app.set_alternate_layout</attribute>
</item>
</section>
</submenu>
<submenu id="tools_menu">
<attribute name="label" translatable="yes">Tools</attribute>
@@ -323,6 +333,16 @@
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Display picons</attribute>
<attribute name="action">app.display_picons</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Alternate layout</attribute>
<attribute name="action">app.set_alternate_layout</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Tools</attribute>
@@ -368,7 +388,7 @@
</item>
<item>
<attribute name="label" translatable="yes">Export to m3u</attribute>
<attribute name="action">app.on_export_to_m3u</attribute>
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
</item>
<section>
<item>

View File

@@ -1158,81 +1158,66 @@ Author: Dmitriy Yefremov
<!-- column-name data -->
<column type="PyObject"/>
</columns>
<signal name="row-deleted" handler="on_recordings_model_changed" swapped="no"/>
<signal name="row-inserted" handler="on_recordings_model_changed" swapped="no"/>
</object>
<object class="GtkFrame" id="recordings_frame">
<object class="GtkBox" id="recordings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="recordings_box">
<object class="GtkPaned" id="recordings_paned">
<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">2</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="recordings_header_box">
<object class="GtkFrame" id="recordings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkButton" id="recordings_remove_button">
<object class="GtkBox" id="recordings_main_box">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_recording_remove" swapped="no"/>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="remove_recording_image">
<object class="GtkBox" id="recordings_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</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">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recordings_fs_box">
<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_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="recordings_search_box">
<property name="can_focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkSearchEntry" id="fav_search_entry1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">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="spacing">5</property>
<child>
<object class="GtkButton" id="recordings_remove_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_recording_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_recording_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -1241,196 +1226,198 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="fav_search_down_button1">
<object class="GtkScrolledWindow" id="recordings_view_scrolled_window">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkArrow" id="fav_down_arrow1">
<object class="GtkTreeView" id="recordings_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="arrow_type">down</property>
<property name="can_focus">True</property>
<property name="model">recordings_model</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">5</property>
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="recordings_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_service_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Service</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_service_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_title_column">
<property name="sizing">autosize</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Title</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_title_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_time_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Time</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_time_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_len_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Length</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_len_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_file_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">File</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_file_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_desc_column">
<property name="resizable">True</property>
<property name="title" translatable="yes">Description</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_desc_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="fav_search_up_button1">
<object class="GtkBox" id="recordings_status_box">
<property name="height_request">24</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="spacing">5</property>
<child>
<object class="GtkArrow" id="fav_up_arrow1">
<object class="GtkImage" id="recordings_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="arrow_type">up</property>
<property name="icon_name">document-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="recordings_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>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="group"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
<child type="label">
<object class="GtkLabel" id="recordings_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Recordings</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkPaned" id="recordings_paned">
<object class="GtkFrame" id="recordings_paths_frame">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkScrolledWindow" id="recordings_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="recordings_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">recordings_model</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">5</property>
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="recordings_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_service_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Service</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_service_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_title_column">
<property name="sizing">autosize</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Title</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_title_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_time_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Time</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_time_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_len_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Length</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_len_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_file_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">File</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_file_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_desc_column">
<property name="resizable">True</property>
<property name="title" translatable="yes">Description</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_desc_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
<property name="width_request">250</property>
<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="shadow_type">in</property>
<property name="min_content_height">100</property>
<child>
@@ -1438,6 +1425,7 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">rec_paths_model</property>
<property name="headers_visible">False</property>
<property name="search_column">1</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
@@ -1479,26 +1467,26 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Paths</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="recordings_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Recordings</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<object class="GtkListStore" id="timer_model">

View File

@@ -126,7 +126,7 @@ class EpgTool(Gtk.Box):
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
time = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
time = f"{start_time.strftime('%A, %H:%M')} - {end_time.strftime('%H:%M')}"
return title, time, desc, event
@@ -223,7 +223,7 @@ class TimerTool(Gtk.Box):
elif self._action is TimerTool.TimerAction.EVENT:
self.set_timer_from_event_data()
else:
log("{} error: No action set for timer!".format(__class__.__name__))
log(f"{__class__.__name__} error: No action set for timer!")
@property
def request(self):
@@ -242,34 +242,34 @@ class TimerTool(Gtk.Box):
s_ref = quote(t_data.get("sRef", ""))
if self._action is TimerTool.TimerAction.EVENT:
args.append("timeraddbyeventid?sRef={}".format(s_ref))
args.append("eventid={}".format(t_data.get("eit", "0")))
args.append("justplay={}".format(t_data.get("justplay", "")))
args.append("tags={}".format(""))
args.append(f"timeraddbyeventid?sRef={s_ref}")
args.append(f"eventid={t_data.get('eit', '0')}")
args.append(f"justplay={t_data.get('justplay', '')}")
args.append(f"tags={''}")
else:
if self._action is TimerTool.TimerAction.ADD:
args.append("timeradd?sRef={}".format(s_ref))
args.append("deleteOldOnSave={}".format(0))
args.append(f"timeradd?sRef={s_ref}")
args.append(f"deleteOldOnSave={0}")
elif self._action is TimerTool.TimerAction.CHANGE:
args.append("timerchange?sRef={}".format(s_ref))
args.append("channelOld={}".format(s_ref))
args.append("beginOld={}".format(self._timer_data.get("e2timebegin", "0")))
args.append("endOld={}".format(self._timer_data.get("e2timeend", "0")))
args.append("deleteOldOnSave={}".format(1))
args.append(f"timerchange?sRef={s_ref}")
args.append(f"channelOld={s_ref}")
args.append(f"beginOld={self._timer_data.get('e2timebegin', '0')}")
args.append(f"endOld={self._timer_data.get('e2timeend', '0')}")
args.append(f"deleteOldOnSave={1}")
args.append("begin={}".format(t_data.get("begin", "")))
args.append("end={}".format(t_data.get("end", "")))
args.append("name={}".format(quote(t_data.get("name", ""))))
args.append("description={}".format(quote(t_data.get("description", ""))))
args.append("tags={}".format(""))
args.append("eit={}".format("0"))
args.append("disabled={}".format(t_data.get("disabled", "1")))
args.append("justplay={}".format(t_data.get("justplay", "1")))
args.append("afterevent={}".format(t_data.get("afterevent", "0")))
args.append("repeated={}".format(TimerTool.get_repetition_flags(self._days_buttons)))
args.append(f"begin={t_data.get('begin', '')}")
args.append(f"end={t_data.get('end', '')}")
args.append(f"name={quote(t_data.get('name', ''))}")
args.append(f"description={quote(t_data.get('description', ''))}")
args.append(f"tags={''}")
args.append(f"eit={'0'}")
args.append(f"disabled={t_data.get('disabled', '1')}")
args.append(f"justplay={t_data.get('justplay', '1')}")
args.append(f"afterevent={t_data.get('afterevent', '0')}")
args.append(f"repeated={TimerTool.get_repetition_flags(self._days_buttons)}")
if self._timer_location_switch.get_active():
args.append("dirname={}".format(self._timer_location_entry.get_text()))
args.append(f"dirname={self._timer_location_entry.get_text()}")
return "&".join(args)
@@ -292,8 +292,7 @@ class TimerTool(Gtk.Box):
self._timer_begins_min_button.set_value(minute)
self._timer_begins_calendar.select_day(date.day)
self._timer_begins_calendar.select_month(date.month - 1, date.year)
self._timer_begins_entry.set_text(
"{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
self._timer_begins_entry.set_text(f"{date.year}-{date.month}-{date.day} {hour}:{minute:02d}")
def get_ends_date(self):
date = self._timer_ends_calendar.get_date()
@@ -308,7 +307,7 @@ class TimerTool(Gtk.Box):
self._timer_ends_min_button.set_value(minute)
self._timer_ends_calendar.select_day(date.day)
self._timer_ends_calendar.select_month(date.month - 1, date.year)
self._timer_ends_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
self._timer_ends_entry.set_text(f"{date.year}-{date.month}-{date.day} {hour}:{minute:02d}")
def set_timer_for_add(self):
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", ""))
@@ -446,7 +445,7 @@ class TimerTool(Gtk.Box):
service = timer.get("e2servicename", "") or ""
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
time = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
time = f"{start_time.strftime('%A, %H:%M')} - {end_time.strftime('%H:%M')}"
return disabled, name, service, time, description, timer
@@ -486,7 +485,7 @@ class TimerTool(Gtk.Box):
@run_idle
def timer_add_edit_callback(self, resp):
if "error_code" in resp:
msg = "Error getting timer status.\n{}".format(resp.get("error_code"))
msg = f"Error getting timer status.\n{resp.get('error_code')}"
self._app.show_error_message(msg)
log(msg)
return
@@ -628,7 +627,7 @@ class TimerTool(Gtk.Box):
service = self._app.current_services.get(fav_id, None)
if service:
if service.service_type == BqServiceType.ALT.name:
msg = "Alternative service.\n\n {}".format(get_message("Not implemented yet!"))
msg = "Alternative service.\n\n {get_message('Not implemented yet!')}"
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=msg)
context.finish(False, False, time)
return
@@ -647,6 +646,7 @@ class RecordingsTool(Gtk.Box):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("layout-changed", self.on_layout_changed)
self._app.connect("profile-changed", self.init)
self._settings = settings
self._ftp = None
@@ -658,14 +658,18 @@ class RecordingsTool(Gtk.Box):
handlers = {"on_path_press": self.on_path_press,
"on_path_activated": self.on_path_activated,
"on_recordings_activated": self.on_recordings_activated,
"on_recording_remove": self.on_recording_remove}
"on_recording_remove": self.on_recording_remove,
"on_recordings_model_changed": self.on_recordings_model_changed}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
objects=("recordings_frame", "recordings_model", "rec_paths_model"))
objects=("recordings_box", "recordings_model", "rec_paths_model"))
self._rec_view = builder.get_object("recordings_view")
self._paths_view = builder.get_object("recordings_paths_view")
self._paned = builder.get_object("recordings_paned")
self.pack_start(builder.get_object("recordings_frame"), True, True, 0)
self._recordings_count_label = builder.get_object("recordings_count_label")
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
if settings.alternate_layout:
self.on_layout_changed(app, True)
self.init()
self.show()
@@ -674,6 +678,14 @@ class RecordingsTool(Gtk.Box):
self._rec_view.get_model().clear()
self._paths_view.get_model().clear()
def on_layout_changed(self, app, alt_layout):
ch1 = self._paned.get_child1()
ch2 = self._paned.get_child2()
self._paned.remove(ch1)
self._paned.remove(ch2)
self._paned.add1(ch2)
self._paned.add(ch1)
@run_task
def init(self, app=None, arg=None):
GLib.idle_add(self.clear_data)
@@ -716,10 +728,14 @@ class RecordingsTool(Gtk.Box):
for f in files:
f_data = f.split()
if len(f_data) < 9:
log(f"{__class__.__name__}. Folder data parsing error. [{f}]")
continue
f_type = f_data[0][0]
if f_type == "d":
model.append((self._icon, f_data[-1], self._ftp.pwd()))
model.append((self._icon, " ".join(f_data[8:]), self._ftp.pwd()))
def on_path_activated(self, view, path, column):
row = view.get_model()[path][:]
@@ -774,6 +790,9 @@ class RecordingsTool(Gtk.Box):
self._app.show_error_message(resp)
break
def on_recordings_model_changed(self, model, path, itr=None):
self._recordings_count_label.set_text(str(len(model)))
def on_playback(self, box, state):
""" Updates state of the UI elements for playback mode. """
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
@@ -797,6 +816,7 @@ class ControlTool(Gtk.Box):
self._settings = settings
self._app = app
self._app.connect("layout-changed", self.on_layout_changed)
self._pix = None
handlers = {"on_volume_changed": self.on_volume_changed,
@@ -806,7 +826,7 @@ class ControlTool(Gtk.Box):
objects=("control_box", "volume_adjustment"))
self.pack_start(builder.get_object("control_box"), True, True, 0)
self._stack = builder.get_object("stack")
self._remote_box = builder.get_object("remote_box")
self._screenshot_area = builder.get_object("screenshot_area")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
self._screenshot_check_button = builder.get_object("screenshot_check_button")
@@ -819,6 +839,9 @@ class ControlTool(Gtk.Box):
self._agc_level_bar = builder.get_object("agc_level_bar")
self._volume_button = builder.get_object("volume_button")
self.init_actions(app)
if settings.alternate_layout:
self.on_layout_changed(app, True)
self.show()
def init_actions(self, app):
@@ -852,6 +875,9 @@ class ControlTool(Gtk.Box):
app.set_action("on_screenshot_video", self.on_screenshot_video)
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
def on_layout_changed(self, app, alt_layout):
self._remote_box.reorder_child(self._remote_box.get_children()[0], 1)
# ***************** Remote controller ********************* #
def on_remote(self, action, state=False):
@@ -871,7 +897,7 @@ class ControlTool(Gtk.Box):
@run_with_delay(0.5)
def on_volume_changed(self, button, value):
self._app.send_http_request(HttpAPI.Request.VOL, "{:.0f}".format(value), self.on_response)
self._app.send_http_request(HttpAPI.Request.VOL, f"{value:.0f}", self.on_response)
def update_volume(self, vol):
if "error_code" in vol:

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2021 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -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">2.0.1 Beta</property>
<property name="version">2.1.2 Beta</property>
<property name="copyright">2018-2021 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -812,17 +812,61 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkSearchBar" id="filter_bar">
<object class="GtkBox" id="filter_bar">
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkSwitch" id="filter_auto_switch">
<property name="name">filter_switch</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Automatically set the name selected in the bouquet list.</property>
<property name="valign">center</property>
<property name="margin_right">15</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="filter_auto_label">
<property name="name">filter_auto_label</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Auto</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child type="center">
<object class="GtkSearchEntry" id="filter_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<signal name="search-changed" handler="on_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
@@ -865,6 +909,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="model">service_sort_model</property>
<property name="search_column">0</property>
<property name="fixed_height_mode">True</property>
<property name="enable_grid_lines">both</property>
<signal name="button-press-event" handler="on_popup_menu" object="source_popup_menu" swapped="no"/>
<signal name="drag-begin" handler="on_drag_begin" swapped="no"/>
@@ -876,6 +921,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="service_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
@@ -894,6 +940,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="pos_column">
<property name="sizing">fixed</property>
<property name="min_width">70</property>
<property name="title" translatable="yes">Pos</property>
<property name="alignment">0.5</property>
@@ -912,6 +959,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ref_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Reference</property>
<property name="alignment">0.5</property>
@@ -1071,6 +1119,7 @@ Author: Dmitriy Yefremov
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">9</property>
<signal name="button-press-event" handler="on_bouquet_popup_menu" object="bouquet_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_bq_cursor_changed" swapped="no"/>
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
<signal name="key-release-event" handler="on_key_press" swapped="no"/>
<child internal-child="selection">
@@ -1081,6 +1130,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="num_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">30</property>
<property name="title" translatable="yes">Num</property>
<property name="alignment">0.5</property>
@@ -1101,6 +1151,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="fav_service_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
@@ -1157,7 +1208,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="fav_type_column">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="sizing">fixed</property>
<property name="min_width">40</property>
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
@@ -1177,7 +1228,7 @@ Author: Dmitriy Yefremov
<object class="GtkTreeViewColumn" id="fav_pos_column">
<property name="visible">False</property>
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="sizing">fixed</property>
<property name="min_width">10</property>
<property name="title" translatable="yes">Pos</property>
<child>
@@ -1194,7 +1245,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="fav_id_column">
<property name="visible">False</property>
<property name="sizing">autosize</property>
<property name="sizing">fixed</property>
<property name="title">fav_id</property>
<child>
<object class="GtkCellRendererText" id="fav_id_cellrenderertext4"/>
@@ -1207,7 +1258,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="fav_extra_column">
<property name="visible">False</property>
<property name="sizing">autosize</property>
<property name="sizing">fixed</property>
<property name="title" translatable="yes">extra</property>
<child>
<object class="GtkCellRendererText" id="fav_tooltip_cellrenderertext"/>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -37,7 +37,7 @@ from urllib.error import HTTPError, URLError
from gi.repository import GLib
from app.commons import run_idle, run_task
from app.commons import run_idle, run_task, run_with_delay
from app.connections import download_data, DownloadType
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP
@@ -54,7 +54,7 @@ class RefsSource(Enum):
class EpgDialog:
def __init__(self, transient, settings, services, bouquet, fav_model, bouquet_name):
def __init__(self, app, bouquet, bouquet_name):
handlers = {"on_close_dialog": self.on_close_dialog,
"on_apply": self.on_apply,
@@ -80,12 +80,14 @@ class EpgDialog:
"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_press": self.on_key_press}
"on_key_press": self.on_key_press,
"on_bq_cursor_changed": self.on_bq_cursor_changed}
self._app = app
self._services = {}
self._ex_services = services
self._ex_fav_model = fav_model
self._settings = settings
self._ex_services = self._app.current_services
self._ex_fav_model = self._app.fav_view.get_model()
self._settings = self._app.app_settings
self._bouquet = bouquet
self._bouquet_name = bouquet_name
self._current_ref = []
@@ -98,7 +100,7 @@ class EpgDialog:
builder = get_builder(UI_RESOURCES_PATH + "epg.glade", handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(transient)
self._dialog.set_transient_for(self._app.app_window)
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")
@@ -110,8 +112,8 @@ class EpgDialog:
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
self._filter_entry = builder.get_object("filter_entry")
self._filter_auto_switch = builder.get_object("filter_auto_switch")
self._services_filter_model = builder.get_object("services_filter_model")
self._services_filter_model.set_visible_func(self.services_filter_function)
# Info
@@ -142,6 +144,8 @@ class EpgDialog:
header_bar.pack_end(right_box)
builder.get_object("toolbar_box").set_visible(False)
self._app.connect("epg-dat-downloaded", self.on_epg_dat_downloaded)
# Setting the last size of the dialog window
window_size = self._settings.get("epg_tool_window_size")
if window_size:
@@ -177,8 +181,11 @@ class EpgDialog:
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)
if self._update_epg_data_on_start:
self.download_epg_from_stb()
else:
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_data(self):
self._services_model.clear()
@@ -194,23 +201,15 @@ class EpgDialog:
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(f"Download epg.dat file error: {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:
except (OSError, ValueError) as e:
self.show_info_message(f"Read data error: {e}", Gtk.MessageType.ERROR)
return
yield True
if self._refs_source is RefsSource.SERVICES:
self.init_lamedb_source(refs)
yield from self.init_lamedb_source(refs)
elif self._refs_source is RefsSource.XML:
xml_gen = self.init_xml_source(refs)
try:
@@ -235,8 +234,15 @@ class EpgDialog:
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.pos, s.fav_id), filtered)))
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(filtered):
self._services_model.append((srv.service, srv.pos, srv.fav_id))
if index % factor == 0:
yield True
self.update_source_count_info()
yield True
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()
@@ -305,7 +311,13 @@ class EpgDialog:
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))
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(s_refs):
self._services_model.append((srv.name, " ", srv.data))
if index % factor == 0:
yield True
self.update_source_info(info)
self.update_source_count_info()
yield True
@@ -323,6 +335,13 @@ class EpgDialog:
elif ctrl and key is KeyboardKey.V:
self.on_assign_ref()
def on_bq_cursor_changed(self, view):
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
path, column = view.get_cursor()
model = view.get_model()
if path:
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
@@ -403,12 +422,16 @@ class EpgDialog:
self._services[new_fav_id] = service
row[Column.FAV_ID] = new_fav_id
row[Column.FAV_LOCKED] = EPG_ICON
src = f"{get_message('EPG source')}: {data[0]} ({data[1]})"
pos = f"({data[1] if self._refs_source is RefsSource.SERVICES else 'XML'})"
src = f"{get_message('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
row[Column.FAV_TOOLTIP] = f"{get_message('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
def on_filter_toggled(self, button: Gtk.ToggleButton):
self._filter_bar.set_search_mode(button.get_active())
def on_filter_toggled(self, button):
self._filter_bar.set_visible(button.get_active())
if not button.get_active():
self._filter_entry.set_text("")
@run_with_delay(1)
def on_filter_changed(self, entry):
self._services_filter_model.refilter()
@@ -582,10 +605,19 @@ class EpgDialog:
# ***************** Downloads *********************#
def on_epg_dat_downloaded(self, app, value):
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@run_task
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
try:
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
except Exception as e:
GLib.idle_add(self.show_info_message, f"Download epg.dat file error: {e}", Gtk.MessageType.ERROR)
else:
GLib.idle_add(self._app.emit, "epg-dat-downloaded", None)
if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -47,6 +47,11 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="icon_name">folder-new</property>
</object>
<object class="GtkImage" id="file_download_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-transmit</property>
</object>
<object class="GtkListStore" id="file_list_store">
<columns>
<!-- column-name icon -->
@@ -68,6 +73,11 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="icon_name">folder-new</property>
</object>
<object class="GtkImage" id="ftp_download_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-receive</property>
</object>
<object class="GtkListStore" id="ftp_list_store">
<columns>
<!-- column-name icon -->
@@ -84,116 +94,59 @@ Author: Dmitriy Yefremov
<column type="gchararray"/>
</columns>
</object>
<object class="GtkFrame" id="main_frame">
<object class="GtkBox" id="main_ftp_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<property name="margin_top">2</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="main_ftp_box">
<object class="GtkPaned" id="paned">
<property name="width_request">320</property>
<property name="height_request">240</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="can_focus">True</property>
<property name="position_set">True</property>
<property name="wide_handle">True</property>
<signal name="size-allocate" handler="on_paned_size_allocate" swapped="no"/>
<child>
<object class="GtkBox" id="ftp_button_box">
<object class="GtkFrame" id="ftp_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="bookmark_button">
<property name="can_focus">False</property>
<property name="model">bookmarks_list_store</property>
<property name="id_column">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkPaned" id="paned">
<property name="width_request">320</property>
<property name="height_request">240</property>
<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="wide_handle">True</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="ftp_bpx">
<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">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="ftp_info_box">
<object class="GtkBox" id="ftp_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel" id="ftp_label">
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">FTP:</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -202,12 +155,93 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkLabel" id="ftp_info_label">
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="ftp_actions_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">75</property>
<property name="yalign">1</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="ftp_add_folder_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add folder</property>
<signal name="clicked" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
<child>
<object class="GtkImage" id="ftp_add_folder_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-new-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ftp_edit_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Edit</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_ftp_edit" swapped="no"/>
<child>
<object class="GtkImage" id="ftp_edit_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-edit-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ftp_remove_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_ftp_remove" swapped="no"/>
<child>
<object class="GtkImage" id="ftp_remove_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -215,6 +249,19 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="bookmark_button">
<property name="can_focus">False</property>
<property name="model">bookmarks_list_store</property>
<property name="id_column">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -269,7 +316,7 @@ Author: Dmitriy Yefremov
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
<signal name="edited" handler="on_ftp_edited" swapped="no"/>
<signal name="edited" handler="on_ftp_renamed" swapped="no"/>
</object>
<attributes>
<attribute name="text">1</attribute>
@@ -344,37 +391,22 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="file_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="pc_info_box">
<object class="GtkBox">
<property name="height_request">24</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="pc_label">
<object class="GtkImage" id="ftp_info_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_left">10</property>
<property name="label" translatable="yes">PC:</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
<property name="icon_name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -383,11 +415,104 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkLabel" id="pc_info_label">
<object class="GtkLabel" id="ftp_info_label">
<property name="height_request">16</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">75</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="ftp_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">FTP</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">False</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="pc_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="file_box">
<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="GtkBox" id="pc_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="pc_add_folder_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add folder</property>
<signal name="clicked" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
<child>
<object class="GtkImage" id="pc_add_folder_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-new-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="pc_remove_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_file_remove" swapped="no"/>
<child>
<object class="GtkImage" id="pc_remove_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -448,7 +573,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkCellRendererText" id="file_name_column_renderer">
<property name="ellipsize">end</property>
<signal name="edited" handler="on_file_edited" swapped="no"/>
<signal name="edited" handler="on_file_renamed" swapped="no"/>
</object>
<attributes>
<attribute name="text">1</attribute>
@@ -523,27 +648,64 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="height_request">24</property>
<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="spacing">5</property>
<child>
<object class="GtkLabel" id="pc_info_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="pc_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">PC</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="resize">True</property>
<property name="shrink">False</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="main_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">FTP client</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="GtkImage" id="remove_image">
@@ -559,11 +721,28 @@ Author: Dmitriy Yefremov
<object class="GtkImage" id="rename_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">gtk-edit</property>
<property name="icon_name">edit-find-replace</property>
</object>
<object class="GtkMenu" id="ftp_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="ftp_download_menu_item">
<property name="label" translatable="yes">Receive</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">ftp_download_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_copy" swapped="no"/>
<accelerator key="F5" signal="activate"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_create_folder_menu_item">
<property name="label" translatable="yes">Create folder</property>
@@ -577,12 +756,23 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_edit_menu_item">
<property name="label" translatable="yes">Edit</property>
<property name="label">gtk-edit</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_ftp_edit" swapped="no"/>
<accelerator key="F4" signal="activate"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_rename_menu_item">
<property name="label" translatable="yes">Rename</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">rename_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_edit" object="ftp_name_column_renderer" swapped="no"/>
<signal name="activate" handler="on_ftp_rename" object="ftp_name_column_renderer" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
<accelerator key="F2" signal="activate"/>
</object>
@@ -608,11 +798,28 @@ Author: Dmitriy Yefremov
<object class="GtkImage" id="rename_image_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">gtk-edit</property>
<property name="icon_name">edit-find-replace</property>
</object>
<object class="GtkMenu" id="file_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="file_download_menu_item">
<property name="label" translatable="yes">Send</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">file_download_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_copy" swapped="no"/>
<accelerator key="F5" signal="activate"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="file_create_folder_menu_item">
<property name="label" translatable="yes">Create folder</property>
@@ -625,13 +832,13 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="file_edit_menu_item">
<property name="label" translatable="yes">Edit</property>
<object class="GtkImageMenuItem" id="file_rename_menu_item">
<property name="label" translatable="yes">Rename</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">rename_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_edit" object="file_name_column_renderer" swapped="no"/>
<signal name="activate" handler="on_file_rename" object="file_name_column_renderer" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
<accelerator key="F2" signal="activate"/>
</object>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,6 +32,7 @@ from collections import namedtuple
from datetime import datetime
from enum import IntEnum
from ftplib import all_errors
from io import TextIOWrapper, BytesIO
from pathlib import Path
from shutil import rmtree
from urllib.parse import urlparse, unquote
@@ -40,9 +41,10 @@ from gi.repository import GLib
from app.commons import log, run_task, run_idle
from app.connections import UtfFTP
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
@@ -63,6 +65,86 @@ class FtpClientBox(Gtk.HBox):
ATTR = 4
EXTRA = 5
class TextEditDialog(Gtk.Dialog):
""" Simple text edit dialog. """
def __init__(self, path, use_header_bar=0, *args, **kwargs):
super().__init__(title=f"DemonEditor [{path}]", use_header_bar=use_header_bar, *args, **kwargs)
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK, )
content_box = self.get_content_area()
self._search_entry = Gtk.SearchEntry(visible=True, primary_icon_name="system-search-symbolic")
self._search_entry.connect("search-changed", self.on_search_changed)
if use_header_bar:
bar = self.get_header_bar()
bar.pack_start(self._search_entry)
bar.set_title("DemonEditor")
bar.set_subtitle(path)
else:
search_bar = Gtk.SearchBar(visible=True)
search_bar.add(self._search_entry)
search_bar.set_search_mode(True)
content_box.pack_start(search_bar, False, False, 0)
scrolled_window = Gtk.ScrolledWindow(hexpand=True, vexpand=True,
min_content_width=720,
min_content_height=320)
content_box.pack_start(scrolled_window, True, True, 0)
try:
import gi
gi.require_version("GtkSource", "3.0")
from gi.repository import GtkSource
except (ImportError, ValueError) as e:
self._text_view = Gtk.TextView()
self._buf = self._text_view.get_buffer()
log(e)
else:
self._text_view = GtkSource.View(show_line_numbers=True, show_line_marks=True)
self._buf = self._text_view.get_buffer()
self._buf.set_highlight_syntax(True)
self._buf.set_highlight_matching_brackets(True)
lang_manager = GtkSource.LanguageManager.new()
self._buf.set_language(lang_manager.guess_language(path))
# Style
self._buf.set_style_scheme(GtkSource.StyleSchemeManager().get_default().get_scheme("tango"))
self._tag_found = self._buf.create_tag("found", background="yellow")
scrolled_window.add(self._text_view)
self.show_all()
@property
def text(self):
return self._buf.get_text(self._buf.get_start_iter(), self._buf.get_end_iter(), include_hidden_chars=True)
@text.setter
def text(self, value):
self._buf.set_text(value)
def on_search_changed(self, entry):
self._buf.remove_tag(self._tag_found, self._buf.get_start_iter(), self._buf.get_end_iter())
cursor_mark = self._buf.get_insert()
start = self._buf.get_iter_at_mark(cursor_mark)
if start.get_offset() == self._buf.get_char_count():
start = self._buf.get_start_iter()
self.search_and_mark(entry.get_text(), start)
def search_and_mark(self, text, start, first=True):
end = self._buf.get_end_iter()
match = start.forward_search(text, 0, end)
if match is not None:
match_start, match_end = match
self._buf.apply_tag(self._tag_found, match_start, match_end)
if first:
self._text_view.scroll_to_iter(match_start, 0.0, False, 0.0, 0.0)
GLib.idle_add(self.search_and_mark, text, match_end, False)
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_spacing(2)
@@ -78,9 +160,12 @@ class FtpClientBox(Gtk.HBox):
"on_ftp_row_activated": self.on_ftp_row_activated,
"on_file_row_activated": self.on_file_row_activated,
"on_ftp_edit": self.on_ftp_edit,
"on_ftp_edited": self.on_ftp_edited,
"on_file_edit": self.on_file_edit,
"on_file_edited": self.on_file_edited,
"on_ftp_rename": self.on_ftp_rename,
"on_ftp_renamed": self.on_ftp_renamed,
"on_ftp_copy": self.on_ftp_copy,
"on_file_rename": self.on_file_rename,
"on_file_renamed": self.on_file_renamed,
"on_file_copy": self.on_file_copy,
"on_file_remove": self.on_file_remove,
"on_ftp_remove": self.on_ftp_file_remove,
"on_file_create_folder": self.on_file_create_folder,
@@ -94,11 +179,12 @@ class FtpClientBox(Gtk.HBox):
"on_view_popup_menu": on_popup_menu,
"on_view_key_press": self.on_view_key_press,
"on_view_press": self.on_view_press,
"on_view_release": self.on_view_release}
"on_view_release": self.on_view_release,
"on_paned_size_allocate": self.on_paned_size_allocate}
builder = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
self.add(builder.get_object("main_frame"))
self.add(builder.get_object("main_ftp_box"))
self._ftp_info_label = builder.get_object("ftp_info_label")
self._ftp_view = builder.get_object("ftp_view")
self._ftp_model = builder.get_object("ftp_list_store")
@@ -109,8 +195,10 @@ class FtpClientBox(Gtk.HBox):
# Buttons
self._connect_button = builder.get_object("connect_button")
disconnect_button = builder.get_object("disconnect_button")
disconnect_button.bind_property("visible", builder.get_object("ftp_actions_box"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_create_folder_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_edit_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_rename_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
# Force Ctrl
@@ -219,7 +307,7 @@ class FtpClientBox(Gtk.HBox):
else:
r_size = self.get_size_from_bytes(size)
date = "{}, {} {}".format(f_data[5], f_data[6], f_data[7])
date = f"{f_data[5]}, {f_data[6]} {f_data[7]}"
self._ftp_model.append(File(icon, " ".join(f_data[8:]), r_size, date, f_data[0], size))
def on_connect(self, item=None):
@@ -259,23 +347,22 @@ class FtpClientBox(Gtk.HBox):
def open_file(self, path):
GLib.idle_add(self._file_view.set_sensitive, False)
try:
cmd = ["open" if self._settings.is_darwin else "xdg-open", path]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
cmd = [self.get_open_file_cmd(), path]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
finally:
GLib.idle_add(self._file_view.set_sensitive, True)
@run_task
def open_ftp_file(self, f_path):
is_darwin = self._settings.is_darwin
GLib.idle_add(self._ftp_view.set_sensitive, False)
try:
import tempfile
import os
path = os.path.expanduser("~/Desktop") if is_darwin else None
path = os.path.expanduser("~/Desktop") if not IS_LINUX else None
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=not is_darwin) as tf:
msg = "Downloading file: {}. Status: {}"
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=IS_LINUX) as tf:
msg = "Downloading file: {}. Status: {}"
try:
status = self._ftp.retrbinary("RETR " + f_path, tf.write)
self.update_ftp_info(msg.format(f_path, status))
@@ -283,12 +370,74 @@ class FtpClientBox(Gtk.HBox):
self.update_ftp_info(msg.format(f_path, e))
tf.flush()
cmd = ["open" if is_darwin else "xdg-open", tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
cmd = [self.get_open_file_cmd(), tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
finally:
GLib.idle_add(self._ftp_view.set_sensitive, True)
def on_ftp_edit(self, renderer):
@staticmethod
def get_open_file_cmd():
if IS_DARWIN:
return "open"
elif IS_WIN:
return "start"
return "xdg-open"
@run_task
def on_ftp_edit(self, item=None):
path = self.get_ftp_edit_path()
if path:
row = self._ftp_model[path]
f_path = row[self.Column.NAME]
size = row[self.Column.SIZE]
if size == self.FOLDER or f_path == self.ROOT:
self._app.show_error_message("Not allowed in this context!")
else:
b_size = row[self.Column.EXTRA]
if b_size.isdigit() and int(b_size) > self.MAX_SIZE / 5:
self._app.show_error_message("The file size is too large!")
else:
msg = "Retrieving file: {}. Status: {}"
io = BytesIO()
try:
status = self._ftp.retrbinary("RETR " + f_path, io.write)
self.update_ftp_info(msg.format(f_path, status))
except all_errors as e:
self.update_ftp_info(msg.format(f_path, e))
else:
io.seek(0)
self.show_edit_dialog(f_path, TextIOWrapper(io, errors="ignore").read())
def on_ftp_edited(self, f_path, txt_data):
buf = BytesIO()
buf.write(txt_data.encode())
buf.seek(0)
msg = "Uploading file: {}. Status: {}"
try:
status = self._ftp.storbinary(f"STOR {f_path}", buf)
self.update_ftp_info(msg.format(f_path, status))
except all_errors as e:
self.update_ftp_info(msg.format(f_path, e))
@run_idle
def show_edit_dialog(self, f_path, data):
dialog = self.TextEditDialog(f_path, IS_GNOME_SESSION)
dialog.text = data
ok = Gtk.ResponseType.OK
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
self.on_ftp_edited(f_path, dialog.text)
dialog.destroy()
def on_ftp_rename(self, renderer):
path = self.get_ftp_edit_path()
if path:
renderer.set_property("editable", True)
self._ftp_view.set_cursor(path, self._ftp_view.get_column(0), True)
def get_ftp_edit_path(self):
model, paths = self._ftp_view.get_selection().get_selected_rows()
if not paths:
return
@@ -296,11 +445,9 @@ class FtpClientBox(Gtk.HBox):
if len(paths) > 1:
self._app.show_error_message("Please, select only one item!")
return
return paths
renderer.set_property("editable", True)
self._ftp_view.set_cursor(paths, self._ftp_view.get_column(0), True)
def on_ftp_edited(self, renderer, path, new_value):
def on_ftp_renamed(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._ftp_model[path]
old_name = row[self.Column.NAME]
@@ -308,11 +455,11 @@ class FtpClientBox(Gtk.HBox):
return
resp = self._ftp.rename_file(old_name, new_value)
self.update_ftp_info("{} Status: {}".format(old_name, resp))
self.update_ftp_info(f"{old_name} Status: {resp}")
if resp[0] == "2":
row[self.Column.NAME] = new_value
def on_file_edit(self, renderer):
def on_file_rename(self, renderer):
model, paths = self._file_view.get_selection().get_selected_rows()
if len(paths) > 1:
self._app.show_error_message("Please, select only one item!")
@@ -321,7 +468,7 @@ class FtpClientBox(Gtk.HBox):
renderer.set_property("editable", True)
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
def on_file_edited(self, renderer, path, new_value):
def on_file_renamed(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._file_model[path]
old_name = row[self.Column.NAME]
@@ -331,7 +478,7 @@ class FtpClientBox(Gtk.HBox):
path = Path(row[self.Column.ATTR])
if path.exists():
try:
new_path = path.rename("{}/{}".format(path.parent, new_value))
new_path = path.rename(f"{path.parent}/{new_value}")
except ValueError as e:
log(e)
self._app.show_error_message(str(e))
@@ -340,8 +487,16 @@ class FtpClientBox(Gtk.HBox):
row[self.Column.NAME] = new_value
row[self.Column.ATTR] = str(new_path.resolve())
def on_file_copy(self, item=None):
uris = self.get_file_uris()
self.copy_to_ftp(uris) if uris else None
def on_ftp_copy(self, item=None):
uris = self.get_ftp_uris()
self.copy_to_pc(uris) if uris else None
def on_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
model, paths = self._file_view.get_selection().get_selected_rows()
@@ -359,7 +514,7 @@ class FtpClientBox(Gtk.HBox):
list(map(model.remove, to_delete))
def on_ftp_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
model, paths = self._ftp_view.get_selection().get_selected_rows()
@@ -385,7 +540,7 @@ class FtpClientBox(Gtk.HBox):
name = self.get_new_folder_name(self._file_model)
cur_path = self._file_model.get_value(itr, self.Column.ATTR)
path = Path("{}/{}".format(cur_path, name))
path = Path(f"{cur_path}/{name}")
try:
path.mkdir()
@@ -406,13 +561,13 @@ class FtpClientBox(Gtk.HBox):
name = self.get_new_folder_name(self._ftp_model)
try:
folder = "{}/{}".format(cur_path, name)
folder = f"{cur_path}/{name}"
resp = self._ftp.mkd(folder)
except all_errors as e:
self.update_ftp_info(str(e))
log(e)
else:
if resp == "{}/{}".format(cur_path, name):
if resp == f"{cur_path}/{name}":
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
renderer.set_property("editable", True)
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)
@@ -424,7 +579,7 @@ class FtpClientBox(Gtk.HBox):
count = 0
while name in names:
count += 1
name = "{}{}".format(name, count)
name = f"{name}{count}"
return name
# ***************** Drag-and-drop ********************* #
@@ -453,30 +608,43 @@ class FtpClientBox(Gtk.HBox):
return True
def on_ftp_drag_data_get(self, view, context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
uris = self.get_ftp_uris()
data.set_uris(uris) if uris else None
def get_ftp_uris(self):
""" Returns the selected paths in FTP view as a list containing uris string or None. """
model, paths = self._ftp_view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = []
for r in [model[p][:] for p in paths]:
if r[self.Column.SIZE] != self.LINK and r[self.Column.NAME] != self.ROOT:
uris.append(Path("/{}:{}".format(r[self.Column.NAME], r[self.Column.ATTR])).as_uri())
data.set_uris([sep.join(uris)])
path = Path(f"/{r[self.Column.NAME]}:{r[self.Column.ATTR]}")
uris.append(str(path.resolve()) if IS_WIN else path.as_uri())
return [sep.join(uris)]
@run_task
def on_ftp_drag_data_received(self, view, context, x, y, data: Gtk.SelectionData, info, time):
if not self._ftp:
return
self.copy_to_ftp(data.get_uris())
Gtk.drag_finish(context, True, False, time)
return True
@run_task
def copy_to_ftp(self, uris):
resp = "2"
try:
GLib.idle_add(self._app.wait_dialog.show)
uris = data.get_uris()
if self._settings.is_darwin and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
if len(uris) == 1:
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
for uri in uris:
uri = urlparse(unquote(uri)).path
if IS_WIN:
uri = uri.lstrip("/")
path = Path(uri)
if path.is_dir():
try:
@@ -484,34 +652,40 @@ class FtpClientBox(Gtk.HBox):
except all_errors as e:
pass # NOP
self._ftp.cwd(path.name)
resp = self._ftp.upload_dir(str(path.resolve()) + "/", self.update_ftp_info)
resp = self._ftp.upload_dir(str(path.resolve()) + SEP, self.update_ftp_info)
else:
resp = self._ftp.send_file(path.name, str(path.parent) + "/", callback=self.update_ftp_info)
resp = self._ftp.send_file(path.name, str(path.parent) + SEP, callback=self.update_ftp_info)
finally:
GLib.idle_add(self._app.wait_dialog.hide)
if resp and resp[0] == "2":
itr = self._ftp_model.get_iter_first()
if itr:
self.init_ftp_data(self._ftp_model.get_value(itr, self.Column.ATTR))
def on_file_drag_data_get(self, view, context, data, info, time):
uris = self.get_file_uris()
data.set_uris(uris) if uris else None
def get_file_uris(self):
""" Returns the selected paths in the file view as a list containing uris string or None. """
model, paths = self._file_view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
return [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
self.copy_to_pc(data.get_uris())
Gtk.drag_finish(context, True, False, time)
return True
def on_file_drag_data_get(self, view, context, data: Gtk.SelectionData, info, time):
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
data.set_uris(uris)
@run_task
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
def copy_to_pc(self, uris):
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + "/"
try:
GLib.idle_add(self._app.wait_dialog.show)
uris = data.get_uris()
if self._settings.is_darwin and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
if len(uris) == 1:
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
for uri in uris:
name, sep, attr = unquote(Path(uri).name).partition(":")
@@ -528,9 +702,6 @@ class FtpClientBox(Gtk.HBox):
GLib.idle_add(self._app.wait_dialog.hide)
self.init_file_data(cur_path)
Gtk.drag_finish(context, True, False, time)
return True
def on_view_drag_end(self, view, context):
self._select_enabled = True
view.get_selection().unselect_all()
@@ -556,9 +727,17 @@ class FtpClientBox(Gtk.HBox):
self.on_file_create_folder(self._file_name_renderer)
elif key is KeyboardKey.F2 or ctrl and KeyboardKey.R:
if self._ftp_view.is_focus():
self.on_ftp_edit(self._ftp_name_renderer)
self.on_ftp_rename(self._ftp_name_renderer)
elif self._file_view.is_focus():
self.on_file_edit(self._file_name_renderer)
self.on_file_rename(self._file_name_renderer)
elif key is KeyboardKey.F4:
if self._ftp_view.is_focus():
self.on_ftp_edit()
elif key is KeyboardKey.F5:
if self._ftp_view.is_focus():
self.on_ftp_copy()
elif self._file_view.is_focus():
self.on_file_copy()
elif key is KeyboardKey.DELETE:
if self._ftp_view.is_focus():
self.on_ftp_file_remove()
@@ -581,6 +760,10 @@ class FtpClientBox(Gtk.HBox):
# Enable selection.
self._select_enabled = True
def on_paned_size_allocate(self, paned, allocation):
""" Sets default homogeneous sizes. """
paned.set_position(0.5 * allocation.width)
def get_size_from_bytes(self, size):
""" Simple convert function from bytes to other units like K, M or G. """
try:
@@ -593,11 +776,11 @@ class FtpClientBox(Gtk.HBox):
if b < kb:
return str(b)
elif kb <= b < mb:
return "{0:.1f} K".format(b / kb)
return f"{b / kb:.1f} K"
elif mb <= b < gb:
return "{0:.1f} M".format(b / mb)
return f"{b / mb:.1f} M"
elif gb <= b:
return "{0:.1f} G".format(b / gb)
return f"{b / gb:.1f} G"
if __name__ == '__main__':

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from contextlib import suppress
from pathlib import Path
@@ -5,7 +33,7 @@ from app.commons import run_idle, log
from app.eparser import get_bouquets, get_services, BouquetsReader
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.settings import SettingsType
from app.settings import SettingsType, IS_DARWIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
@@ -19,8 +47,8 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
profile = settings.setting_type
if profile is SettingsType.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "userbouquet.*{}".format(pattern)
pattern = f".{bq_type.value}"
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
elif profile is SettingsType.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
@@ -38,6 +66,10 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
return
if profile is SettingsType.ENIGMA_2:
if IS_DARWIN and file_path.rfind("userbouquet.") < 0:
show_dialog(DialogType.ERROR, transient, text="Not allowed in this context!")
return
bq = get_enigma2_bouquet(file_path)
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
@@ -55,7 +87,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
else:
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
file_path = "{}/".format(Path(file_path).parent)
file_path = f"{Path(file_path).parent}{SEP}"
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()

View File

@@ -1253,6 +1253,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="width_chars">5</property>
<property name="max_width_chars">4</property>
<property name="text">0</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>

View File

@@ -630,7 +630,7 @@ class M3uImportDialog(IptvListDialog):
super().__init__(transient, s_type)
self._app = app
self._picons = app._picons
self._picons = app.picons
self._pic_path = app._settings.profile_picons_path
self._services = None
self._url_count = 0
@@ -802,7 +802,7 @@ class M3uImportDialog(IptvListDialog):
def update_fav_model(self):
services = self._app.current_services
picons = self._app._picons
picons = self._app.picons
model = self._app.fav_view.get_model()
for r in model:
s = services.get(r[Column.FAV_ID], None)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,14 +29,15 @@
""" Helper module for the GUI. """
__all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "locate_in_services",
"scroll_to", "get_base_model", "update_picons_data", "copy_picon_reference", "assign_picons",
"remove_picon", "is_only_one_item_selected", "gen_bouquets", "BqGenType", "append_picons",
"get_selection", "get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs",
"get_iptv_url", "update_entry_data", "append_text_to_tview", "on_popup_menu")
"scroll_to", "get_base_model", "copy_picon_reference", "assign_picons", "remove_picon",
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection",
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
"update_entry_data", "append_text_to_tview", "on_popup_menu")
import os
import shutil
from collections import defaultdict
from pathlib import Path
from urllib.parse import unquote
from gi.repository import GdkPixbuf, GLib
@@ -44,8 +45,8 @@ from gi.repository import GdkPixbuf, GLib
from app.eparser import Service
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from app.settings import SettingsType, SEP, IS_WIN
from .dialogs import show_dialog, DialogType, get_chooser_dialog
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
from .dialogs import show_dialog, DialogType, get_message
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
@@ -64,7 +65,7 @@ def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_t
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
fav_id = f"1:64:0:0:0:0:0:0:0:0::{response}\n#DESCRIPTION {response}\n"
text = response
s_type = m_type.name
@@ -110,8 +111,8 @@ def move_items(key, view: Gtk.TreeView):
children_num = model.iter_n_children(parent_itr)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
children_num -= 1
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0))
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num))
min_path = Gtk.TreePath.new_from_string(f"{parent_index}:{0}")
max_path = Gtk.TreePath.new_from_string(f"{parent_index}:{children_num}")
is_tree_store = True
if key is KeyboardKey.UP:
@@ -281,7 +282,7 @@ def set_lock(blacklist, services, model, paths, target, services_model):
locked = has_locked_hide(model, paths, col_num)
ids = []
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name}
for path in paths:
itr = model.get_iter(path)
@@ -386,26 +387,36 @@ def scroll_to(index, view, paths=None):
selection.select_path(index)
# ***************** Picons *********************#
# ***************** Picons ********************* #
def update_picons_data(path, picons, size=32):
if not os.path.exists(path):
return
def get_picon_dialog(transient, title, button_text, multiple=True):
""" Returns a copy dialog with a preview of images [picons -> *.png]. """
dialog = Gtk.FileChooserNative.new(title, transient, Gtk.FileChooserAction.OPEN, button_text)
dialog.set_select_multiple(multiple)
dialog.set_modal(True)
# Filter.
file_filter = Gtk.FileFilter()
file_filter.set_name("*.png")
file_filter.add_pattern("*.png")
file_filter.add_mime_type("image/png") if IS_DARWIN else None
dialog.add_filter(file_filter)
for file in os.listdir(path):
pf = get_picon_pixbuf(path + file, size)
if pf:
picons[file] = pf
if IS_LINUX:
preview_image = Gtk.Image(margin_right=10)
dialog.set_preview_widget(preview_image)
def update_preview_widget(dlg):
path = dialog.get_preview_filename()
if not path:
return
def append_picons(picons, model):
def append_picons_data(pcs, mod):
for r in mod:
mod.set_value(mod.get_iter(r.path), Column.SRV_PICON, pcs.get(r[Column.SRV_PICON_ID], None))
yield True
pix = get_picon_pixbuf(path, 220)
preview_image.set_from_pixbuf(pix)
dlg.set_preview_widget_active(bool(pix))
app = append_picons_data(picons, model)
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
dialog.connect("update-preview", update_preview_widget)
return dialog
def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
@@ -415,10 +426,12 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picons_files = []
if not src_path:
src_path = get_chooser_dialog(transient, settings, "*.png files", ("*.png",))
if src_path == Gtk.ResponseType.CANCEL:
dialog = get_picon_dialog(transient, get_message("Picon selection"), get_message("Open"), False)
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) or not dialog.get_filenames():
return picons_files
src_path = dialog.get_filenames()[0]
if IS_WIN:
src_path = src_path.lstrip("/")
dst_path = dst_path.lstrip("/") if dst_path else dst_path
@@ -450,7 +463,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
pass # NOP
else:
picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file)
picon = get_picon_pixbuf(picon_file, settings.list_picon_size)
picons[picon_id] = picon
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
@@ -531,15 +544,17 @@ def copy_picon_reference(target, view, services, clipboard, transient):
show_dialog(DialogType.ERROR, transient, "No reference is present!")
def remove_all_unused_picons(settings, picons, services):
def remove_all_unused_picons(settings, services):
""" Removes picons from profile picons folder if there are no services for these picons. """
ids = {s.picon_id for s in services}
pcs = list(filter(lambda x: x not in ids, picons))
remove_picons(settings, pcs, picons)
for p in Path(settings.profile_picons_path).glob("*.png"):
if p.name not in ids and p.is_file():
p.unlink()
def remove_picons(settings, picon_ids, picons):
pions_path = settings.profile_picons_path
backup_path = "{}{}{}".format(settings.profile_backup_path, "picons", SEP)
backup_path = f"{settings.profile_backup_path}picons{SEP}"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2021 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -609,6 +609,7 @@ Author: Dmitriy Yefremov
<property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<property name="placeholder_text" translatable="yes">Filter: N1|N2|N3, etc..</property>
<signal name="search-changed" handler="on_picons_filter_changed" swapped="no"/>
</object>
<packing>
@@ -707,6 +708,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
<property name="model">picons_src_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">0</property>
<property name="activate_on_single_click">True</property>
@@ -722,11 +724,15 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="src_picon_column">
<property name="sizing">fixed</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Picon</property>
<property name="alignment">0.49000000953674316</property>
<child>
<object class="GtkCellRendererPixbuf" id="picons_src_renderer"/>
<object class="GtkCellRendererPixbuf" id="picons_src_renderer">
<property name="height">50</property>
<property name="ypad">5</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
@@ -735,6 +741,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="src_title_column">
<property name="sizing">fixed</property>
<property name="title" translatable="yes">Name</property>
<property name="alignment">0.49000000953674316</property>
<child>
@@ -750,6 +757,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="src_path_column">
<property name="visible">False</property>
<property name="sizing">fixed</property>
<property name="title" translatable="yes">column</property>
<child>
<object class="GtkCellRendererText" id="path_src_renderer"/>
@@ -835,6 +843,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services. </property>
<property name="model">picons_dst_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">0</property>
<property name="activate_on_single_click">True</property>
@@ -851,11 +860,15 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="dest_picon_column">
<property name="sizing">fixed</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Picon</property>
<property name="alignment">0.49000000953674316</property>
<child>
<object class="GtkCellRendererPixbuf" id="picons_dest_renderer"/>
<object class="GtkCellRendererPixbuf" id="picons_dest_renderer">
<property name="height">50</property>
<property name="ypad">5</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
@@ -864,6 +877,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="dest_title_column">
<property name="sizing">fixed</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.49000000953674316</property>
@@ -880,6 +894,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="dest_path_column">
<property name="visible">False</property>
<property name="sizing">fixed</property>
<property name="title" translatable="yes">column</property>
<child>
<object class="GtkCellRendererText" id="path_dest_renderer"/>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -37,14 +37,14 @@ from gi.repository import GLib, GdkPixbuf, Gio
from app.commons import run_idle, run_task, run_with_delay, log
from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType, Settings, SEP, IS_LINUX, IS_DARWIN
from app.settings import SettingsType, Settings, SEP, IS_DARWIN
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
PiconsError)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message, get_builder, get_chooser_dialog
from .main_helper import (update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon,
get_picon_pixbuf)
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page
get_picon_pixbuf, get_picon_dialog)
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
class PiconManager(Gtk.Box):
@@ -59,6 +59,7 @@ class PiconManager(Gtk.Box):
self._app.connect("page-changed", self.update_picons_dest)
self._app.connect("filter-toggled", self.on_app_filter_toggled)
self._app.connect("profile-changed", self.on_profile_changed)
self._app.connect("picon-assign", self.on_picon_assign)
self._app.fav_view.connect("row-activated", self.on_fav_changed)
self._picon_ids = picon_ids
self._sat_positions = sat_positions
@@ -196,6 +197,11 @@ class PiconManager(Gtk.Box):
self._manager_button.bind_property("active", builder.get_object("add_menu_button"), "visible")
# Init drag-and-drop
self.init_drag_and_drop()
# Rendering.
column = builder.get_object("dest_picon_column")
column.set_cell_data_func(builder.get_object("picons_dest_renderer"), self.picon_data_func)
column = builder.get_object("src_picon_column")
column.set_cell_data_func(builder.get_object("picons_src_renderer"), self.picon_data_func)
# Settings
self._settings = settings
self._s_type = settings.setting_type
@@ -250,6 +256,19 @@ class PiconManager(Gtk.Box):
self._current_path_label.set_text(self._settings.profile_picons_path)
self.update_picons_dest(app, self._app.page)
def on_picon_assign(self, app, target):
if target is ViewTarget.SERVICES:
model, paths = app.services_view.get_selection().get_selected_rows()
ids = {model[p][Column.SRV_FAV_ID] for p in paths}
else:
model, paths = app.fav_view.get_selection().get_selected_rows()
ids = {model[p][Column.FAV_ID] for p in paths}
self._filter_button.set_active(True)
self._dst_filter_button.set_active(True)
self._picons_filter_entry.set_text(
"|".join(s.service for f, s in self._app.current_services.items() if f in ids))
def update_picons_data(self, view, path=None):
if view is self._picons_dest_view:
self.update_picon_info()
@@ -260,7 +279,7 @@ class PiconManager(Gtk.Box):
def update_picons(self, path, view):
p_model = view.get_model()
model = get_base_model(p_model)
factor = self._app.DEL_FACTOR
factor = self._app.DEL_FACTOR * 2
for index, itr in enumerate([row.iter for row in model]):
model.remove(itr)
@@ -268,18 +287,12 @@ class PiconManager(Gtk.Box):
yield True
self._dst_count_label.set_text("0")
if not os.path.isdir(path):
return
os.makedirs(os.path.dirname(path), exist_ok=True)
for index, file in enumerate(os.listdir(path)):
if self._terminate:
return
p_path = f"{path}{SEP}{file}"
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
model.append((p, file, p_path))
model.append((None, file, f"{path}{SEP}{file}"))
if index % factor == 0:
self._dst_count_label.set_text(str(len(model)))
yield True
@@ -287,6 +300,9 @@ class PiconManager(Gtk.Box):
self._dst_count_label.set_text(str(len(model)))
yield True
def picon_data_func(self, column, renderer, model, itr, data):
renderer.set_property("pixbuf", self.get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
def update_picons_from_file(self, view, uri):
""" Adds picons in the view on dragging from file system. """
path = Path(urlparse(unquote(uri)).path.strip())
@@ -378,7 +394,7 @@ class PiconManager(Gtk.Box):
t_mod = target_view.get_model()
dest_path = self._settings.profile_picons_path
self.update_picons_dest_view(self._app.on_assign_picon(target_view, model[path][-1], dest_path))
self.update_picons_dest_view(self._app.on_assign_picon_file(target_view, model[path][-1], dest_path))
self.show_assign_info([t_mod.get_value(t_mod.get_iter_from_string(itr), c_id) for itr in itr_str.split(",")])
@run_idle
@@ -464,42 +480,12 @@ class PiconManager(Gtk.Box):
def on_add(self, item):
""" Adds (copies) picons from an external folder to the profile picons folder. """
dialog = self.get_copy_dialog()
dialog = get_picon_dialog(self._app_window, get_message("Add picons"), get_message("Add"))
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self.copy_picons_file(dialog.get_filenames())
def get_copy_dialog(self):
""" Returns a copy dialog with a preview of images [picons -> *.png]. """
dialog = Gtk.FileChooserNative.new(get_message("Add picons"), self._app_window,
Gtk.FileChooserAction.OPEN, get_message("Add"))
dialog.set_select_multiple(True)
dialog.set_modal(True)
# Filter.
file_filter = Gtk.FileFilter()
file_filter.set_name("*.png")
file_filter.add_pattern("*.png")
file_filter.add_mime_type("image/png") if IS_DARWIN else None
dialog.add_filter(file_filter)
if IS_LINUX:
preview_image = Gtk.Image(margin_right=10)
dialog.set_preview_widget(preview_image)
def update_preview_widget(dlg):
path = dialog.get_preview_filename()
if not path:
return
pix = get_picon_pixbuf(path, 220)
preview_image.set_from_pixbuf(pix)
dlg.set_preview_widget_active(bool(pix))
dialog.connect("update-preview", update_preview_widget)
return dialog
def on_extract(self, item):
""" Extracts picons from an archives to the profile picons folder. """
file_filter = None
@@ -563,6 +549,7 @@ class PiconManager(Gtk.Box):
filter_model = model.get_model()
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
base_model.remove(itr)
self._app.update_picons()
if view is self._picons_dest_view:
self._dst_count_label.set_text(str(len(model)))
@@ -891,15 +878,6 @@ class PiconManager(Gtk.Box):
self._is_downloading = False
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._terminate = True
self._is_downloading = False
self._app.update_picons()
GLib.idle_add(self._app_window.destroy)
@run_task
def run_func(self, func, update=False):
try:
@@ -957,10 +935,10 @@ class PiconManager(Gtk.Box):
@run_with_delay(0.5)
def on_picons_filter_changed(self, entry):
txt = entry.get_text().upper()
self._filter_cache.clear()
txt = entry.get_text().upper().split("|")
for s in self._app.current_services.values():
self._filter_cache[s.picon_id] = txt in s.service.upper()
self._filter_cache[s.picon_id] = any(t in s.service.upper() or t in str(s.picon_id) for t in txt)
GLib.idle_add(self._picons_src_filter_model.refilter, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._picons_dst_filter_model.refilter, priority=GLib.PRIORITY_LOW)

View File

@@ -1397,6 +1397,9 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<signal name="delete-event" handler="on_quit" swapped="no"/>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox" id="satellites_update_main_box">
<property name="visible">True</property>
@@ -1787,113 +1790,172 @@ Author: Dmitriy Yefremov
<object class="GtkFrame" id="sat_update_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="sat_update_scrolled_window">
<object class="GtkBox" id="sat_update_box">
<property name="visible">True</property>
<property name="can_focus">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="shadow_type">in</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkTreeView" id="sat_update_tree_view">
<object class="GtkScrolledWindow" id="sat_update_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_sat_list_model_sort</property>
<property name="search_column">0</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_popup_menu" object="satellites_update_popup_menu" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
</object>
</child>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeViewColumn" id="upd_satellite_column">
<property name="title" translatable="yes">Satellite</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="upd_satellite_cellrenderertext">
<property name="xalign">0.0099999997764825821</property>
<object class="GtkTreeView" id="sat_update_tree_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_sat_list_model_sort</property>
<property name="search_column">0</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_popup_menu" object="satellites_update_popup_menu" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_position_column">
<property name="title" translatable="yes">Position</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText" id="upd_position_cellrenderertext">
<property name="xalign">0.49000000953674316</property>
<object class="GtkTreeViewColumn" id="upd_satellite_column">
<property name="title" translatable="yes">Satellite</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="upd_satellite_cellrenderertext">
<property name="xalign">0.0099999997764825821</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_type_column">
<property name="title" translatable="yes">Type</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="GtkCellRendererText" id="upd_type_cellrenderertext">
<property name="xalign">0.49000000953674316</property>
<object class="GtkTreeViewColumn" id="upd_position_column">
<property name="title" translatable="yes">Position</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText" id="upd_position_cellrenderertext">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_url_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Url</property>
<child>
<object class="GtkCellRendererText" id="upd_url_cellrenderertext"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="clickable">True</property>
<property name="reorderable">True</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererToggle" id="upd_selected_cellrenderer">
<signal name="toggled" handler="on_satellite_toggled" swapped="no"/>
<object class="GtkTreeViewColumn" id="upd_type_column">
<property name="title" translatable="yes">Type</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="GtkCellRendererText" id="upd_type_cellrenderertext">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_url_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Url</property>
<child>
<object class="GtkCellRendererText" id="upd_url_cellrenderertext"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="clickable">True</property>
<property name="reorderable">True</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererToggle" id="upd_selected_cellrenderer">
<signal name="toggled" handler="on_satellite_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">4</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="active">4</attribute>
</attributes>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_update_status_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="satellites_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties-symbolic</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="satellites_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">2</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="sat_update_frame_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Satellites</property>
</object>
</child>
</object>
@@ -1903,190 +1965,318 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkFrame" id="sat_update_tr_frame">
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<object class="GtkPaned" id="sat_update_tr_paned">
<property name="can_focus">True</property>
<property name="orientation">vertical</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkPaned" id="sat_update_tr_paned">
<object class="GtkFrame" id="sat_update_tr_frame">
<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="orientation">vertical</property>
<property name="wide_handle">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="sat_update_tr_scrolled_window">
<object class="GtkBox" id="sat_update_tr_box">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</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">5</property>
<child>
<object class="GtkTreeView" id="sat_update_tr_view">
<object class="GtkScrolledWindow" id="sat_update_tr_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_transponder_store</property>
<property name="search_column">0</property>
<property name="activate_on_single_click">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeViewColumn" id="tr_view_tr_column">
<property name="title" translatable="yes">Transponder</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="upd_tr_renderer">
<property name="xalign">0.0099999997764825821</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
<object class="GtkTreeView" id="sat_update_tr_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_transponder_store</property>
<property name="search_column">0</property>
<property name="activate_on_single_click">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_link_column">
<property name="visible">False</property>
<property name="title" translatable="yes">link</property>
<child>
<object class="GtkCellRendererText" id="upd_tr_link_renderer"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_selected_column">
<property name="title" translatable="yes">Selected</property>
<child>
<object class="GtkCellRendererToggle" id="upd_tr_select_renderer">
<signal name="toggled" handler="on_transponder_toggled" swapped="no"/>
<object class="GtkTreeViewColumn" id="tr_view_tr_column">
<property name="title" translatable="yes">Transponder</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="upd_tr_renderer">
<property name="xalign">0.0099999997764825821</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_link_column">
<property name="visible">False</property>
<property name="title" translatable="yes">link</property>
<child>
<object class="GtkCellRendererText" id="upd_tr_link_renderer"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_selected_column">
<property name="title" translatable="yes">Selected</property>
<child>
<object class="GtkCellRendererToggle" id="upd_tr_select_renderer">
<signal name="toggled" handler="on_transponder_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_update_tr_status_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="transponders_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties-symbolic</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="transponders_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">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="sat_update_srv_scrolled_window">
<child type="label">
<object class="GtkLabel" id="sat_update_tr_frame_label">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="sat_update_srv_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_service_store</property>
<property name="search_column">1</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_servcie_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererPixbuf" id="srv_picon_renderer">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="srv_service_renderer">
<property name="xalign">0.0099999997764825821</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_package_column">
<property name="title" translatable="yes">Package</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_package_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_type_column">
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_type_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_sid_column">
<property name="title" translatable="yes">SID</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_sid_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_cas_column">
<property name="visible">False</property>
<property name="title" translatable="yes">CAS</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_cas_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
</child>
</object>
</child>
</object>
</child>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Transponders</property>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child type="label_item">
<placeholder/>
<child>
<object class="GtkFrame" id="sat_update_srv_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="sat_update_srv_box">
<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">5</property>
<child>
<object class="GtkScrolledWindow" id="sat_update_srv_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="sat_update_srv_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_service_store</property>
<property name="search_column">1</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_servcie_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererPixbuf" id="srv_picon_renderer">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="srv_service_renderer">
<property name="xalign">0.0099999997764825821</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_package_column">
<property name="title" translatable="yes">Package</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_package_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_type_column">
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_type_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_sid_column">
<property name="title" translatable="yes">SID</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_sid_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_cas_column">
<property name="visible">False</property>
<property name="title" translatable="yes">CAS</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_cas_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_update_srv_status_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="services_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties-symbolic</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="services_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">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="sat_update_srv_frame_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Services</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -170,10 +170,10 @@ class SatellitesTool(Gtk.Box):
self.on_edit(view, force=True)
def on_satellite_add(self, item):
self.on_satellite(None)
self.on_satellite()
def on_transponder_add(self, item):
self.on_transponder(None)
self.on_transponder()
def on_edit(self, view, force=False):
""" Common edit """
@@ -442,7 +442,7 @@ class UpdateDialog:
if title:
self._window.set_title(title)
self._transponder_frame = builder.get_object("sat_update_tr_frame")
self._transponder_paned = builder.get_object("sat_update_tr_paned")
self._sat_view = builder.get_object("sat_update_tree_view")
self._transponder_view = builder.get_object("sat_update_tr_view")
self._service_view = builder.get_object("sat_update_srv_view")
@@ -451,6 +451,9 @@ class UpdateDialog:
self._receive_button = builder.get_object("receive_data_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
self._info_bar_message_label = builder.get_object("info_bar_message_label")
self._satellites_count_label = builder.get_object("satellites_count_label")
self._transponders_count_label = builder.get_object("transponders_count_label")
self._services_count_label = builder.get_object("services_count_label")
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
update_button = builder.get_object("sat_update_button")
self._sat_view.bind_property("sensitive", update_button, "sensitive")
@@ -502,9 +505,7 @@ class UpdateDialog:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
get_base_model(self._sat_view.get_model()).clear()
self._transponder_view.get_model().clear()
self._service_view.get_model().clear()
self.clear_data()
self.is_download = True
self._sat_view.set_sensitive(False)
@@ -514,6 +515,14 @@ class UpdateDialog:
self.get_sat_list(src, self.append_satellites)
def clear_data(self):
get_base_model(self._sat_view.get_model()).clear()
self._transponder_view.get_model().clear()
self._service_view.get_model().clear()
self._satellites_count_label.set_text("0")
self._transponders_count_label.set_text("0")
self._services_count_label.set_text("0")
@run_task
def get_sat_list(self, src, callback):
sat_src = SatelliteSource.FLYSAT
@@ -523,8 +532,7 @@ class UpdateDialog:
sat_src = SatelliteSource.KINGOFSAT
sats = self._parser.get_satellites_list(sat_src)
if sats:
callback(sats)
callback(sats)
self.is_download = False
@run_idle
@@ -534,6 +542,7 @@ class UpdateDialog:
model.append(sat)
self._sat_view.set_sensitive(True)
self._satellites_count_label.set_text(str(len(model)))
@run_idle
def on_receive_data(self, item):
@@ -678,10 +687,10 @@ class SatellitesUpdateDialog(UpdateDialog):
appender.send("-" * 75 + "\n")
sat_count = len(sats)
sats = {s[2]: s for s in sats} # key = position, v = satellite
sats = {s[0]: s for s in sats} # key = name, v = satellite
for row in self._main_model:
pos = row[2]
pos = row[0]
if pos in sats:
sat = sats.pop(pos)
appender.send(f"Updating satellite: {row[0]}\n")
@@ -729,7 +738,7 @@ class ServicesUpdateDialog(UpdateDialog):
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
self._transponder_frame.set_visible(True)
self._transponder_paned.set_visible(True)
self._source_box.remove(0)
self._source_box.connect("changed", self.on_update_satellites_list)
self._source_box.set_active(0)
@@ -754,7 +763,7 @@ class ServicesUpdateDialog(UpdateDialog):
non_cached_sats = []
sat_names = {}
t_names = {}
t_urls = []
t_urls = set()
services = []
for r in (r for r in model if r[-1]):
@@ -766,7 +775,7 @@ class ServicesUpdateDialog(UpdateDialog):
trs = self._transponders.get(url, None)
if trs:
for t in filter(lambda tp: tp.url in self._selected_transponders, trs):
t_urls.append(t.url)
t_urls.add(t.url)
t_names[t.url] = t.text
else:
non_cached_sats.append(url)
@@ -783,7 +792,7 @@ class ServicesUpdateDialog(UpdateDialog):
appender.send(f"Getting transponders for: {sat_names.get(futures[future])}.\n")
for t in future.result():
t_urls.append(t.url)
t_urls.add(t.url)
t_names[t.url] = t.text
appender.send("-" * 75 + "\n")
@@ -804,14 +813,17 @@ class ServicesUpdateDialog(UpdateDialog):
return
appender.send(f"Getting services for: {t_names.get(futures[future], '')}.\n")
list(map(services.append, future.result()))
try:
list(map(services.append, future.result()))
except ValueError as e:
log(f"Getting services error: {e} [{t_names.get(futures[future])}]")
appender.send("-" * 75 + "\n")
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
try:
from app.eparser.enigma.lamedb import LameDbReader
# Used for double checking!
# Used for double check!
reader = LameDbReader(path=None)
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
except ValueError as e:
@@ -829,8 +841,7 @@ class ServicesUpdateDialog(UpdateDialog):
self._services_parser.source = sat_src
sats = self._parser.get_satellites_list(sat_src)
if sats:
callback(sats)
callback(sats)
self.is_download = False
def on_satellite_toggled(self, toggle, path):
@@ -896,6 +907,7 @@ class ServicesUpdateDialog(UpdateDialog):
model.clear()
list(map(model.append, [(t.text, t.url, t.url in self._selected_transponders) for t in trs_list]))
self._sat_view.set_sensitive(True)
self._transponders_count_label.set_text(str(len(model)))
@run_task
def on_activate_transponder(self, view, path, column):
@@ -915,6 +927,7 @@ class ServicesUpdateDialog(UpdateDialog):
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
self._transponder_view.set_sensitive(True)
self._services_count_label.set_text(str(len(model)))
def update_transponder_selection(self, select):
m = self._transponder_view.get_model()

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2021 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -3662,6 +3662,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal

View File

@@ -297,6 +297,7 @@ if IS_LINUX:
LEFT = 113
RIGHT = 114
F2 = 68
F4 = 70
F5 = 71
F7 = 73
SPACE = 65
@@ -337,6 +338,7 @@ elif IS_DARWIN:
LEFT = 123
RIGHT = 123
F2 = 120
F4 = 118
F5 = 96
F7 = 98
SPACE = 49
@@ -375,6 +377,7 @@ else:
LEFT = 37
RIGHT = 39
F2 = 113
F4 = 115
F5 = 116
F7 = 118
SPACE = 32

View File

@@ -6,3 +6,11 @@ button {
min-height: 24px;
min-width: 24px;
}
entry {
min-height: 24px;
}
switch {
margin-right: 2px;
}

View File

@@ -1,5 +1,5 @@
#!/bin/bash
VER="2.0.1_Beta"
VER="2.1.2_Beta"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"

View File

@@ -1,5 +1,5 @@
Package: demon-editor
Version: 2.0.1-Beta
Version: 2.1.2-Beta
Section: utils
Priority: optional
Architecture: all
@@ -10,6 +10,7 @@ Depends: python3 (>= 3.6),
python3-gi-cairo,
gir1.2-notify-0.7
Recommends: libmpv1,
python3-chardet
python3-chardet,
libgtksourceview (>= 3.0)
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Description: Enigma2 channel and satellite list editor

View File

@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018-2021 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -69,7 +69,7 @@ app = BUNDLE(coll,
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"2.0.1.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2021, Dmitriy Yefremov",
'CFBundleShortVersionString': f"2.1.2.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2022, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false'
})

View File

@@ -50,6 +50,9 @@ msgstr "Цякучы IP:"
msgid "Assign"
msgstr "Прывязаць"
msgid "Assign file"
msgstr "Прывязаць файл"
msgid "Bouquet details"
msgstr "Сэрвісы букета"
@@ -230,6 +233,9 @@ msgstr "Скід профілю"
msgid "Satellites"
msgstr "Спадарожнікі"
msgid "Transponders"
msgstr "Транспондары"
msgid "Satellites.xml file:"
msgstr "Файл satellites.xml:"
@@ -1300,3 +1306,30 @@ msgstr "Перацягніце сэрвісы на патрэбны пікон
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Усталёўвае тэчку профілю па змаўчанні для захоўвання піконаў, рэзервовых копій і т. п."
msgid "New sub-bouquet"
msgstr "Стварыць саббукет"
msgid "Mark not presented in Bouquets"
msgstr "Адзначыць адсутныя у букетах"
msgid "Not in Bouquets"
msgstr "Адсутныя у букетах"
msgid "Do not show services present in Bouquets."
msgstr "Не паказваць сэрвісы якія прысутнічаюць у букетах."
msgid "IPTV services only"
msgstr "Толькі IPTV сэрвісы"
msgid "Display picons"
msgstr "Адлюстроўваць пiконы"
msgid "Alternate layout"
msgstr "Альтэрнатыўная кампаноўка"
msgid "Layout of elements has been changed!"
msgstr "Зменена месцаванне элементаў!"
msgid "Restart the program to apply all changes."
msgstr "Перазапусціце праграму, каб ужыць усе змены."

View File

@@ -52,6 +52,9 @@ msgstr "Akt.IP:"
msgid "Assign"
msgstr "Zuweisen"
msgid "Assign file"
msgstr "Datei zuweisen"
msgid "Bouquet details"
msgstr "Bouquet-Details"
@@ -232,6 +235,9 @@ msgstr "Profil zurücksetzen"
msgid "Satellites"
msgstr "Satelliten"
msgid "Transponders"
msgstr "Transpondern"
msgid "Satellites.xml file:"
msgstr "Satellites.xml Datei:"
@@ -1314,3 +1320,30 @@ msgstr "Ziehe die Dienste auf das gewünschte Picon oder Picon in die Liste der
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Legt den Profilordner als Standardordner zum Speichern von Piconen, Backups usw. fest."
msgid "New sub-bouquet"
msgstr "Neues Sub-Bouquet"
msgid "Mark not presented in Bouquets"
msgstr "Markieren die nicht in Bouquets vorhanden"
msgid "Not in Bouquets"
msgstr "Nicht in Bouquets"
msgid "Do not show services present in Bouquets."
msgstr "Vorhandene in Bouquets Dienste nicht anzeigen."
msgid "IPTV services only"
msgstr "Nur IPTV-Dienste"
msgid "Display picons"
msgstr "Picons anzeigen"
msgid "Alternate layout"
msgstr "Alternatives Layout"
msgid "Layout of elements has been changed!"
msgstr "Das Layout der Elemente wurde geändert!"
msgid "Restart the program to apply all changes."
msgstr "Starte das Programm neu, um alle Änderungen zu übernehmen."

View File

@@ -1233,7 +1233,7 @@ msgid "Mark duplicates"
msgstr "Seleziona duplicati"
msgid "Load only for selected bouquet"
msgstr "Larica solo per i bouquet selezionati"
msgstr "Scarica solo per i bouquet selezionati"
msgid "The task is canceled!"
msgstr "Operazione cancellata!"

View File

@@ -50,6 +50,9 @@ msgstr "Текущий IP:"
msgid "Assign"
msgstr "Привязать"
msgid "Assign file"
msgstr "Привязать файл"
msgid "Bouquet details"
msgstr "Сервисы букета"
@@ -230,6 +233,9 @@ msgstr "Сброс профиля"
msgid "Satellites"
msgstr "Спутники"
msgid "Transponders"
msgstr "Транспондеры"
msgid "Satellites.xml file:"
msgstr "Файл satellites.xml:"
@@ -1297,3 +1303,30 @@ msgstr "Перетащите сервисы на нужный пикон или
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Устанавливает папку профиля по умолчанию для хранения пиконов, резервных копий и т. п."
msgid "New sub-bouquet"
msgstr "Создать суббукет"
msgid "Mark not presented in Bouquets"
msgstr "Отметить отсутствующие в букетах"
msgid "Not in Bouquets"
msgstr "Не в букетах"
msgid "Do not show services present in Bouquets."
msgstr "Не показывать сервисы присутствующие в букетах."
msgid "IPTV services only"
msgstr "Только IPTV сервисы"
msgid "Display picons"
msgstr "Отображать пиконы"
msgid "Alternate layout"
msgstr "Альтернативная компоновка"
msgid "Layout of elements has been changed!"
msgstr "Изменено расположение элементов!"
msgid "Restart the program to apply all changes."
msgstr "Перезапустите программу, чтобы применить все изменения."