mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-09 09:37:23 +02:00
Compare commits
218 Commits
3.4.2-b1
...
3.9.0-a1-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a5f55522 | ||
|
|
a7f334682f | ||
|
|
a9b9e5865e | ||
|
|
d59458a84b | ||
|
|
076bcb0cce | ||
|
|
8cbf03eb51 | ||
|
|
abed7bf9cb | ||
|
|
388e748673 | ||
|
|
ba3a3ae0aa | ||
|
|
9bb4f6d75d | ||
|
|
06242ce611 | ||
|
|
e4b1c98b2a | ||
|
|
6a8426e6ef | ||
|
|
583927f1b1 | ||
|
|
185a9b0082 | ||
|
|
ee7eb01af5 | ||
|
|
07c8034393 | ||
|
|
8ec73bc0f9 | ||
|
|
940b28ff6d | ||
|
|
316260703c | ||
|
|
e6c7b6572c | ||
|
|
0e50cf4927 | ||
|
|
f55dff1618 | ||
|
|
669916a5a9 | ||
|
|
4154f4d2f5 | ||
|
|
f3d133b7a3 | ||
|
|
a432da60e5 | ||
|
|
d34128b701 | ||
|
|
d16d0e62e2 | ||
|
|
1a6270478a | ||
|
|
4144e37049 | ||
|
|
61faced24d | ||
|
|
add73558f3 | ||
|
|
259f20794c | ||
|
|
afdb9a3c6b | ||
|
|
8581e7be1f | ||
|
|
5496aec95f | ||
|
|
5441bb0c02 | ||
|
|
3991d2935f | ||
|
|
c42115ba7d | ||
|
|
929987b2f1 | ||
|
|
9a0bead39c | ||
|
|
d18e9c116b | ||
|
|
e477e54dcd | ||
|
|
48b9cb23eb | ||
|
|
5db027c1cf | ||
|
|
128fd8a792 | ||
|
|
32043b7df0 | ||
|
|
bb847cd94c | ||
|
|
64b836363b | ||
|
|
5f34652905 | ||
|
|
32078bc7e3 | ||
|
|
9c582c26db | ||
|
|
aa5addb280 | ||
|
|
2d8ae1bbe2 | ||
|
|
5405d3a9a8 | ||
|
|
d91cbc395c | ||
|
|
e44aedebad | ||
|
|
9ccbd46b71 | ||
|
|
3f0e9e44a9 | ||
|
|
32a847fc5d | ||
|
|
795ad51098 | ||
|
|
64827a60d8 | ||
|
|
b98d55ffb6 | ||
|
|
aa21862303 | ||
|
|
f205e4fa66 | ||
|
|
4ffecb9ce4 | ||
|
|
ecc95e9c82 | ||
|
|
4c5cbcb514 | ||
|
|
8bd24e9642 | ||
|
|
64234c26e0 | ||
|
|
0b1d31fb8a | ||
|
|
a638a6b1fc | ||
|
|
917e184486 | ||
|
|
bc17a6720a | ||
|
|
838cdbd350 | ||
|
|
e795b21499 | ||
|
|
2482e1a424 | ||
|
|
3fd9e3ce9d | ||
|
|
67524f7ede | ||
|
|
721a585eb9 | ||
|
|
45e210dade | ||
|
|
1d98803763 | ||
|
|
568a68916c | ||
|
|
89ed1c0715 | ||
|
|
a1ca26c47e | ||
|
|
27392d6000 | ||
|
|
f5ebd06fc0 | ||
|
|
7b933188f8 | ||
|
|
ec5a89e716 | ||
|
|
f8b1c29638 | ||
|
|
cdbee2b429 | ||
|
|
ad748bf14c | ||
|
|
a7ca2ed0fc | ||
|
|
e32042af35 | ||
|
|
40d2ac9ecf | ||
|
|
324b7bf103 | ||
|
|
3dd3b1df34 | ||
|
|
bbd0b07540 | ||
|
|
9a37280d14 | ||
|
|
1b83c0f7f6 | ||
|
|
c83310b98e | ||
|
|
6a4bd4e5da | ||
|
|
bca50a5d30 | ||
|
|
45b856946d | ||
|
|
3fcc96cd02 | ||
|
|
0894cb5a47 | ||
|
|
1ac6537496 | ||
|
|
285ea66197 | ||
|
|
1edb313e2c | ||
|
|
c9755e0116 | ||
|
|
367a4a2e36 | ||
|
|
fc65ee29ee | ||
|
|
f0e9684f51 | ||
|
|
7084eec407 | ||
|
|
ef79dac603 | ||
|
|
89ee80e4ef | ||
|
|
73df75a519 | ||
|
|
7ed0d6d355 | ||
|
|
b4a0a72db3 | ||
|
|
63e0f1ea14 | ||
|
|
d446240d91 | ||
|
|
3d0f010798 | ||
|
|
4fc4cb7e2a | ||
|
|
80147b4cc0 | ||
|
|
6270c03376 | ||
|
|
2285211100 | ||
|
|
642bca81c2 | ||
|
|
198cb3867d | ||
|
|
d0db68acb4 | ||
|
|
43544a9df3 | ||
|
|
ebf6454181 | ||
|
|
ea09cef837 | ||
|
|
d7853c31ff | ||
|
|
3dcc942c25 | ||
|
|
ffdd98d406 | ||
|
|
4d9ae8c23a | ||
|
|
e2c97169fb | ||
|
|
414fd22f71 | ||
|
|
4ff7129750 | ||
|
|
1546baab30 | ||
|
|
444df51706 | ||
|
|
5e84656c20 | ||
|
|
245d10fb03 | ||
|
|
b9f2e5cb3a | ||
|
|
c040c1145c | ||
|
|
92a91cd995 | ||
|
|
6255b60453 | ||
|
|
30aa967f82 | ||
|
|
b90040f473 | ||
|
|
43bf5ac44b | ||
|
|
e714b10431 | ||
|
|
f0d0813e75 | ||
|
|
9dc4df73c4 | ||
|
|
f2da1e4cd4 | ||
|
|
3d4588833b | ||
|
|
4cf19e5413 | ||
|
|
cd4a814838 | ||
|
|
d7c49f50f2 | ||
|
|
b61b8e16fa | ||
|
|
56d2a3e991 | ||
|
|
b65ea9c0d3 | ||
|
|
a62ee8f378 | ||
|
|
0db8ee6d47 | ||
|
|
ed41b01f63 | ||
|
|
c2eaecb8b8 | ||
|
|
f5313f2c40 | ||
|
|
c6a0b80fdd | ||
|
|
7813aeb059 | ||
|
|
3a0e5c09a1 | ||
|
|
ab6a44dc3f | ||
|
|
caefb4587d | ||
|
|
93ff78d7ce | ||
|
|
1d583ecd99 | ||
|
|
d4914ac451 | ||
|
|
6207b6a10d | ||
|
|
1eb8fe621d | ||
|
|
79b41b1661 | ||
|
|
7a36ba8148 | ||
|
|
08e970fc96 | ||
|
|
9681fcbc79 | ||
|
|
7b002b208f | ||
|
|
95a1732f01 | ||
|
|
57e4fdff7f | ||
|
|
89c456993f | ||
|
|
2f3fc31023 | ||
|
|
5c18e49cf7 | ||
|
|
64530bcb85 | ||
|
|
4d472609b4 | ||
|
|
640b995ab8 | ||
|
|
42980a988f | ||
|
|
64c5f28957 | ||
|
|
a32bf230cf | ||
|
|
b8cac728a8 | ||
|
|
079f07cfd2 | ||
|
|
9a5884cc9a | ||
|
|
115237a10f | ||
|
|
994bd0ee1c | ||
|
|
27e5b373a3 | ||
|
|
43c05b1739 | ||
|
|
bd96c286e9 | ||
|
|
1bded41eab | ||
|
|
5f0f51679c | ||
|
|
380bb3150b | ||
|
|
d1a7a486a2 | ||
|
|
1be167bec3 | ||
|
|
dd3e88589c | ||
|
|
c5a2df6d7d | ||
|
|
c9fc3803c7 | ||
|
|
6afd518cfc | ||
|
|
02a51c9b56 | ||
|
|
c96cfa0e1b | ||
|
|
08bc4ff4c4 | ||
|
|
ae2b78e990 | ||
|
|
88be9fe49c | ||
|
|
9dae9b7219 | ||
|
|
f296a6c90b | ||
|
|
0486776d83 |
@@ -110,7 +110,7 @@ just load your data via *"File/Open"* and press *"Save"*. When importing separat
|
||||
|
||||
**The built-in Telnet client does not support ANSI escape sequences!**
|
||||
|
||||
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.z
|
||||
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.
|
||||
#### Command line arguments:
|
||||
* **-l** - write logs to file.
|
||||
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
|
||||
|
||||
@@ -51,7 +51,7 @@ BQ_FILES_LIST = ("tv", "radio", # Enigma2.
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
|
||||
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
|
||||
WEB_TV_XML_FILE = ("webtv.xml",)
|
||||
WEB_TV_XML_FILE = ("webtv.xml", "webtv_usr.xml")
|
||||
PICONS_SUF = (".jpg", ".png")
|
||||
PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
|
||||
|
||||
@@ -131,33 +131,37 @@ class UtfFTP(FTP):
|
||||
def download_dir(self, path, save_path, callback=None):
|
||||
""" Downloads directory from FTP with all contents.
|
||||
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
"""
|
||||
os.makedirs(os.path.join(save_path, path), exist_ok=True)
|
||||
dir_path = os.path.join(save_path, path, "")
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
current_path = self.pwd()
|
||||
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
|
||||
try:
|
||||
self.cwd(path)
|
||||
except all_errors as e:
|
||||
msg = f"Download dir error: {e}".rstrip()
|
||||
log(msg)
|
||||
return f"500 {msg}"
|
||||
|
||||
for f in files:
|
||||
f_data = self.get_file_data(f)
|
||||
f_path = f_data[8]
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
try:
|
||||
os.makedirs(os.path.join(save_path, f_path), exist_ok=True)
|
||||
except OSError as e:
|
||||
msg = "Download dir error: {}".format(e).rstrip()
|
||||
log(msg)
|
||||
return "500 " + msg
|
||||
else:
|
||||
self.download_dir(f_path, save_path, callback)
|
||||
self.download_dir(f_path, dir_path, callback)
|
||||
else:
|
||||
try:
|
||||
self.download_file(f_path, save_path, callback)
|
||||
self.download_file(f_path, dir_path, callback)
|
||||
except OSError as e:
|
||||
log("Download dir error: {}".format(e).rstrip())
|
||||
log(f"Download dir error: {e}".rstrip())
|
||||
|
||||
self.cwd(current_path)
|
||||
resp = "226 Transfer complete."
|
||||
msg = "Copy directory {}. Status: {}".format(path, resp)
|
||||
msg = f"Copying directory: {path}. Status: {resp}"
|
||||
log(msg)
|
||||
|
||||
if callback:
|
||||
@@ -648,6 +652,9 @@ class HttpAPI:
|
||||
N_ZAP = "zapto"
|
||||
N_STREAM = "build_playlist?id="
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
class Remote(str, Enum):
|
||||
""" Args for HttpRequestType [REMOTE] class. """
|
||||
ONE = "2"
|
||||
@@ -682,6 +689,9 @@ class HttpAPI:
|
||||
NEXT = "407"
|
||||
BACK = "412"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
class Power(str, Enum):
|
||||
""" Args for HttpRequestType [POWER] class. """
|
||||
TOGGLE_STANDBY = "0"
|
||||
@@ -691,6 +701,9 @@ class HttpAPI:
|
||||
WAKEUP = "4"
|
||||
STANDBY = "5"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
PARAM_REQUESTS = {Request.REMOTE,
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
@@ -894,10 +907,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
|
||||
try:
|
||||
log("Testing HTTP connection...")
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return resp.get("e2enigmaversion", "")
|
||||
return resp
|
||||
return resp.get("e2enigmaversion" if s_type is SettingsType.ENIGMA_2 else "data", "")
|
||||
except (RemoteDisconnected, URLError, HTTPError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ class BouquetsWriter:
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
"""
|
||||
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
|
||||
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
|
||||
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet'
|
||||
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet'
|
||||
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
|
||||
|
||||
@@ -76,18 +76,18 @@ class BouquetsWriter:
|
||||
m_count = 0
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.file
|
||||
if not bq_name:
|
||||
if self._force_bq_names:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
else:
|
||||
bq_name = f"de{count:02d}"
|
||||
while bq_name in bq_file_names:
|
||||
count += 1
|
||||
bq_name = f"de{count:02d}"
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
f_name = bq.file
|
||||
bq_type = BqType(bq.type)
|
||||
if not f_name:
|
||||
if self._force_bq_names or bq_type is BqType.BOUQUET:
|
||||
f_name = f"userbouquet.{re.sub(self._NAME_PATTERN, '_', bq.name)}.{bqs.type}"
|
||||
else:
|
||||
f_name = f"userbouquet.de{count:02d}.{bqs.type}"
|
||||
while f_name in bq_file_names:
|
||||
count += 1
|
||||
f_name = f"userbouquet.de{count:02d}.{bqs.type}"
|
||||
bq_file_names.add(f_name)
|
||||
|
||||
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)
|
||||
@@ -95,17 +95,16 @@ class BouquetsWriter:
|
||||
m_count += 1
|
||||
else:
|
||||
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)
|
||||
self.write_sub_bouquet(self._path, f_name, bq, bqs.type)
|
||||
else:
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
|
||||
self.write_bouquet(f"{self._path}{f_name}", bq.name, bq.services)
|
||||
bq_type = 2 if bqs.type == BqType.RADIO.value else 1
|
||||
# Parental lock.
|
||||
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, bq_name, bqs.type)
|
||||
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, f_name)
|
||||
self._black_list.add(locked) if bq.locked else self._black_list.discard(locked)
|
||||
# Hiding.
|
||||
s_type = ServiceType.HIDDEN if bq.hidden else ServiceType.BOUQUET
|
||||
line.append(self._SERVICE.format(s_type, bq_type, bq_name, bqs.type))
|
||||
line.append(self._SERVICE.format(s_type, bq_type, f_name))
|
||||
|
||||
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(line)
|
||||
@@ -153,11 +152,11 @@ class BouquetsWriter:
|
||||
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")
|
||||
sb_file = sb.file or f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
|
||||
self.write_bouquet(f"{path}{sb_file}", sb.name, sb.services)
|
||||
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{sb_file}\" ORDER BY bouquet\n")
|
||||
|
||||
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
with open(f"{self._path}{file_name}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
|
||||
@@ -181,9 +180,7 @@ class ServiceType(Enum):
|
||||
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_ALT_PAT = re.compile(r".*alternatives\.+(.*)\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(r".*\s+\W(.*bouquet)\.+(.*)\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(r".*subbouquet\.+(.*)\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?([\w-]+)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
|
||||
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
|
||||
|
||||
__slots__ = ["_path"]
|
||||
@@ -209,19 +206,19 @@ class BouquetsReader:
|
||||
|
||||
for line in file.readlines():
|
||||
if "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
mt = re.match(self._BQ_PAT, line)
|
||||
s_data = line.split(":")
|
||||
s_type = ServiceType(s_data[1])
|
||||
if name:
|
||||
prefix, b_name = name.group(1), name.group(2)
|
||||
if mt:
|
||||
file_name, prefix, b_name = mt.group(1), mt.group(2), mt.group(3)
|
||||
if b_name in b_names:
|
||||
log(f"The list of bouquets contains duplicate [{b_name}] names!")
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type, prefix)
|
||||
rb_name, services = self.get_bouquet(self._path, file_name, b_name)
|
||||
if rb_name in real_b_names:
|
||||
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
log(f"Bouquet file '{file_name}' has duplicate name: {rb_name}")
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = f"{rb_name} {real_b_names[rb_name]}"
|
||||
else:
|
||||
@@ -230,8 +227,7 @@ class BouquetsReader:
|
||||
s_data[:2] = "10"
|
||||
locked = ":".join(s_data).rstrip()
|
||||
hidden = s_type is ServiceType.HIDDEN
|
||||
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, b_name))
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, file_name))
|
||||
else:
|
||||
if len(s_data) == 12 and s_type is ServiceType.MARKER:
|
||||
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
|
||||
@@ -244,15 +240,15 @@ class BouquetsReader:
|
||||
return bouquets
|
||||
|
||||
@staticmethod
|
||||
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
|
||||
def get_bouquet(path, f_name, bq_name):
|
||||
""" Parsing services ids from bouquet file. """
|
||||
with open(f"{path}{prefix}.{bq_name}.{bq_type}", encoding="utf-8", errors="replace") as file:
|
||||
with open(f"{path}{f_name}", 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(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
|
||||
log(f"Bouquet file '{f_name}' is empty or wrong!")
|
||||
return f"{bq_name} [empty]", services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
@@ -272,17 +268,17 @@ class BouquetsReader:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
|
||||
elif s_type is ServiceType.ALT:
|
||||
alt = re.match(BouquetsReader._ALT_PAT, srv)
|
||||
alt = re.match(BouquetsReader._BQ_PAT, srv)
|
||||
if alt:
|
||||
alt_name, alt_type = alt.group(1), alt.group(2)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
|
||||
af_name, alt_name = alt.group(1), alt.group(3)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, af_name, alt_name)
|
||||
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
|
||||
elif s_type is ServiceType.BOUQUET:
|
||||
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
|
||||
sub = re.match(BouquetsReader._BQ_PAT, srv)
|
||||
if sub:
|
||||
sub_name, sub_type = sub.group(1), sub.group(2)
|
||||
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
|
||||
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
|
||||
sf_name, sub_name, sub_type = sub.group(1), sub.group(3), sub.group(4)
|
||||
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sf_name, sub_name)
|
||||
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sf_name)
|
||||
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
|
||||
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
|
||||
stream_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import re
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.satxml import get_pos_str
|
||||
from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON
|
||||
from .blacklist import get_blacklist
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag, T_FEC, TrType, FEC_DEFAULT, T_SYSTEM
|
||||
@@ -68,7 +69,7 @@ class LameDbReader:
|
||||
return self.parse_v5()
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
|
||||
def parse_v3(self, services, transponders):
|
||||
def parse_v3(self, services_data, transponders):
|
||||
""" Parsing version 3. """
|
||||
for t in transponders:
|
||||
tr = transponders[t].lower()
|
||||
@@ -91,7 +92,7 @@ class LameDbReader:
|
||||
|
||||
transponders[t] = tr
|
||||
|
||||
return self.parse_services(services, transponders)
|
||||
return self.parse_services(services_data, transponders)
|
||||
|
||||
def parse_v4(self):
|
||||
""" Parsing version 4. """
|
||||
@@ -111,7 +112,7 @@ class LameDbReader:
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
trs, srvs = {}, []
|
||||
for line in lns:
|
||||
if line.startswith("s:"):
|
||||
srv_data = line.strip("s:").split(",", 2)
|
||||
@@ -133,15 +134,12 @@ class LameDbReader:
|
||||
|
||||
return self.parse_services(srvs, trs)
|
||||
|
||||
def parse_services(self, services, transponders):
|
||||
def parse_services(self, services_data, transponders):
|
||||
""" Parsing services. """
|
||||
services_list = []
|
||||
blacklist = get_blacklist(self._path) if self._path else {}
|
||||
srvs = self.split(services, 3)
|
||||
if srvs[0][0] == "": # Remove first empty element.
|
||||
srvs.remove(srvs[0])
|
||||
|
||||
for srv in srvs:
|
||||
for srv in self.get_services(services_data):
|
||||
data_id = str(srv[0]).lower() # Lower is for lamedb ver.3.
|
||||
data = data_id.split(_SEP)
|
||||
sp = "0"
|
||||
@@ -220,8 +218,7 @@ class LameDbReader:
|
||||
freq = f"{int(freq) // 1000}"
|
||||
rate = f"{int(rate) // 1000}"
|
||||
if tr_type is TrType.Satellite:
|
||||
pos = int(pos)
|
||||
pos = f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
|
||||
pos = get_pos_str(int(pos))
|
||||
except ValueError as e:
|
||||
log(f"Parse error [parse_services]: {e}")
|
||||
|
||||
@@ -243,11 +240,12 @@ class LameDbReader:
|
||||
|
||||
transponders, sep, services = services.partition("services") # 2 step
|
||||
services, sep, _ = services.partition("\nend") # 3 step
|
||||
services = services.strip()
|
||||
|
||||
if match.group() == "/3/":
|
||||
return self.parse_v3(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
return self.parse_v3(services.splitlines(), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
return self.parse_services(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
return self.parse_services(services.splitlines(), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
@staticmethod
|
||||
def get_services_lines(services):
|
||||
@@ -282,17 +280,25 @@ class LameDbReader:
|
||||
|
||||
return transponders
|
||||
|
||||
def split(self, itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
def get_services(self, itr, size=3):
|
||||
""" Separates and extract services data. """
|
||||
services = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
i = 0
|
||||
for line in itr:
|
||||
i += 1
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
if i == size:
|
||||
if not line.startswith("p:"):
|
||||
# To prevent cases of incorrect service data formation
|
||||
# (e.g. the name contains a line break)
|
||||
tmp.pop()
|
||||
i -= 1
|
||||
else:
|
||||
services.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
i = 0
|
||||
return services
|
||||
|
||||
|
||||
class LameDbWriter:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -58,6 +58,9 @@ class StreamType(Enum):
|
||||
|
||||
|
||||
def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
""" Parses *m3u* file and returns tuple with EPG src URLs and services list. """
|
||||
pattern = re.compile(r'(\S+)="(.*?)"')
|
||||
|
||||
with open(path, "rb") as file:
|
||||
data = file.read()
|
||||
encoding = "utf-8"
|
||||
@@ -73,8 +76,10 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
|
||||
aggr = [None] * 10
|
||||
s_aggr = aggr[: -3]
|
||||
services = []
|
||||
epg_src = None
|
||||
group = None
|
||||
groups = set()
|
||||
services = []
|
||||
marker_counter = 1
|
||||
sid_counter = 1
|
||||
name = None
|
||||
@@ -85,51 +90,46 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
m_name = BqServiceType.MARKER.name
|
||||
|
||||
for line in str(data, encoding=encoding, errors="ignore").splitlines():
|
||||
if line.startswith("#EXTM3U"):
|
||||
data = dict(pattern.findall(line))
|
||||
epg_src = data.get("x-tvg-url", data.get("url-tvg", None))
|
||||
epg_src = epg_src.split(",") if epg_src else None
|
||||
if line.startswith("#EXTINF"):
|
||||
line, sep, name = line.rpartition(",")
|
||||
|
||||
data = re.split('"', line)
|
||||
size = len(data)
|
||||
if size < 3:
|
||||
continue
|
||||
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
|
||||
picon = d.get("tvg-logo", None)
|
||||
data = dict(pattern.findall(line))
|
||||
name = data.get("tvg-name", name)
|
||||
picon = data.get("tvg-logo", None)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = d.get("group-title", None)
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
group = data.get("group-title", None)
|
||||
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = line.strip("#EXTGRP:").strip()
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif not line.startswith("#"):
|
||||
group = line.strip("#EXTGRP:").strip()
|
||||
elif not line.startswith("#") and "://" in line:
|
||||
url = line.strip()
|
||||
params[0] = sid_counter
|
||||
sid_counter += 1
|
||||
fav_id = get_fav_id(url, name, s_type, params)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
p_id = get_picon_id(params)
|
||||
if group not in groups:
|
||||
# Some playlists have "random" of group names.
|
||||
# We will take only the first one we found on the list!
|
||||
groups.add(group)
|
||||
m_id = MARKER_FORMAT.format(marker_counter, group, group)
|
||||
marker_counter += 1
|
||||
services.append(Service(None, None, None, group, *aggr[0:3], m_name, *aggr, m_id, None))
|
||||
|
||||
if all((name, url, fav_id)):
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
|
||||
services.append(srv)
|
||||
services.append(Service(None, None, IPTV_ICON, name, *aggr[0:2], group,
|
||||
st, picon, p_id, *s_aggr, url, fav_id, None))
|
||||
else:
|
||||
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
|
||||
|
||||
return services
|
||||
return epg_src, services
|
||||
|
||||
|
||||
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.*?)::::.*")
|
||||
pattern = re.compile(".*:(http.*).*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
|
||||
lines = ["#EXTM3U\n"]
|
||||
current_grp = None
|
||||
|
||||
@@ -141,7 +141,8 @@ def export_to_m3u(path, bouquet, s_type, url=None):
|
||||
continue
|
||||
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")
|
||||
u = res.group(1)
|
||||
lines.append(f"{unquote(u[:u.rfind(':')])}\n")
|
||||
elif s_type is BqServiceType.MARKER:
|
||||
current_grp = f"#EXTGRP:{s.name}\n"
|
||||
elif s_type is BqServiceType.DEFAULT and url:
|
||||
@@ -153,12 +154,13 @@ def export_to_m3u(path, bouquet, s_type, url=None):
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1):
|
||||
""" Returns fav id depending on the profile. """
|
||||
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1, force_quote=True):
|
||||
""" Returns fav id depending on the settings type. """
|
||||
if settings_type is SettingsType.ENIGMA_2:
|
||||
st_type = st_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, quote(url), name, name, None)
|
||||
url = quote(url) if force_quote else url
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, url, name, name, None)
|
||||
elif settings_type is SettingsType.NEUTRINO_MP:
|
||||
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT
|
||||
from app.eparser.neutrino import KSP, SP, get_xml_attributes, get_attributes, API_VER
|
||||
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
|
||||
@@ -35,7 +36,7 @@ from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDE
|
||||
|
||||
_FILE = "bouquets.xml"
|
||||
_U_FILE = "ubouquets.xml"
|
||||
_W_FILE = "webtv.xml"
|
||||
_W_FILE = "webtv_usr.xml"
|
||||
|
||||
_COMMENT = " File was created in DemonEditor. Enjoy watching! "
|
||||
|
||||
@@ -60,20 +61,27 @@ def parse_bouquets(file, name, bq_type):
|
||||
hidden = bq_attrs.get("hidden", "0")
|
||||
locked = bq_attrs.get("locked", "0")
|
||||
services = []
|
||||
|
||||
for srv_elem in elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
s_attrs = get_xml_attributes(srv_elem)
|
||||
ssid = s_attrs.get("i", "0")
|
||||
on = s_attrs.get("on", "0")
|
||||
tr_id = s_attrs.get("t", "0")
|
||||
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
|
||||
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
|
||||
if "i" in s_attrs:
|
||||
ssid = s_attrs.get("i", "0")
|
||||
on = s_attrs.get("on", "0")
|
||||
tr_id = s_attrs.get("t", "0")
|
||||
fav_id = f"{tr_id}:{on}:{ssid}"
|
||||
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
|
||||
elif "u" in s_attrs:
|
||||
services.append(get_webtv_service(s_attrs))
|
||||
else:
|
||||
log(f"Parse bouquets [Neutrino] error: Unknown service type. -> {s_attrs}")
|
||||
|
||||
bouquets[2].append(Bouquet(name=bq_name,
|
||||
type=bq_type,
|
||||
services=services,
|
||||
locked=locked == "1",
|
||||
hidden=hidden == "1",
|
||||
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
|
||||
file=SP.join(f"{k}{KSP}{v}" for k, v in bq_attrs.items())))
|
||||
|
||||
if BqType(bq_type) is BqType.BOUQUET:
|
||||
for bq in bouquets.bouquets:
|
||||
@@ -96,20 +104,7 @@ def parse_webtv(path, name, bq_type):
|
||||
for elem in dom.getElementsByTagName("webtv"):
|
||||
if elem.hasAttributes():
|
||||
web_attrs = get_xml_attributes(elem)
|
||||
title = web_attrs.get("title", "")
|
||||
url = web_attrs.get("url", "")
|
||||
description = web_attrs.get("description", "")
|
||||
urlkey = web_attrs.get("urlkey", None)
|
||||
account = web_attrs.get("account", None)
|
||||
usrname = web_attrs.get("usrname", None)
|
||||
psw = web_attrs.get("psw", None)
|
||||
s_type = web_attrs.get("type", None)
|
||||
iconsrc = web_attrs.get("iconsrc", None)
|
||||
iconsrc_b = web_attrs.get("iconsrc_b", None)
|
||||
group = web_attrs.get("group", None)
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
|
||||
iconsrc_b, group)
|
||||
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
|
||||
services.append(get_webtv_service(web_attrs))
|
||||
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
|
||||
bouquets[2].append(bouquet)
|
||||
@@ -117,6 +112,21 @@ def parse_webtv(path, name, bq_type):
|
||||
return bouquets
|
||||
|
||||
|
||||
def get_webtv_service(web_attrs):
|
||||
title = web_attrs.get("title", web_attrs.get("n", ""))
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(web_attrs.get("url", web_attrs.get("u", )),
|
||||
web_attrs.get("description", ""),
|
||||
web_attrs.get("urlkey", None),
|
||||
web_attrs.get("account", None),
|
||||
web_attrs.get("usrname", None),
|
||||
web_attrs.get("psw", None),
|
||||
web_attrs.get("type", None),
|
||||
web_attrs.get("iconsrc", None),
|
||||
web_attrs.get("iconsrc_b", None),
|
||||
web_attrs.get("group", None))
|
||||
return BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0)
|
||||
|
||||
|
||||
def write_bouquets(path, bouquets):
|
||||
for bq in bouquets:
|
||||
bq_type = BqType(bq.type)
|
||||
@@ -153,14 +163,25 @@ def write_bouquet(file, bouquet):
|
||||
root.appendChild(bq_elem)
|
||||
|
||||
for srv in bq.services:
|
||||
tr_id, on, ssid = srv.fav_id.split(":")
|
||||
srv_elem = doc.createElement("S")
|
||||
srv_elem.setAttribute("i", ssid)
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
srv_elem.setAttribute("t", tr_id)
|
||||
srv_elem.setAttribute("on", on)
|
||||
srv_elem.setAttribute("frq", srv.freq)
|
||||
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
|
||||
s_type = BqServiceType(srv.service_type)
|
||||
|
||||
if s_type is BqServiceType.DEFAULT:
|
||||
tr_id, on, ssid = srv.fav_id.split(":")
|
||||
srv_elem.setAttribute("i", ssid)
|
||||
srv_elem.setAttribute("t", tr_id)
|
||||
srv_elem.setAttribute("on", on)
|
||||
srv_elem.setAttribute("frq", srv.freq)
|
||||
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
|
||||
elif s_type is BqServiceType.IPTV:
|
||||
s_data = srv.fav_id.split("::")
|
||||
if s_data:
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
srv_elem.setAttribute("u", s_data[0])
|
||||
else:
|
||||
log(f"Write bouquet [Neutrino] error: Unsupported service type. -> {s_type.value}")
|
||||
|
||||
bq_elem.appendChild(srv_elem)
|
||||
|
||||
doc.write_xml(file)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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
|
||||
@@ -192,5 +192,10 @@ def indent(elem, parent=None, index=-1, level=0, space=" "):
|
||||
elem.tail = f"\n{space * (level - 1)}"
|
||||
|
||||
|
||||
def get_pos_str(pos: int) -> str:
|
||||
""" Converts satellite position int value to readable string. """
|
||||
return f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
169
app/settings.py
169
app/settings.py
@@ -31,7 +31,7 @@ import json
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum, IntEnum
|
||||
from enum import IntEnum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
@@ -51,8 +51,8 @@ IS_LINUX = sys.platform == "linux"
|
||||
USE_HEADER_BAR = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
|
||||
|
||||
|
||||
class Defaults(Enum):
|
||||
""" Default program settings """
|
||||
class Defaults:
|
||||
""" Default program settings. """
|
||||
USER = "root"
|
||||
PASSWORD = ""
|
||||
HOST = "127.0.0.1"
|
||||
@@ -114,30 +114,30 @@ class SettingsType(IntEnum):
|
||||
def get_default_settings(self):
|
||||
""" Returns default settings for current type. """
|
||||
if self is self.ENIGMA_2:
|
||||
srv_path = Defaults.BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.BOX_PICON_PATH.value
|
||||
epg_path = Defaults.BOX_EPG_PATH.value
|
||||
srv_path = Defaults.BOX_SERVICES_PATH
|
||||
sat_path = Defaults.BOX_SATELLITE_PATH
|
||||
picons_path = Defaults.BOX_PICON_PATH
|
||||
epg_path = Defaults.BOX_EPG_PATH
|
||||
http_timeout = 5
|
||||
telnet_timeout = 5
|
||||
else:
|
||||
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH.value
|
||||
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH
|
||||
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH
|
||||
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH
|
||||
epg_path = ""
|
||||
http_timeout = 2
|
||||
telnet_timeout = 1
|
||||
|
||||
return {"setting_type": self.value,
|
||||
"host": Defaults.HOST.value,
|
||||
"port": Defaults.FTP_PORT.value,
|
||||
"host": Defaults.HOST,
|
||||
"port": Defaults.FTP_PORT,
|
||||
"timeout": 5,
|
||||
"user": Defaults.USER.value,
|
||||
"password": Defaults.PASSWORD.value,
|
||||
"http_port": Defaults.HTTP_PORT.value,
|
||||
"user": Defaults.USER,
|
||||
"password": Defaults.PASSWORD,
|
||||
"http_port": Defaults.HTTP_PORT,
|
||||
"http_timeout": http_timeout,
|
||||
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
|
||||
"telnet_port": Defaults.TELNET_PORT.value,
|
||||
"http_use_ssl": Defaults.HTTP_USE_SSL,
|
||||
"telnet_port": Defaults.TELNET_PORT,
|
||||
"telnet_timeout": telnet_timeout,
|
||||
"services_path": srv_path,
|
||||
"user_bouquet_path": srv_path,
|
||||
@@ -161,6 +161,15 @@ class PlayStreamsMode(IntEnum):
|
||||
M3U = 2
|
||||
|
||||
|
||||
class PlaybackMode(IntEnum):
|
||||
""" Playback mode by double click of mouse in the bouquet (FAV) list. """
|
||||
DISABLED = 0
|
||||
STREAM = 1
|
||||
PLAY = 2
|
||||
ZAP = 3
|
||||
ZAP_PLAY = 4
|
||||
|
||||
|
||||
class EpgSource(IntEnum):
|
||||
HTTP = 0 # HTTP API -> WebIf
|
||||
DAT = 1 # epg.dat file
|
||||
@@ -402,9 +411,9 @@ class Settings:
|
||||
@property
|
||||
def picons_paths(self):
|
||||
if self.setting_type is SettingsType.NEUTRINO_MP:
|
||||
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS.value)
|
||||
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS)
|
||||
else:
|
||||
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS.value)
|
||||
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS)
|
||||
|
||||
@picons_paths.setter
|
||||
def picons_paths(self, value):
|
||||
@@ -417,35 +426,43 @@ class Settings:
|
||||
|
||||
@property
|
||||
def profile_folder_is_default(self):
|
||||
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT.value)
|
||||
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT)
|
||||
|
||||
@profile_folder_is_default.setter
|
||||
def profile_folder_is_default(self, value):
|
||||
self._settings["profile_folder_is_default"] = value
|
||||
|
||||
@property
|
||||
def use_common_picon_path(self):
|
||||
return self._settings.get("use_common_picon_path", False)
|
||||
|
||||
@use_common_picon_path.setter
|
||||
def use_common_picon_path(self, value):
|
||||
self._settings["use_common_picon_path"] = value
|
||||
|
||||
@property
|
||||
def default_data_path(self):
|
||||
return self._settings.get("default_data_path", DATA_PATH)
|
||||
|
||||
@default_data_path.setter
|
||||
def default_data_path(self, value):
|
||||
self._settings["default_data_path"] = value
|
||||
self._settings["default_data_path"] = Settings.normalize_path(value)
|
||||
|
||||
@property
|
||||
def default_backup_path(self):
|
||||
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH.value)
|
||||
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH)
|
||||
|
||||
@default_backup_path.setter
|
||||
def default_backup_path(self, value):
|
||||
self._settings["default_backup_path"] = value
|
||||
self._settings["default_backup_path"] = Settings.normalize_path(value)
|
||||
|
||||
@property
|
||||
def default_picon_path(self):
|
||||
return self._settings.get("default_picon_path", Defaults.PICON_PATH.value)
|
||||
return self._settings.get("default_picon_path", Defaults.PICON_PATH)
|
||||
|
||||
@default_picon_path.setter
|
||||
def default_picon_path(self, value):
|
||||
self._settings["default_picon_path"] = value
|
||||
self._settings["default_picon_path"] = Settings.normalize_path(value)
|
||||
|
||||
@property
|
||||
def profile_data_path(self):
|
||||
@@ -457,6 +474,9 @@ class Settings:
|
||||
|
||||
@property
|
||||
def profile_picons_path(self):
|
||||
if self.use_common_picon_path:
|
||||
return self.default_picon_path
|
||||
|
||||
if self.profile_folder_is_default:
|
||||
return f"{self.profile_data_path}picons{SEP}"
|
||||
return f"{self.default_picon_path}{self._current_profile}{SEP}"
|
||||
@@ -477,17 +497,17 @@ class Settings:
|
||||
|
||||
@property
|
||||
def recordings_path(self):
|
||||
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH.value)
|
||||
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH)
|
||||
|
||||
@recordings_path.setter
|
||||
def recordings_path(self, value):
|
||||
self._settings["recordings_path"] = value
|
||||
self._settings["recordings_path"] = Settings.normalize_path(value)
|
||||
|
||||
# ******** Streaming ********* #
|
||||
|
||||
@property
|
||||
def activate_transcoding(self):
|
||||
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING.value)
|
||||
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING)
|
||||
|
||||
@activate_transcoding.setter
|
||||
def activate_transcoding(self, value):
|
||||
@@ -495,7 +515,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def active_preset(self):
|
||||
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET.value)
|
||||
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET)
|
||||
|
||||
@active_preset.setter
|
||||
def active_preset(self, value):
|
||||
@@ -511,7 +531,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def play_streams_mode(self):
|
||||
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE.value))
|
||||
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE))
|
||||
|
||||
@play_streams_mode.setter
|
||||
def play_streams_mode(self, value):
|
||||
@@ -519,7 +539,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def stream_lib(self):
|
||||
return self._settings.get("stream_lib", Defaults.STREAM_LIB.value)
|
||||
return self._settings.get("stream_lib", Defaults.STREAM_LIB)
|
||||
|
||||
@stream_lib.setter
|
||||
def stream_lib(self, value):
|
||||
@@ -527,7 +547,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def fav_click_mode(self):
|
||||
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
|
||||
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE)
|
||||
|
||||
@fav_click_mode.setter
|
||||
def fav_click_mode(self, value):
|
||||
@@ -535,7 +555,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def main_list_playback(self):
|
||||
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK.value)
|
||||
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK)
|
||||
|
||||
@main_list_playback.setter
|
||||
def main_list_playback(self, value):
|
||||
@@ -576,6 +596,14 @@ class Settings:
|
||||
def epg_xml_source(self, value):
|
||||
self._cp_settings["epg_xml_source"] = value
|
||||
|
||||
@property
|
||||
def epg_xml_sources(self):
|
||||
return self._cp_settings.get("epg_xml_sources", [self.epg_xml_source])
|
||||
|
||||
@epg_xml_sources.setter
|
||||
def epg_xml_sources(self, value):
|
||||
self._cp_settings["epg_xml_sources"] = value
|
||||
|
||||
# *********** FTP ************ #
|
||||
|
||||
@property
|
||||
@@ -590,7 +618,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def backup_before_save(self):
|
||||
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE.value)
|
||||
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE)
|
||||
|
||||
@backup_before_save.setter
|
||||
def backup_before_save(self, value):
|
||||
@@ -598,7 +626,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def backup_before_downloading(self):
|
||||
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING.value)
|
||||
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING)
|
||||
|
||||
@backup_before_downloading.setter
|
||||
def backup_before_downloading(self, value):
|
||||
@@ -606,7 +634,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def v5_support(self):
|
||||
return self._settings.get("v5_support", Defaults.V5_SUPPORT.value)
|
||||
return self._settings.get("v5_support", Defaults.V5_SUPPORT)
|
||||
|
||||
@v5_support.setter
|
||||
def v5_support(self, value):
|
||||
@@ -614,7 +642,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def unlimited_copy_buffer(self):
|
||||
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER.value)
|
||||
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER)
|
||||
|
||||
@unlimited_copy_buffer.setter
|
||||
def unlimited_copy_buffer(self, value):
|
||||
@@ -622,7 +650,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def extensions_support(self):
|
||||
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT.value)
|
||||
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT)
|
||||
|
||||
@extensions_support.setter
|
||||
def extensions_support(self, value):
|
||||
@@ -630,7 +658,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def force_bq_names(self):
|
||||
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
|
||||
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES)
|
||||
|
||||
@force_bq_names.setter
|
||||
def force_bq_names(self, value):
|
||||
@@ -638,7 +666,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def http_api_support(self):
|
||||
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
|
||||
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT)
|
||||
|
||||
@http_api_support.setter
|
||||
def http_api_support(self, value):
|
||||
@@ -646,7 +674,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def enable_yt_dl(self):
|
||||
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL.value)
|
||||
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL)
|
||||
|
||||
@enable_yt_dl.setter
|
||||
def enable_yt_dl(self, value):
|
||||
@@ -654,7 +682,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def enable_yt_dl_update(self):
|
||||
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL.value)
|
||||
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL)
|
||||
|
||||
@enable_yt_dl_update.setter
|
||||
def enable_yt_dl_update(self, value):
|
||||
@@ -662,7 +690,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def enable_send_to(self):
|
||||
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
|
||||
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO)
|
||||
|
||||
@enable_send_to.setter
|
||||
def enable_send_to(self, value):
|
||||
@@ -722,7 +750,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def list_picon_size(self):
|
||||
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE.value)
|
||||
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE)
|
||||
|
||||
@list_picon_size.setter
|
||||
def list_picon_size(self, value):
|
||||
@@ -730,7 +758,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def tooltip_logo_size(self):
|
||||
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE.value)
|
||||
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE)
|
||||
|
||||
@tooltip_logo_size.setter
|
||||
def tooltip_logo_size(self, value):
|
||||
@@ -738,7 +766,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def use_colors(self):
|
||||
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
|
||||
return self._settings.get("use_colors", Defaults.USE_COLORS)
|
||||
|
||||
@use_colors.setter
|
||||
def use_colors(self, value):
|
||||
@@ -746,7 +774,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def new_color(self):
|
||||
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
|
||||
return self._settings.get("new_color", Defaults.NEW_COLOR)
|
||||
|
||||
@new_color.setter
|
||||
def new_color(self, value):
|
||||
@@ -754,7 +782,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def extra_color(self):
|
||||
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
|
||||
return self._settings.get("extra_color", Defaults.EXTRA_COLOR)
|
||||
|
||||
@extra_color.setter
|
||||
def extra_color(self, value):
|
||||
@@ -903,13 +931,14 @@ class Settings:
|
||||
# **************** Get-Set settings **************** #
|
||||
|
||||
@staticmethod
|
||||
def get_settings():
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
Settings.write_settings(Settings.get_default_settings())
|
||||
def get_settings(config_file=CONFIG_FILE, default_settings=None):
|
||||
if not os.path.isfile(config_file) or os.stat(config_file).st_size == 0:
|
||||
df = Settings.get_default_settings() if default_settings is None else default_settings
|
||||
Settings.write_settings(df, config_file=config_file)
|
||||
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
|
||||
with open(config_file, "r", encoding="utf-8") as cf:
|
||||
try:
|
||||
return json.load(config_file)
|
||||
return json.load(cf)
|
||||
except ValueError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
@@ -919,18 +948,18 @@ class Settings:
|
||||
|
||||
return {
|
||||
"version": Settings.__VERSION,
|
||||
"default_profile": Defaults.DEFAULT_PROFILE.value,
|
||||
"default_profile": Defaults.DEFAULT_PROFILE,
|
||||
"profiles": {profile_name: def_settings},
|
||||
"v5_support": Defaults.V5_SUPPORT.value,
|
||||
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
|
||||
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
|
||||
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
|
||||
"use_colors": Defaults.USE_COLORS.value,
|
||||
"new_color": Defaults.NEW_COLOR.value,
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDINGS_PATH.value
|
||||
"v5_support": Defaults.V5_SUPPORT,
|
||||
"http_api_support": Defaults.HTTP_API_SUPPORT,
|
||||
"enable_yt_dl": Defaults.ENABLE_YT_DL,
|
||||
"enable_send_to": Defaults.ENABLE_SEND_TO,
|
||||
"use_colors": Defaults.USE_COLORS,
|
||||
"new_color": Defaults.NEW_COLOR,
|
||||
"extra_color": Defaults.EXTRA_COLOR,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT,
|
||||
"records_path": Defaults.RECORDINGS_PATH
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -941,10 +970,14 @@ class Settings:
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
|
||||
|
||||
@staticmethod
|
||||
def write_settings(config):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
|
||||
json.dump(config, config_file, indent=" ")
|
||||
def write_settings(config, config_path=CONFIG_PATH, config_file=CONFIG_FILE):
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
with open(config_file, "w", encoding="utf-8") as cf:
|
||||
json.dump(config, cf, indent=" ")
|
||||
|
||||
@staticmethod
|
||||
def normalize_path(path):
|
||||
return f"{os.path.normpath(path)}{SEP}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
100
app/tools/epg.py
100
app/tools/epg.py
@@ -34,7 +34,7 @@ import shutil
|
||||
import struct
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from tempfile import NamedTemporaryFile
|
||||
from urllib.parse import urlparse
|
||||
@@ -61,6 +61,10 @@ EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "N/A", None) # For Pyth
|
||||
|
||||
class Reader(metaclass=abc.ABCMeta):
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def cache(self) -> dict: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, clb=None): pass
|
||||
|
||||
@@ -121,6 +125,10 @@ class EPG:
|
||||
self._refs = {}
|
||||
self._desc = {}
|
||||
|
||||
@property
|
||||
def cache(self) -> dict:
|
||||
return self._refs
|
||||
|
||||
def download(self, clb=None):
|
||||
pass
|
||||
|
||||
@@ -229,13 +237,19 @@ class XmlTvReader(Reader):
|
||||
|
||||
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
|
||||
|
||||
SUFFIXES = {".gz", ".xz", ".lzma", ".xml"}
|
||||
|
||||
Service = namedtuple("Service", ["id", "names", "logo", "events"])
|
||||
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
|
||||
|
||||
def __init__(self, path, url):
|
||||
def __init__(self, path, url=None):
|
||||
self._path = path
|
||||
self._url = url
|
||||
self._ids = {}
|
||||
self._cache = {}
|
||||
|
||||
@property
|
||||
def cache(self) -> dict:
|
||||
return self._cache
|
||||
|
||||
def download(self, clb=None):
|
||||
""" Downloads an XMLTV file. """
|
||||
@@ -244,23 +258,28 @@ class XmlTvReader(Reader):
|
||||
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
|
||||
return
|
||||
|
||||
with requests.get(url=self._url, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
with requests.get(url=self._url, stream=True) as resp:
|
||||
if resp.reason == "OK":
|
||||
suf = self._url[self._url.rfind("."):]
|
||||
if suf not in (".gz", ".xz", ".lzma"):
|
||||
if suf not in self.SUFFIXES:
|
||||
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
|
||||
return
|
||||
|
||||
data_len = request.headers.get("content-length")
|
||||
data_size = resp.headers.get("content-length")
|
||||
if not data_size:
|
||||
log(f"{self.__class__.__name__} [download *.{suf}] error: Error getting data size.")
|
||||
if clb:
|
||||
clb()
|
||||
return
|
||||
|
||||
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
|
||||
downloaded = 0
|
||||
data_len = int(data_len)
|
||||
data_size = int(data_size)
|
||||
log("Downloading XMLTV file...")
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
for data in resp.iter_content(chunk_size=1024):
|
||||
downloaded += len(data)
|
||||
tf.write(data)
|
||||
done = int(50 * downloaded / data_len)
|
||||
done = int(50 * downloaded / data_size)
|
||||
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
|
||||
sys.stdout.flush()
|
||||
tf.seek(0)
|
||||
@@ -281,47 +300,70 @@ class XmlTvReader(Reader):
|
||||
shutil.copyfileobj(lzf, self._path)
|
||||
except (lzma.LZMAError, OSError) as e:
|
||||
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
|
||||
else:
|
||||
try:
|
||||
import gzip
|
||||
with gzip.open(self._path, "wb") as f_out:
|
||||
shutil.copyfileobj(tf, f_out)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.xml] error: {e}")
|
||||
|
||||
if IS_WIN and os.path.isfile(tf.name):
|
||||
tf.close()
|
||||
os.remove(tf.name)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {request.reason}")
|
||||
log(f"{self.__class__.__name__} [download] error: {resp.reason}")
|
||||
|
||||
if clb:
|
||||
clb()
|
||||
|
||||
def get_current_events(self, names: set) -> dict:
|
||||
events = {}
|
||||
events = defaultdict(list)
|
||||
|
||||
dt = datetime.utcnow()
|
||||
utc = dt.timestamp()
|
||||
offset = datetime.now() - dt
|
||||
|
||||
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
|
||||
ev = max(filter(lambda s: s.start < utc, srv.events), key=lambda x: x.start, default=None)
|
||||
if ev:
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
start = start.timestamp()
|
||||
end_time = end_time.timestamp()
|
||||
|
||||
for n in srv.names:
|
||||
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), ev.desc, ev)
|
||||
for srv in filter(lambda s: any(name in names for name in s.names), self._cache.values()):
|
||||
[self.process_event(ev, events, offset, srv) for ev in filter(lambda s: s.duration > utc, srv.events)]
|
||||
|
||||
return events
|
||||
|
||||
@staticmethod
|
||||
def process_event(ev, events, offset, srv):
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
start = start.timestamp()
|
||||
end_time = end_time.timestamp()
|
||||
duration = end_time - start
|
||||
|
||||
for n in srv.names:
|
||||
data = {"e2eventservicename": n,
|
||||
"e2eventtitle": ev.title,
|
||||
"e2eventdescription": ev.desc,
|
||||
"e2eventstart": start,
|
||||
"e2eventduration": duration}
|
||||
events[n].append(EpgEvent(n, ev.title, start, end_time, duration, ev.desc, data))
|
||||
|
||||
def parse(self):
|
||||
""" Parses XML. """
|
||||
try:
|
||||
import gzip
|
||||
log("Processing XMLTV data...")
|
||||
suf = os.path.splitext(self._path)[1]
|
||||
if suf == ".gz":
|
||||
import gzip
|
||||
|
||||
with gzip.open(self._path, "rb") as gzf:
|
||||
log("Processing XMLTV data...")
|
||||
list(map(self.process_node, ET.iterparse(gzf)))
|
||||
log("XMLTV data parsing is complete.")
|
||||
with gzip.open(self._path, "rb") as gzf:
|
||||
list(map(self.process_node, ET.iterparse(gzf)))
|
||||
elif suf == ".xml":
|
||||
with open(self._path, "rb") as xml:
|
||||
list(map(self.process_node, ET.iterparse(xml)))
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [parse] error: Unsupported file type [{suf}].")
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [parse] error: {e}")
|
||||
else:
|
||||
log("XMLTV data parsing is complete.")
|
||||
|
||||
def process_node(self, node):
|
||||
event, element = node
|
||||
@@ -329,9 +371,9 @@ class XmlTvReader(Reader):
|
||||
ch_id = element.get("id", None)
|
||||
logo = None # Currently not in use.
|
||||
# Since a service can have several names, we will store a set of names in the "names" field!
|
||||
self._ids[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
|
||||
self._cache[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
|
||||
elif element.tag == self.PR_TAG:
|
||||
channel = self._ids.get(element.get(self.CH_TAG, None), None)
|
||||
channel = self._cache.get(element.get(self.CH_TAG, None), None)
|
||||
if channel:
|
||||
events = channel[-1]
|
||||
start = element.get("start", None)
|
||||
|
||||
@@ -30,17 +30,20 @@ import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from gi.repository import Gdk, Gtk, GObject
|
||||
from gi.repository import GObject
|
||||
|
||||
from app.commons import run_task, log, LOG_DATE_FORMAT, run_with_delay
|
||||
from app.commons import run_task, log, LOG_DATE_FORMAT
|
||||
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
|
||||
|
||||
class Player(Gtk.DrawingArea):
|
||||
class Player(GObject.GObject):
|
||||
""" Base player class. Also used as a factory. """
|
||||
|
||||
def __init__(self, mode, widget, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
self._handle = self.get_window_handle(widget.playback_widget)
|
||||
|
||||
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
@@ -55,16 +58,9 @@ class Player(Gtk.DrawingArea):
|
||||
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self.connect("draw", self.on_draw)
|
||||
self.connect("motion-notify-event", self.on_mouse_motion)
|
||||
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
widget.add(self)
|
||||
|
||||
parent = widget.get_parent()
|
||||
parent.connect("play", self.on_play)
|
||||
parent.connect("stop", self.on_stop)
|
||||
parent.connect("pause", self.on_pause)
|
||||
self.show()
|
||||
widget.connect("play", self.on_play)
|
||||
widget.connect("stop", self.on_stop)
|
||||
widget.connect("pause", self.on_pause)
|
||||
|
||||
def get_play_mode(self):
|
||||
pass
|
||||
@@ -114,14 +110,14 @@ class Player(Gtk.DrawingArea):
|
||||
def on_release(self, widget, state):
|
||||
self.release()
|
||||
|
||||
def get_window_handle(self):
|
||||
def get_window_handle(self, widget):
|
||||
""" Returns the identifier [pointer] for the window.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if IS_LINUX:
|
||||
return self.get_window().get_xid()
|
||||
return widget.get_window().get_xid()
|
||||
else:
|
||||
try:
|
||||
import ctypes
|
||||
@@ -133,31 +129,13 @@ class Player(Gtk.DrawingArea):
|
||||
# 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)
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
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]
|
||||
|
||||
return get_pointer(gpointer)
|
||||
|
||||
def on_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
def on_mouse_motion(self, widget, event):
|
||||
display = widget.get_display()
|
||||
window = widget.get_window()
|
||||
cursor = Gdk.Cursor.new_from_name(display, "default")
|
||||
window.set_cursor(cursor)
|
||||
|
||||
self.hide_mouse_cursor(window, display)
|
||||
|
||||
@run_with_delay(3)
|
||||
def hide_mouse_cursor(self, window, display):
|
||||
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
|
||||
window.set_cursor(cursor)
|
||||
|
||||
@staticmethod
|
||||
def make(name, mode, widget):
|
||||
""" Factory method. We will not use a separate factory to return a specific implementation.
|
||||
@@ -190,7 +168,7 @@ class MpvPlayer(Player):
|
||||
try:
|
||||
from app.tools import mpv
|
||||
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle()),
|
||||
self._player = mpv.MPV(wid=str(self._handle),
|
||||
input_default_bindings=False,
|
||||
input_cursor=False,
|
||||
cursor_autohide="no")
|
||||
@@ -198,9 +176,6 @@ class MpvPlayer(Player):
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No libmpv is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
|
||||
def on_open(event):
|
||||
log("Starting playback...")
|
||||
@@ -241,8 +216,9 @@ class MpvPlayer(Player):
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
self._player.stop()
|
||||
self._is_playing = True
|
||||
if self._is_playing:
|
||||
self._player.stop()
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.pause = not self._player.pause
|
||||
@@ -252,8 +228,9 @@ class MpvPlayer(Player):
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._player.terminate()
|
||||
self.__INSTANCE = None
|
||||
if self._player:
|
||||
self._player.terminate()
|
||||
self.__INSTANCE = None
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
@@ -290,10 +267,8 @@ class GstPlayer(Player):
|
||||
self.STATE = Gst.State
|
||||
self.STAT_RETURN = Gst.StateChangeReturn
|
||||
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
self._player = Gst.ElementFactory.make("playbin", "player")
|
||||
self._player.set_window_handle(self.get_window_handle())
|
||||
self._player.set_window_handle(self._handle)
|
||||
|
||||
bus = self._player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
@@ -329,8 +304,9 @@ class GstPlayer(Player):
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
log("Stop playback...")
|
||||
self._player.set_state(self.STATE.READY)
|
||||
if self._is_playing:
|
||||
log("Stop playback...")
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
@@ -345,9 +321,10 @@ class GstPlayer(Player):
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._is_playing = False
|
||||
self._player.set_state(self.STATE.NULL)
|
||||
self.__INSTANCE = None
|
||||
if self._player:
|
||||
self._is_playing = False
|
||||
self._player.set_state(self.STATE.NULL)
|
||||
self.__INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_property("uri", mrl)
|
||||
@@ -412,7 +389,7 @@ class VlcPlayer(Player):
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
|
||||
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
|
||||
args = f"--quiet {'--no-xlib' if IS_LINUX else ''}"
|
||||
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)
|
||||
@@ -420,16 +397,13 @@ class VlcPlayer(Player):
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No VLC is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
ev_mgr = self._player.event_manager()
|
||||
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et: self.emit("position", self._player.get_time()))
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
|
||||
|
||||
self.init_video_widget(widget)
|
||||
self.init_video_widget()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget):
|
||||
@@ -449,7 +423,7 @@ class VlcPlayer(Player):
|
||||
def stop(self):
|
||||
if self._is_playing:
|
||||
self._player.stop()
|
||||
self._is_playing = False
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.pause()
|
||||
@@ -492,13 +466,13 @@ class VlcPlayer(Player):
|
||||
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):
|
||||
def init_video_widget(self):
|
||||
if IS_LINUX:
|
||||
self._player.set_xwindow(self.get_window_handle())
|
||||
self._player.set_xwindow(self._handle)
|
||||
elif IS_DARWIN:
|
||||
self._player.set_nsobject(self.get_window_handle())
|
||||
self._player.set_nsobject(self._handle)
|
||||
else:
|
||||
self._player.set_hwnd(self.get_window_handle())
|
||||
self._player.set_hwnd(self._handle)
|
||||
|
||||
|
||||
class Recorder:
|
||||
|
||||
@@ -29,12 +29,13 @@ from functools import partial, wraps
|
||||
from warnings import warn
|
||||
|
||||
if os.name == 'nt':
|
||||
dll = ctypes.util.find_library('mpv-1.dll')
|
||||
dll = ctypes.util.find_library('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll')
|
||||
if dll is None:
|
||||
raise OSError('Cannot find mpv-1.dll in your system %PATH%. One way to deal with this is to ship mpv-1.dll '
|
||||
'with your script and put the directory your script is in into %PATH% before "import mpv": '
|
||||
'os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
|
||||
'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
|
||||
raise OSError(
|
||||
'Cannot find [lib]mpv-*.dll in your system %PATH%. One way to deal with this is to ship [lib]mpv-*.dll '
|
||||
'with your script and put the directory your script is in into %PATH% before "import mpv": '
|
||||
'os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
|
||||
'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
|
||||
backend = CDLL(dll)
|
||||
fs_enc = 'utf-8'
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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
|
||||
@@ -221,7 +221,8 @@ class PiconsCzDownloader:
|
||||
"piconblack80": "b50",
|
||||
"piconblack3d": "b50",
|
||||
"piconwin11": "win11220",
|
||||
"piconSNPtransparent": "t50"
|
||||
"piconSNPtransparent": "t50",
|
||||
"piconSNPblack": "b50",
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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
|
||||
@@ -50,7 +50,7 @@ class SatelliteSource(Enum):
|
||||
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",)
|
||||
KINGOFSAT = ("https://en.kingofsat.tv/satellites.php",)
|
||||
|
||||
@staticmethod
|
||||
def get_sources(src):
|
||||
@@ -271,7 +271,7 @@ class SatellitesParser(HTMLParser):
|
||||
trs = []
|
||||
|
||||
if self._source is SatelliteSource.KINGOFSAT:
|
||||
sat_url = "https://en.kingofsat.net/" + sat_url
|
||||
sat_url = f"https://en.kingofsat.tv/{sat_url}"
|
||||
|
||||
try:
|
||||
request = requests.get(url=sat_url, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
@@ -429,7 +429,7 @@ class SatellitesParser(HTMLParser):
|
||||
class ServicesParser(HTMLParser):
|
||||
""" Services parser for LYNGSAT source. """
|
||||
|
||||
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' '):
|
||||
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' ', lang=None):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
@@ -452,6 +452,12 @@ class ServicesParser(HTMLParser):
|
||||
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._lang = "en"
|
||||
if lang:
|
||||
langs = {"en", "fr", "nl", "de", "se", "no", "pt", "es", "it", "pl",
|
||||
"cz", "gr", "fi", "ar", "tr", "ru", "sc", "ro", "hu", "sq"}
|
||||
lang, _, _ = lang.partition("_")
|
||||
self._lang = lang if lang in langs else self._lang
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
@@ -565,7 +571,7 @@ class ServicesParser(HTMLParser):
|
||||
""" Returns transponder links. """
|
||||
try:
|
||||
if self._source is SatelliteSource.KINGOFSAT:
|
||||
sat_url = "https://en.kingofsat.net/" + sat_url
|
||||
sat_url = f"https://en.kingofsat.tv/{sat_url}"
|
||||
self.init_data(sat_url)
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
@@ -580,7 +586,7 @@ class ServicesParser(HTMLParser):
|
||||
if len(r) == 13 and SatellitesParser.POS_PAT.match(r[0].text):
|
||||
t_cell = r[4]
|
||||
if t_cell.url and t_cell.url.startswith("tp.php?tp="):
|
||||
t_cell.url = f"https://en.kingofsat.net/{t_cell.url}"
|
||||
t_cell.url = f"https://{self._lang}.kingofsat.tv/{t_cell.url}"
|
||||
t_cell.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
|
||||
trs.append(t_cell)
|
||||
return trs
|
||||
@@ -705,6 +711,10 @@ class ServicesParser(HTMLParser):
|
||||
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)
|
||||
if not all((freq, nid, tid)):
|
||||
log(f"Error. Not enough parameters [Frequency={freq}, NID={nid}, TID={tid}].")
|
||||
continue
|
||||
|
||||
freq, nid, tid = int(float(freq)), int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
@@ -730,11 +740,12 @@ class ServicesParser(HTMLParser):
|
||||
|
||||
s_type = self._S_TYPES.get(s_type, "3")
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
|
||||
reg, grp = r[3].text, r[4].text
|
||||
|
||||
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,
|
||||
services.append(Service(flags, "s", None, name, reg, grp, pkg, _s_type, None, picon_id,
|
||||
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
|
||||
|
||||
return services
|
||||
@@ -743,9 +754,9 @@ class ServicesParser(HTMLParser):
|
||||
""" Returns converted transponder data. """
|
||||
sys = get_key_by_value(SYSTEM, sys)
|
||||
mod = get_key_by_value(MODULATION, mod)
|
||||
fec = get_key_by_value(FEC, fec)
|
||||
fec = get_key_by_value(FEC, fec) or "0"
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = f"{3600 - pos if pos < 0 else pos:04x}0000"
|
||||
namespace = f"{3600 - abs(pos) if pos < 0 else pos:04x}0000"
|
||||
tr_flag = 1
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
|
||||
5491
app/tools/vlc.py
5491
app/tools/vlc.py
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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
|
||||
@@ -111,7 +111,7 @@ class YouTube:
|
||||
if not self._yt_dl:
|
||||
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
|
||||
if not self._yt_dl:
|
||||
raise YouTubeException("youtube-dl initialization error.")
|
||||
raise YouTubeException("yt-dlp initialization error.")
|
||||
return self._yt_dl.get_yt_link(url, skip_errors)
|
||||
|
||||
return self.get_yt_link_by_id(video_id)
|
||||
@@ -129,8 +129,7 @@ class YouTube:
|
||||
fmts = streaming_data.get("formats", None) if streaming_data else 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}
|
||||
links = {Quality[i["itag"]]: i["url"] for i in fmts if i.get("itag", -1) in Quality and "url" in i}
|
||||
|
||||
if links and title:
|
||||
return links, title.replace("+", " ")
|
||||
@@ -149,7 +148,7 @@ class YouTube:
|
||||
if self._settings.enable_yt_dl and url:
|
||||
try:
|
||||
if not self._yt_dl:
|
||||
raise YouTubeException("youtube-dl is not initialized!")
|
||||
raise YouTubeException("yt-dlp is not initialized!")
|
||||
|
||||
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
|
||||
info = self._yt_dl.get_info(url, skip_errors=False)
|
||||
@@ -173,17 +172,22 @@ class InnerTube:
|
||||
_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"
|
||||
}
|
||||
"WEB_EMBED": {"context": {"client": {"clientName": "WEB_EMBEDDED_PLAYER",
|
||||
"clientVersion": "2.20210721.00.00",
|
||||
"clientScreen": "EMBED"}},
|
||||
"header": {"User-Agent": "Mozilla/5.0"},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"},
|
||||
|
||||
"ANDROID_EMBED": {"context": {"client": {"clientName": "ANDROID_EMBEDDED_PLAYER",
|
||||
"clientVersion": "17.31.35",
|
||||
"clientScreen": "EMBED",
|
||||
"androidSdkVersion": 30}},
|
||||
"header": {"User-Agent": "com.google.android.youtube/"},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"}
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, client="ANDROID"):
|
||||
def __init__(self, client="ANDROID_EMBED"):
|
||||
""" Initialize an InnerTube object.
|
||||
|
||||
@param client: Client to use for the object. Default to web because it returns the most playback types.
|
||||
@@ -303,14 +307,14 @@ class PlayListParser(HTMLParser):
|
||||
|
||||
|
||||
class YouTubeDL:
|
||||
""" Utility class [experimental] for working with youtube-dl.
|
||||
""" Utility class [experimental] for working with yt-dlp.
|
||||
|
||||
[https://github.com/ytdl-org/youtube-dl]
|
||||
[https://github.com/yt-dlp/yt-dlp]
|
||||
"""
|
||||
|
||||
_DL_INSTANCE = None
|
||||
_DownloadError = None
|
||||
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
|
||||
_LATEST_RELEASE_URL = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest"
|
||||
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
|
||||
"extract_flat": False, # Do not resolve URLs, return the immediate result.
|
||||
"quiet": True, # Do not print messages to stdout.
|
||||
@@ -335,7 +339,7 @@ class YouTubeDL:
|
||||
return cls._DL_INSTANCE
|
||||
|
||||
def init(self):
|
||||
if not os.path.isfile(f"{self._path}youtube_dl{SEP}version.py"):
|
||||
if not os.path.isfile(f"{self._path}yt_dlp{SEP}version.py"):
|
||||
self.get_latest_release()
|
||||
|
||||
if self._path not in sys.path:
|
||||
@@ -345,39 +349,39 @@ class YouTubeDL:
|
||||
|
||||
def init_dl(self):
|
||||
try:
|
||||
import youtube_dl
|
||||
import yt_dlp
|
||||
except ModuleNotFoundError as e:
|
||||
log(f"YouTubeDLHelper error: {e}")
|
||||
raise YouTubeException(e)
|
||||
except ImportError as 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!"
|
||||
if self._path not in yt_dlp.__file__:
|
||||
msg = "Another version of yt-dlp was found on your system!"
|
||||
log(msg)
|
||||
raise YouTubeException(msg)
|
||||
|
||||
if self._update:
|
||||
if hasattr(youtube_dl.version, "__version__"):
|
||||
if hasattr(yt_dlp.version, "__version__"):
|
||||
l_ver = self.get_last_release_id()
|
||||
cur_ver = youtube_dl.version.__version__
|
||||
if l_ver and youtube_dl.version.__version__ < l_ver:
|
||||
msg = f"youtube-dl has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
|
||||
cur_ver = yt_dlp.version.__version__
|
||||
if l_ver and yt_dlp.version.__version__ < l_ver:
|
||||
msg = f"yt-dlp has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
self.get_latest_release()
|
||||
|
||||
self._DownloadError = youtube_dl.utils.DownloadError
|
||||
self._dl = youtube_dl.YoutubeDL(self._OPTIONS)
|
||||
msg = "youtube-dl initialized..."
|
||||
self._DownloadError = yt_dlp.utils.DownloadError
|
||||
self._dl = yt_dlp.YoutubeDL(self._OPTIONS)
|
||||
msg = "yt-dlp initialized..."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
|
||||
@staticmethod
|
||||
def get_last_release_id():
|
||||
""" Getting last release id. """
|
||||
url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
|
||||
url = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest"
|
||||
try:
|
||||
with urlopen(url, timeout=10) as resp:
|
||||
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
|
||||
@@ -387,7 +391,7 @@ class YouTubeDL:
|
||||
def get_latest_release(self):
|
||||
try:
|
||||
self._is_update_process = True
|
||||
log("Getting the last youtube-dl release...")
|
||||
log("Getting the last yt-dlp release...")
|
||||
|
||||
with urlopen(YouTubeDL._LATEST_RELEASE_URL, timeout=10) as resp:
|
||||
r = json.loads(resp.read().decode("utf-8"))
|
||||
@@ -396,7 +400,7 @@ class YouTubeDL:
|
||||
if os.path.isdir(self._path):
|
||||
shutil.rmtree(self._path)
|
||||
|
||||
zip_file = self._path + "yt.zip"
|
||||
zip_file = f"{self._path}yt.zip"
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
f_name, headers = urlretrieve(zip_url, filename=zip_file)
|
||||
|
||||
@@ -404,12 +408,12 @@ class YouTubeDL:
|
||||
|
||||
with zipfile.ZipFile(f_name) as arch:
|
||||
for info in arch.infolist():
|
||||
pref, sep, f = info.filename.partition("/youtube_dl/")
|
||||
pref, sep, f = info.filename.partition("/yt_dlp/")
|
||||
if sep:
|
||||
arch.extract(info.filename)
|
||||
shutil.move(info.filename, f"{self._path}{sep}{f}")
|
||||
shutil.rmtree(pref)
|
||||
msg = "Getting the last youtube-dl release is done!"
|
||||
msg = "Getting the last yt-dlp release is done!"
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
|
||||
@@ -424,14 +424,6 @@
|
||||
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
|
||||
<attribute name="action">app.on_import_yt_list</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import m3u</attribute>
|
||||
<attribute name="action">app.on_import_m3u</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG configuration</attribute>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -38,7 +38,7 @@ from pathlib import Path
|
||||
from app.commons import run_idle, get_size_from_bytes
|
||||
from app.settings import SettingsType, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.main_helper import append_text_to_tview, show_info_bar_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, HeaderBar
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ class BackupDialog:
|
||||
self._model = builder.get_object("main_list_store")
|
||||
self._main_view = builder.get_object("main_view")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._text_view_scrolled_window = builder.get_object("text_view_scrolled_window")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
@@ -149,16 +148,12 @@ class BackupDialog:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
def on_info_button_toggled(self, button):
|
||||
active = button.get_active()
|
||||
self._text_view_scrolled_window.set_visible(active)
|
||||
if active:
|
||||
if button.get_active():
|
||||
self.on_cursor_changed(self._main_view)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
@@ -196,7 +191,7 @@ class BackupDialog:
|
||||
return
|
||||
|
||||
file_name = model.get_value(model.get_iter(paths[0]), 0)
|
||||
full_file_name = self._backup_path + file_name + ".zip"
|
||||
full_file_name = f"{self._backup_path}{file_name}.zip"
|
||||
|
||||
try:
|
||||
if restore_type is RestoreType.ALL:
|
||||
@@ -245,8 +240,8 @@ def backup_data(path, backup_path, move=True):
|
||||
backup_path = f"{backup_path}{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}{SEP}"
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# Backup files in data dir(skipping dirs and *.xml).
|
||||
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
# Backup files in data dir.
|
||||
for file in filter(lambda f: os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
src, dst = os.path.join(path, file), backup_path + file
|
||||
shutil.move(src, dst) if move else shutil.copy(src, dst)
|
||||
# Compressing to zip and delete remaining files.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -27,7 +27,8 @@ Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
@@ -35,8 +36,8 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
@@ -49,23 +50,21 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<object class="GtkMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<object class="GtkMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
@@ -73,16 +72,14 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<object class="GtkMenuItem" id="remove_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
@@ -90,62 +87,59 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_all_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-select-all-symbolic</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-select-all-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_bouquets_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-revert-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="width_request">560</property>
|
||||
<property name="height_request">320</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="width-request">560</property>
|
||||
<property name="height-request">320</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="title" translatable="yes">Backups</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="icon_name">document-revert</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="icon-name">document-revert</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="main_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">15</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<property name="margin-start">15</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_bouquets_header_button">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">restore_bouquets_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -158,11 +152,11 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkButton" id="restore_all_header_button">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">restore_all_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_restore_all" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -175,11 +169,11 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkButton" id="remove_header_button">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">remove_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="clicked"/>
|
||||
</object>
|
||||
@@ -199,19 +193,19 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Details</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="margin-end">15</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="details_image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -220,7 +214,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -237,121 +231,130 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkFrame" id="main_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<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="can-focus">True</property>
|
||||
<property name="wide-handle">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="backups_box">
|
||||
<object class="GtkViewport" id="backups_viewport">
|
||||
<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">False</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
<object class="GtkBox" id="backups_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-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="main_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_name_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Name</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="name_renderer">
|
||||
<property name="xpad">10</property>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="search-column">0</property>
|
||||
<property name="rubber-banding">True</property>
|
||||
<property name="activate-on-single-click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="backup_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_name_column">
|
||||
<property name="min-width">75</property>
|
||||
<property name="title" translatable="yes">Name</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="name_renderer">
|
||||
<property name="xpad">10</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">120</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="size_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">120</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="size_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_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">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="file_count_label">
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width_chars">4</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</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="file_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width-chars">4</property>
|
||||
<property name="xalign">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>
|
||||
@@ -360,12 +363,10 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
@@ -373,22 +374,36 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<object class="GtkViewport" id="text_viewport">
|
||||
<property name="visible" bind-source="info_check_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pixels_above_lines">5</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">10</property>
|
||||
<property name="right_margin">10</property>
|
||||
<property name="indent">10</property>
|
||||
<property name="cursor_visible">False</property>
|
||||
<property name="accepts_tab">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="pixels-above-lines">5</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left-margin">10</property>
|
||||
<property name="right-margin">10</property>
|
||||
<property name="indent">10</property>
|
||||
<property name="cursor-visible">False</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
@@ -409,14 +424,14 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<property name="layout-style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -426,13 +441,13 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
@@ -336,10 +336,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="network_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="network_scrolled_window">
|
||||
@@ -422,6 +422,9 @@ Author: Dmitriy Yefremov
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -439,8 +442,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="info_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-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
@@ -448,10 +451,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkViewport" id="screenshot_view_port">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">2</property>
|
||||
<property name="margin-right">2</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<child>
|
||||
<object class="GtkDrawingArea" id="screenshot_area">
|
||||
<property name="can-focus">False</property>
|
||||
@@ -469,8 +472,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="remote_signal_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">25</property>
|
||||
<property name="margin-right">25</property>
|
||||
<property name="margin-start">25</property>
|
||||
<property name="margin-end">25</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
@@ -620,6 +623,9 @@ Author: Dmitriy Yefremov
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@@ -652,8 +658,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin-left">5</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="row-spacing">5</property>
|
||||
@@ -1336,6 +1342,9 @@ audio-volume-medium-symbolic</property>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -32,7 +32,7 @@ import re
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from .dialogs import get_builder, get_message
|
||||
from .dialogs import get_builder, translate
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI
|
||||
@@ -41,8 +41,8 @@ from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
|
||||
class ControlTool(Gtk.Box):
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, app, settings, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._settings = settings
|
||||
self._app = app
|
||||
@@ -335,7 +335,7 @@ class ControlTool(Gtk.Box):
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
state = get_message("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
|
||||
state = translate("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
|
||||
GLib.idle_add(self._network_model.set_value, itr, 2, state)
|
||||
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -40,8 +40,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="icon_name">system-help</property>
|
||||
<property name="type_hint">normal</property>
|
||||
<property name="program_name">DemonEditor</property>
|
||||
<property name="version">3.4.2 Beta</property>
|
||||
<property name="copyright">2018-2023 Dmitriy Yefremov
|
||||
<property name="version">3.9.0 Alpha</property>
|
||||
<property name="copyright">2018-2024 Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
|
||||
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
|
||||
|
||||
@@ -38,6 +38,25 @@ from app.settings import SEP, IS_WIN, USE_HEADER_BAR
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
|
||||
|
||||
class BaseDialog(Gtk.Dialog):
|
||||
""" Base dialog class for editing DVB (-> *.xml) data. """
|
||||
DEFAULT_BUTTONS = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)
|
||||
|
||||
def __init__(self, parent, title, buttons=None, *args, **kwargs):
|
||||
super().__init__(transient_for=parent,
|
||||
title=translate(title),
|
||||
modal=True,
|
||||
resizable=False,
|
||||
default_width=240,
|
||||
skip_taskbar_hint=True,
|
||||
skip_pager_hint=True,
|
||||
destroy_with_parent=True,
|
||||
use_header_bar=USE_HEADER_BAR,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
buttons=buttons or self.DEFAULT_BUTTONS,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
class Dialog(Enum):
|
||||
MESSAGE = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -91,7 +110,7 @@ class WaitDialog:
|
||||
|
||||
@run_idle
|
||||
def set_text(self, text):
|
||||
self._label.set_text(get_message(text or self._default_text))
|
||||
self._label.set_text(translate(text or self._default_text))
|
||||
|
||||
@run_idle
|
||||
def hide(self):
|
||||
@@ -135,7 +154,7 @@ def get_chooser_dialog(transient, settings, name, patterns, title=None, file_fil
|
||||
|
||||
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
|
||||
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
|
||||
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
|
||||
dialog = Gtk.FileChooserNative.new(translate(title) if title else "", transient, action_type)
|
||||
dialog.set_create_folders(dirs)
|
||||
dialog.set_modal(True)
|
||||
|
||||
@@ -174,7 +193,7 @@ def get_message_dialog(transient, message_type, buttons_type, text):
|
||||
builder.add_from_string(dialog_str)
|
||||
dialog = builder.get_object("message_dialog")
|
||||
dialog.set_transient_for(transient)
|
||||
dialog.set_markup(get_message(text))
|
||||
dialog.set_markup(translate(text))
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
@@ -202,7 +221,7 @@ def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
|
||||
return builder, dialog
|
||||
|
||||
|
||||
def get_message(message):
|
||||
def translate(message):
|
||||
""" returns translated message """
|
||||
return gettext.dgettext(TEXT_DOMAIN, message)
|
||||
|
||||
@@ -246,9 +265,9 @@ def translate_xml(path, tag="property"):
|
||||
root = et.getroot()
|
||||
for e in root.iter():
|
||||
if e.tag == tag and e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
e.text = translate(e.text)
|
||||
elif e.tag == "item" and e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
e.text = translate(e.text)
|
||||
|
||||
return ET.tostring(root, encoding="unicode", method="xml")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
|
||||
""" Module for working with EPG. """
|
||||
import abc
|
||||
import gzip
|
||||
import locale
|
||||
import os
|
||||
@@ -35,6 +36,8 @@ import shutil
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from hashlib import sha1
|
||||
from itertools import chain
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -45,10 +48,11 @@ from app.connections import download_data, DownloadType, HttpAPI
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.settings import SEP, EpgSource, IS_WIN
|
||||
from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
|
||||
from app.ui.dialogs import translate, show_dialog, DialogType, get_builder, get_chooser_dialog
|
||||
from app.ui.tasks import BGTaskWidget
|
||||
from app.ui.timers import TimerTool
|
||||
from ..main_helper import on_popup_menu, update_entry_data, scroll_to, update_toggle_model, update_filter_sat_positions
|
||||
from ..main_helper import on_popup_menu, update_entry_data, scroll_to, update_toggle_model, update_filter_sat_positions, \
|
||||
show_info_bar_message
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, Page, HeaderBar
|
||||
|
||||
|
||||
@@ -57,32 +61,102 @@ class RefsSource(Enum):
|
||||
XML = 1
|
||||
|
||||
|
||||
class EpgCache(dict):
|
||||
class EpgCache(abc.ABC):
|
||||
def __init__(self, app):
|
||||
super().__init__()
|
||||
self._current_bq = None
|
||||
self.events = {}
|
||||
|
||||
self._reader = None
|
||||
self._canceled = False
|
||||
self._is_run = False
|
||||
self._current_bq = app.current_bouquet
|
||||
self._page = Page.SERVICES
|
||||
|
||||
self._settings = app.app_settings
|
||||
self._src = self._settings.epg_source
|
||||
self._src = EpgSource.XML
|
||||
self._xml_src = None
|
||||
self._path = None
|
||||
|
||||
self._app = app
|
||||
self._app.connect("bouquet-changed", self.on_bouquet_changed)
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
self._app.connect("epg-settings-changed", self.on_settings_changed)
|
||||
self._app.connect("task-canceled", self.on_xml_load_cancel)
|
||||
|
||||
self.init()
|
||||
@property
|
||||
def current_reader(self):
|
||||
return self._reader
|
||||
|
||||
@property
|
||||
def is_run(self):
|
||||
return self._is_run
|
||||
|
||||
@property
|
||||
def current_gz_file_name(self):
|
||||
return self.get_gz_file_name(self._settings.epg_xml_source, self._settings.profile_data_path)
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
|
||||
def on_profile_changed(self, app, p):
|
||||
self._xml_src = self._settings.epg_xml_source
|
||||
self.reset()
|
||||
|
||||
def on_settings_changed(self, app, page):
|
||||
if page is self._page:
|
||||
self.on_profile_changed(app, page)
|
||||
|
||||
def on_xml_load_cancel(self, app, widget):
|
||||
self._canceled = True
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_epg_data(self) -> bool: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_current_event(self, service_name) -> EpgEvent: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_current_events(self, service_name) -> list[EpgEvent]: pass
|
||||
|
||||
@staticmethod
|
||||
def get_gz_file_name(url, path):
|
||||
f_sha1 = sha1(url.encode("utf-8", errors="ignore")).hexdigest()
|
||||
return f"{path}epg{os.sep}{f_sha1}_epg.gz"
|
||||
|
||||
|
||||
class FavEpgCache(EpgCache):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self._app.connect("epg-cache-initialized", self.on_cache_initialized)
|
||||
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.init)
|
||||
|
||||
def on_cache_initialized(self, app, cache):
|
||||
if cache is not self:
|
||||
return
|
||||
|
||||
self._is_run = True
|
||||
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.update_epg_data, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@run_with_delay(5)
|
||||
def init(self):
|
||||
self._src = self._settings.epg_source
|
||||
self._xml_src = self._settings.epg_xml_source
|
||||
self._is_run = False
|
||||
if self._src is EpgSource.XML:
|
||||
url = self._settings.epg_xml_source
|
||||
gz_file = f"{self._settings.profile_data_path}epg{os.sep}epg.gz"
|
||||
gz_file = self.current_gz_file_name
|
||||
self._reader = XmlTvReader(gz_file, url)
|
||||
|
||||
@run_with_delay(2)
|
||||
def process_data():
|
||||
t = BGTaskWidget(self._app, "Processing XMLTV data...", self._reader.parse, )
|
||||
def process():
|
||||
self._reader.parse()
|
||||
GLib.idle_add(self._app.emit, "epg-cache-initialized", self)
|
||||
|
||||
t = BGTaskWidget(self._app, "Processing XMLTV data...", process, )
|
||||
self._app.emit("add-background-task", t)
|
||||
|
||||
if os.path.isfile(gz_file):
|
||||
@@ -102,43 +176,138 @@ class EpgCache(dict):
|
||||
self._reader = EPG.DatReader(f"{self._settings.profile_data_path}epg{os.sep}epg.dat")
|
||||
self._reader.download()
|
||||
|
||||
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.update_epg_data, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
|
||||
def on_profile_changed(self, app, p):
|
||||
self.clear()
|
||||
|
||||
def on_xml_load_cancel(self, app, widget):
|
||||
self._canceled = True
|
||||
def reset(self) -> None:
|
||||
self.events.clear()
|
||||
if self._is_run:
|
||||
self._is_run = False
|
||||
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.init)
|
||||
|
||||
def update_epg_data(self):
|
||||
if self._src is EpgSource.HTTP:
|
||||
api = self._app.http_api
|
||||
bq = self._app.current_bouquet_files.get(self._current_bq, None)
|
||||
|
||||
api, bq = self._app.http_api, self._app.current_bouquet_files.get(self._current_bq, None)
|
||||
if bq and api:
|
||||
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"')
|
||||
req = quote(f'FROM BOUQUET "{bq}"')
|
||||
api.send(HttpAPI.Request.EPG_NOW, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_http_data)
|
||||
elif self._src is EpgSource.XML:
|
||||
self.update_xml_data()
|
||||
|
||||
return self._app.display_epg
|
||||
return self._app.display_epg and self._is_run
|
||||
|
||||
def update_http_data(self, epg):
|
||||
for e in (EpgTool.get_event(e, False) for e in epg.get("event_list", []) if e.get("e2eventid", "").isdigit()):
|
||||
self[e.event_data.get("e2eventservicename", "")] = e
|
||||
self.events[e.event_data.get("e2eventservicename", "")] = e
|
||||
|
||||
@run_task
|
||||
def update_xml_data(self):
|
||||
services = self._app.current_services
|
||||
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, [])}
|
||||
for name, e in self._reader.get_current_events(names).items():
|
||||
self[name] = e
|
||||
for name, events in self._reader.get_current_events(names).items():
|
||||
ev = min(events, key=lambda x: x.start, default=None)
|
||||
if ev:
|
||||
self.events[name] = ev
|
||||
|
||||
def get_current_event(self, service_name):
|
||||
return self.get(service_name, EpgEvent())
|
||||
return self.events.get(service_name, EpgEvent())
|
||||
|
||||
def get_current_events(self, service_name):
|
||||
return [EpgEvent()]
|
||||
|
||||
|
||||
class TabEpgCache(EpgCache):
|
||||
|
||||
def __init__(self, app, path=None, url=None):
|
||||
super().__init__(app)
|
||||
self._page = Page.EPG
|
||||
self._path = path or self.current_gz_file_name
|
||||
self._xml_src = url
|
||||
self._task = None
|
||||
|
||||
self._app.connect("epg-cache-initialized", self.on_cache_initialized)
|
||||
|
||||
self.init()
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
self.update_epg_data()
|
||||
|
||||
def init(self):
|
||||
self._is_run = True
|
||||
self._reader = XmlTvReader(self._path, url=self._xml_src)
|
||||
if self._canceled:
|
||||
return
|
||||
|
||||
if self._app.display_epg and self._xml_src == self._settings.epg_xml_source:
|
||||
ext_cache = self._app.current_epg_cache
|
||||
if ext_cache and ext_cache.is_run:
|
||||
self._app.emit("epg-cache-initialized", ext_cache)
|
||||
return
|
||||
|
||||
self.load_data()
|
||||
|
||||
def load_data(self):
|
||||
if os.path.isfile(self._path):
|
||||
if self._xml_src:
|
||||
# Difference calculation between the current time and file modification.
|
||||
dif = datetime.now() - datetime.fromtimestamp(os.path.getmtime(self._path))
|
||||
# We will update daily. -> Temporarily!!! Skip download if FAV cache is enabled.
|
||||
if dif.days > 0 and not self._app.display_epg:
|
||||
self._task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download,
|
||||
self.process_data, )
|
||||
self._app.emit("add-background-task", self._task)
|
||||
else:
|
||||
self.process_data()
|
||||
else:
|
||||
self._task = BGTaskWidget(self._app, "", self.process_data, )
|
||||
else:
|
||||
self._task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, self.process_data, )
|
||||
self._app.emit("add-background-task", self._task)
|
||||
|
||||
def on_cache_initialized(self, app, cache):
|
||||
if isinstance(cache, FavEpgCache):
|
||||
reader = cache.current_reader
|
||||
if reader:
|
||||
self._reader.cache.update(reader.cache)
|
||||
self._is_run = False
|
||||
else:
|
||||
if not self._app.display_epg or self._settings.epg_source is not EpgSource.XML or self._xml_src is None:
|
||||
self._is_run = False
|
||||
|
||||
self.update_epg_data()
|
||||
|
||||
@run_task
|
||||
def process_data(self):
|
||||
def process():
|
||||
# Skip data parsing data if epg display is enabled and EPG src is XMLTV.
|
||||
if not all((self._xml_src, self._app.display_epg, self._settings.epg_source is EpgSource.XML)):
|
||||
self._reader.parse()
|
||||
self._task = None
|
||||
GLib.idle_add(self._app.emit, "epg-cache-initialized", self)
|
||||
|
||||
self._task = BGTaskWidget(self._app, "Processing XMLTV data...", process, )
|
||||
GLib.idle_add(self._app.emit, "add-background-task", self._task)
|
||||
|
||||
def reset(self) -> None:
|
||||
self._is_run = False
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
|
||||
self.init()
|
||||
|
||||
def update_epg_data(self) -> bool:
|
||||
services = self._app.current_services
|
||||
names = {services[s].service for s in chain.from_iterable(self._app.current_bouquets.values())}
|
||||
for name, events in self._reader.get_current_events(names).items():
|
||||
self.events[name] = events
|
||||
|
||||
self._app.emit("epg-cache-updated", self)
|
||||
|
||||
return self._is_run
|
||||
|
||||
def get_current_event(self, service_name) -> EpgEvent:
|
||||
pass
|
||||
|
||||
def get_current_events(self, service_name) -> list[EpgEvent]:
|
||||
return self.events.get(service_name, [])
|
||||
|
||||
|
||||
class EpgSettingsPopover(Gtk.Popover):
|
||||
@@ -148,17 +317,29 @@ class EpgSettingsPopover(Gtk.Popover):
|
||||
self._app = app
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
|
||||
handlers = {"on_apply": self.on_apply,
|
||||
"on_close": lambda b: self.popdown()}
|
||||
handlers = {"on_add_url": self.on_ad_url,
|
||||
"on_remove_url": self.on_remove_url,
|
||||
"on_apply_url": self.on_apply_url,
|
||||
"on_url_entry_focus_out": self.on_url_entry_focus_out,
|
||||
"on_apply": self.on_apply,
|
||||
"on_close": self.on_close}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}settings.glade", handlers)
|
||||
self.add(builder.get_object("main_box"))
|
||||
|
||||
self._src_selection_box = builder.get_object("source_selection_box")
|
||||
self._xml_source_box = builder.get_object("xml_source_box")
|
||||
self._download_interval_box = builder.get_object("download_interval_box")
|
||||
self._interval_box = builder.get_object("interval_box")
|
||||
self._http_src_button = builder.get_object("http_src_button")
|
||||
self._xml_src_button = builder.get_object("xml_src_button")
|
||||
self._dat_src_button = builder.get_object("dat_src_button")
|
||||
self._interval_button = builder.get_object("interval_button")
|
||||
self._download_interval_button = builder.get_object("download_interval_button")
|
||||
self._url_combo_box = builder.get_object("url_combo_box")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._dat_path_box = builder.get_object("dat_path_box")
|
||||
self._remove_url_button = builder.get_object("remove_url_button")
|
||||
|
||||
self.init()
|
||||
|
||||
@@ -173,46 +354,129 @@ class EpgSettingsPopover(Gtk.Popover):
|
||||
self._dat_src_button.set_active(True)
|
||||
|
||||
self._interval_button.set_value(settings.epg_update_interval)
|
||||
self._url_entry.set_text(settings.epg_xml_source)
|
||||
self._dat_path_box.set_active_id(settings.epg_dat_path)
|
||||
self._url_combo_box.get_model().clear()
|
||||
[self._url_combo_box.append(i, i) for i in settings.epg_xml_sources if i]
|
||||
self._url_combo_box.set_active_id(settings.epg_xml_source)
|
||||
self._remove_url_button.set_sensitive(len(self._url_combo_box.get_model()) > 1)
|
||||
|
||||
def on_ad_url(self, button):
|
||||
self._url_entry.set_can_focus(True)
|
||||
self._url_entry.grab_focus()
|
||||
|
||||
def on_remove_url(self, button):
|
||||
self._url_combo_box.remove(self._url_combo_box.get_active())
|
||||
self._url_combo_box.set_active(0)
|
||||
self._remove_url_button.set_sensitive(len(self._url_combo_box.get_model()) > 1)
|
||||
|
||||
def on_apply_url(self, button):
|
||||
url = self._url_entry.get_text()
|
||||
ids = {r[0] for r in self._url_combo_box.get_model()}
|
||||
if url in ids:
|
||||
self._app.show_error_message("This URL already exists!")
|
||||
return True
|
||||
|
||||
self._url_combo_box.append(url, url)
|
||||
self._url_combo_box.set_active_id(url)
|
||||
self._download_interval_button.grab_focus()
|
||||
self._remove_url_button.set_sensitive(len(ids))
|
||||
|
||||
def on_url_entry_focus_out(self, entry, event):
|
||||
entry.set_can_focus(False)
|
||||
active = self._url_combo_box.get_active_id()
|
||||
txt = entry.get_text()
|
||||
if active != txt:
|
||||
entry.set_text(active or "")
|
||||
|
||||
def on_apply(self, button):
|
||||
settings = self._app.app_settings
|
||||
if self._http_src_button.get_active():
|
||||
settings.epg_source = EpgSource.HTTP
|
||||
src = EpgSource.HTTP
|
||||
elif self._xml_src_button.get_active():
|
||||
settings.epg_source = EpgSource.XML
|
||||
src = EpgSource.XML
|
||||
else:
|
||||
settings.epg_source = EpgSource.DAT
|
||||
src = EpgSource.DAT
|
||||
|
||||
settings.epg_update_interval = self._interval_button.get_value()
|
||||
settings.epg_xml_source = self._url_entry.get_text()
|
||||
settings.epg_dat_path = self._dat_path_box.get_active_id()
|
||||
xml_src = self._url_combo_box.get_active_id()
|
||||
update_interval = self._interval_button.get_value()
|
||||
dat_path = self._dat_path_box.get_active_id()
|
||||
|
||||
if any((src != settings.epg_source,
|
||||
xml_src != settings.epg_xml_source,
|
||||
update_interval != settings.epg_update_interval,
|
||||
dat_path != settings.epg_dat_path)):
|
||||
self._app.emit("epg-settings-changed", Page.SERVICES)
|
||||
|
||||
settings.epg_update_interval = update_interval
|
||||
settings.epg_source = src
|
||||
settings.epg_xml_source = xml_src
|
||||
settings.epg_xml_sources = [r[0] for r in self._url_combo_box.get_model()]
|
||||
settings.epg_dat_path = dat_path
|
||||
self.popdown()
|
||||
|
||||
self._app.change_action_state("display_epg", GLib.Variant.new_boolean(True))
|
||||
def on_close(self, button):
|
||||
self.init()
|
||||
self.popdown()
|
||||
|
||||
def on_profile_changed(self, app, p):
|
||||
self.init()
|
||||
|
||||
|
||||
class TabEpgSettingsPopover(EpgSettingsPopover):
|
||||
|
||||
def init(self):
|
||||
self._xml_src_button.set_active(True)
|
||||
self._http_src_button.set_visible(False)
|
||||
self._src_selection_box.set_visible(False)
|
||||
self._interval_box.set_visible(False)
|
||||
self._xml_source_box.set_margin_top(5)
|
||||
|
||||
settings = self._app.app_settings
|
||||
self._interval_button.set_value(settings.epg_update_interval)
|
||||
self._url_combo_box.get_model().clear()
|
||||
[self._url_combo_box.append(i, i) for i in settings.epg_xml_sources if i]
|
||||
self._url_combo_box.set_active_id(settings.epg_xml_source)
|
||||
|
||||
def on_apply(self, button):
|
||||
settings = self._app.app_settings
|
||||
xml_src = self._url_combo_box.get_active_id()
|
||||
|
||||
if xml_src != settings.epg_xml_source:
|
||||
settings.epg_xml_source = xml_src
|
||||
self._app.emit("epg-settings-changed", Page.EPG)
|
||||
|
||||
settings.epg_xml_sources = [r[0] for r in self._url_combo_box.get_model()]
|
||||
self.popdown()
|
||||
|
||||
|
||||
class EpgTool(Gtk.Box):
|
||||
# Batch size to data load in one pass.
|
||||
LOAD_FACTOR = 100
|
||||
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._current_bq = None
|
||||
self._epg_cache = None
|
||||
self._src = EpgSource.HTTP
|
||||
self._current_bq = app.current_bouquet
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-open", self.on_data_open)
|
||||
self._app.connect("data-extract", self.on_data_extract)
|
||||
self._app.connect("fav-changed", self.on_service_changed)
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
self._app.connect("bouquet-changed", self.on_bouquet_changed)
|
||||
self._app.connect("epg-cache-updated", self.on_epg_cache_updated)
|
||||
self._app.connect("epg-display-changed", self.on_epg_display_changed)
|
||||
self._app.connect("filter-toggled", self.on_filter_toggled)
|
||||
|
||||
handlers = {"on_epg_press": self.on_epg_press,
|
||||
"on_timer_add": self.on_timer_add,
|
||||
"on_epg_filter_changed": self.on_epg_filter_changed,
|
||||
handlers = {"on_epg_filter_changed": self.on_epg_filter_changed,
|
||||
"on_epg_filter_toggled": self.on_epg_filter_toggled,
|
||||
"on_view_query_tooltip": self.on_view_query_tooltip,
|
||||
"on_multi_epg_toggled": self.on_multi_epg_toggled}
|
||||
"on_multi_epg_toggled": self.on_multi_epg_toggled,
|
||||
"on_xmltv_toggled": self.on_xmltv_toggled,
|
||||
"on_epg_press": self.on_epg_press,
|
||||
"on_timer_add": self.on_timer_add}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}tab.glade", handlers)
|
||||
|
||||
@@ -223,7 +487,12 @@ class EpgTool(Gtk.Box):
|
||||
self._filter_button = builder.get_object("epg_filter_button")
|
||||
self._filter_entry = builder.get_object("epg_filter_entry")
|
||||
self._multi_epg_button = builder.get_object("multi_epg_button")
|
||||
self._src_xmltv_button = builder.get_object("src_xmltv_button")
|
||||
self._epg_options_button = builder.get_object("epg_options_button")
|
||||
self._epg_options_button.connect("realize", lambda b: b.set_popover(TabEpgSettingsPopover(self._app)))
|
||||
self._event_count_label = builder.get_object("event_count_label")
|
||||
self._cache_info_label = builder.get_object("cache_info_label")
|
||||
self.set_cache_info(0, 0)
|
||||
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
|
||||
# Custom data functions.
|
||||
renderer = builder.get_object("epg_start_renderer")
|
||||
@@ -241,12 +510,244 @@ class EpgTool(Gtk.Box):
|
||||
|
||||
self.show()
|
||||
|
||||
def on_data_open(self, app, page):
|
||||
if page is not Page.EPG:
|
||||
return
|
||||
|
||||
response = get_chooser_dialog(self._app.app_window, self._app.app_settings, "XMLTV", ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
self.open_data(response)
|
||||
|
||||
def on_data_extract(self, app, page):
|
||||
if page is not Page.EPG:
|
||||
return
|
||||
|
||||
f_filter = Gtk.FileFilter()
|
||||
f_filter.set_name("*.zip, *.gz, *.xz")
|
||||
f_filter.add_mime_type("application/zip")
|
||||
f_filter.add_mime_type("application/gzip")
|
||||
f_filter.add_mime_type("application/x-xz")
|
||||
|
||||
response = get_chooser_dialog(self._app.app_window, self._app.app_settings,
|
||||
"*.zip, *.gz, *.xz files", ("*.zip", "*.gz", "*.xz"), "Open archive", f_filter)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
self.open_data(response)
|
||||
|
||||
def open_data(self, path):
|
||||
if next(self.clear(), False):
|
||||
if not self._epg_cache:
|
||||
self._epg_cache = TabEpgCache(self._app, path)
|
||||
else:
|
||||
if self._epg_cache.is_run:
|
||||
self._app.show_error_message("Data loading in progress!")
|
||||
return
|
||||
|
||||
self._epg_cache._path = path
|
||||
self._epg_cache._xml_src = None
|
||||
self._epg_cache.reset()
|
||||
GLib.idle_add(self._src_xmltv_button.set_active, True)
|
||||
|
||||
def on_service_changed(self, app, srv):
|
||||
if app.page is not Page.EPG:
|
||||
return
|
||||
|
||||
if self._src is EpgSource.HTTP:
|
||||
ref = app.get_service_ref_data(srv)
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._multi_epg_button.get_active():
|
||||
ref += ":"
|
||||
path = next((r.path for r in self._model if r[-1].get("e2eventservicereference", None) == ref), None)
|
||||
scroll_to(path, self._view) if path else None
|
||||
else:
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_http_epg_data)
|
||||
else:
|
||||
if self._epg_cache.is_run:
|
||||
self._app.show_error_message("Data loading in progress!")
|
||||
return
|
||||
|
||||
if self._multi_epg_button.get_active():
|
||||
path = next((r.path for r in self._model if r[-1].get("e2eventservicename", None) == srv.service), None)
|
||||
scroll_to(path, self._view) if path else None
|
||||
else:
|
||||
self._app.wait_dialog.show()
|
||||
self.update_xmltv_epg_data([srv.service])
|
||||
|
||||
def on_profile_changed(self, app, prf):
|
||||
gen = self.update_epg_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
if app.page is Page.EPG and self._multi_epg_button.get_active():
|
||||
self.get_multi_epg()
|
||||
|
||||
def on_epg_cache_updated(self, app, cache):
|
||||
self.set_cache_info(len(cache.events), len(list(chain.from_iterable(cache.events.values()))))
|
||||
|
||||
def set_cache_info(self, s_count, ev_count):
|
||||
self._cache_info_label.set_text(f"{translate('Services')}: {s_count} {translate('Events')}: {ev_count}")
|
||||
|
||||
def on_epg_display_changed(self, app, display):
|
||||
self._epg_options_button.set_visible(not display and self._src is EpgSource.XML)
|
||||
|
||||
def update_http_epg_data(self, epg=None):
|
||||
if epg:
|
||||
events = (self.get_event(e) for e in epg.get("event_list", []) if e.get("e2eventid", "").isdigit())
|
||||
else:
|
||||
events = ()
|
||||
gen = self.update_epg_data(events)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update_xmltv_epg_data(self, names):
|
||||
gen = self.update_epg_data(chain.from_iterable(self._epg_cache.get_current_events(n) for n in names))
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update_epg_data(self, events=()):
|
||||
c_gen = self.clear()
|
||||
yield from c_gen
|
||||
for index, e in enumerate(events):
|
||||
if index % self.LOAD_FACTOR == 0:
|
||||
self._event_count_label.set_text(str(len(self._model)))
|
||||
yield True
|
||||
self._model.append(e)
|
||||
self._event_count_label.set_text(str(len(self._model)))
|
||||
self._app.wait_dialog.hide()
|
||||
yield True
|
||||
|
||||
def clear(self):
|
||||
if len(self._model) < self.LOAD_FACTOR * 20:
|
||||
self._model.clear()
|
||||
else:
|
||||
# Init new models.
|
||||
column_types = (self._model.get_column_type(i) for i in range(self._model.get_n_columns()))
|
||||
self._model = Gtk.ListStore(*column_types)
|
||||
self._filter_model = self._model.filter_new()
|
||||
self._filter_model.set_visible_func(self.epg_filter_function)
|
||||
self._view.set_model(Gtk.TreeModelSort(model=self._filter_model))
|
||||
self._event_count_label.set_text("0")
|
||||
yield True
|
||||
|
||||
@staticmethod
|
||||
def get_event(event, show_day=True):
|
||||
s_name = event.get("e2eventservicename", "")
|
||||
title = event.get("e2eventtitle", "") or ""
|
||||
desc = event.get("e2eventdescription", "") or ""
|
||||
desc = desc.strip()
|
||||
start, duration = int(event.get("e2eventstart", "0")), int(event.get("e2eventduration", "0"))
|
||||
|
||||
return EpgEvent(s_name, title, start, start + duration, duration, desc, event)
|
||||
|
||||
def start_data_func(self, column, renderer, model, itr, data):
|
||||
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_START))
|
||||
renderer.set_property("text", value.strftime(self._time_fmt))
|
||||
|
||||
def end_data_func(self, column, renderer, model, itr, data):
|
||||
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_END))
|
||||
renderer.set_property("text", value.strftime(self._time_fmt))
|
||||
|
||||
def duration_data_func(self, column, renderer, model, itr, data):
|
||||
value = datetime.utcfromtimestamp(model.get_value(itr, Column.EPG_LENGTH))
|
||||
renderer.set_property("text", value.strftime(self._duration_fmt))
|
||||
|
||||
@run_with_delay(2)
|
||||
def on_epg_filter_changed(self, entry):
|
||||
self._filter_model.refilter()
|
||||
|
||||
def on_epg_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
def epg_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr,
|
||||
Column.EPG_SERVICE,
|
||||
Column.EPG_TITLE,
|
||||
Column.EPG_DESC) if s and txt in s.upper()), False)
|
||||
|
||||
def on_filter_toggled(self, app, value):
|
||||
if self._app.page is Page.EPG:
|
||||
active = not self._filter_button.get_active()
|
||||
self._filter_button.set_active(active)
|
||||
if active:
|
||||
self._filter_entry.grab_focus()
|
||||
|
||||
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
dst = view.get_dest_row_at_pos(x, y)
|
||||
if not dst:
|
||||
return False
|
||||
|
||||
path, pos = dst
|
||||
model = view.get_model()
|
||||
data = model[path][-1]
|
||||
if not data:
|
||||
return False
|
||||
|
||||
desc = data.get("e2eventdescription", "") or ""
|
||||
ext_desc = data.get("e2eventdescriptionextended", "") or ""
|
||||
if not any((desc, ext_desc)):
|
||||
return False
|
||||
|
||||
tooltip.set_text(ext_desc if ext_desc else desc)
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
|
||||
return True
|
||||
|
||||
def on_multi_epg_toggled(self, button):
|
||||
if button.get_active():
|
||||
self.get_multi_epg()
|
||||
else:
|
||||
next(self.clear(), False)
|
||||
|
||||
def on_xmltv_toggled(self, button):
|
||||
if button.get_active():
|
||||
self._src = EpgSource.XML
|
||||
if self._epg_cache is None:
|
||||
settings = self._app.app_settings
|
||||
xml_src = self._app.app_settings.epg_xml_source if settings.epg_source is EpgSource.XML else None
|
||||
self._epg_cache = TabEpgCache(self._app, url=xml_src)
|
||||
else:
|
||||
self._src = EpgSource.HTTP
|
||||
self.update_http_epg_data()
|
||||
|
||||
self._epg_options_button.set_visible(not self._app.display_epg and self._src is EpgSource.XML)
|
||||
|
||||
def get_multi_epg(self):
|
||||
if not self._current_bq:
|
||||
return
|
||||
|
||||
self._app.wait_dialog.show()
|
||||
|
||||
if self._src is EpgSource.HTTP:
|
||||
bq, api = self._app.current_bouquet_files.get(self._current_bq, None), self._app.http_api
|
||||
if bq and api:
|
||||
tm = datetime.now().timestamp()
|
||||
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"&time={tm}')
|
||||
api.send(HttpAPI.Request.EPG_MULTI, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_http_epg_data, timeout=15)
|
||||
else:
|
||||
srvs = self._app.current_services
|
||||
self.update_xmltv_epg_data(srvs[s].service for s in self._app.current_bouquets.get(self._current_bq, []))
|
||||
|
||||
# ****************** Timers ***************** #
|
||||
|
||||
def on_epg_press(self, view, event):
|
||||
if self._src_xmltv_button.get_active():
|
||||
return True
|
||||
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
|
||||
self.on_timer_add()
|
||||
|
||||
def on_timer_add(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
p_count = len(paths)
|
||||
|
||||
if p_count == 1:
|
||||
dialog = TimerTool.TimerDialog(self._app.app_window, TimerTool.TimerAction.EVENT, model[paths][-1])
|
||||
dialog = TimerTool.TimerDialog(self._app.app_window, TimerTool.TimerAction.EVENT, model[paths][-1] or {})
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
gen = self.write_timers_list([dialog.get_request()])
|
||||
@@ -280,117 +781,6 @@ class EpgTool(Gtk.Box):
|
||||
|
||||
self._app.emit("change-page", Page.TIMERS.value)
|
||||
|
||||
def on_epg_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
|
||||
self.on_timer_add()
|
||||
|
||||
def on_service_changed(self, app, ref):
|
||||
if app.page is Page.EPG:
|
||||
if self._multi_epg_button.get_active():
|
||||
ref += ":"
|
||||
path = next((r.path for r in self._model if r[-1].get("e2eventservicereference", None) == ref), None)
|
||||
scroll_to(path, self._view) if path else None
|
||||
else:
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
|
||||
|
||||
def on_profile_changed(self, app, prf):
|
||||
self.update_epg_data()
|
||||
|
||||
@run_idle
|
||||
def update_epg_data(self, epg=None):
|
||||
self._event_count_label.set_text("0")
|
||||
self._model.clear()
|
||||
if epg:
|
||||
list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", [])
|
||||
if e.get("e2eventid", "").isdigit())))
|
||||
self._event_count_label.set_text(str(len(self._model)))
|
||||
self._app.wait_dialog.hide()
|
||||
|
||||
@staticmethod
|
||||
def get_event(event, show_day=True):
|
||||
s_name = event.get("e2eventservicename", "")
|
||||
title = event.get("e2eventtitle", "") or ""
|
||||
desc = event.get("e2eventdescription", "") or ""
|
||||
desc = desc.strip()
|
||||
start, duration = int(event.get("e2eventstart", "0")), int(event.get("e2eventduration", "0"))
|
||||
|
||||
return EpgEvent(s_name, title, start, start + duration, duration, desc, event)
|
||||
|
||||
def start_data_func(self, column, renderer, model, itr, data):
|
||||
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_START))
|
||||
renderer.set_property("text", value.strftime(self._time_fmt))
|
||||
|
||||
def end_data_func(self, column, renderer, model, itr, data):
|
||||
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_END))
|
||||
renderer.set_property("text", value.strftime(self._time_fmt))
|
||||
|
||||
def duration_data_func(self, column, renderer, model, itr, data):
|
||||
value = datetime.utcfromtimestamp(model.get_value(itr, Column.EPG_LENGTH))
|
||||
renderer.set_property("text", value.strftime(self._duration_fmt))
|
||||
|
||||
def on_epg_filter_changed(self, entry):
|
||||
self._filter_model.refilter()
|
||||
|
||||
def on_epg_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
def epg_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr,
|
||||
Column.EPG_SERVICE,
|
||||
Column.EPG_TITLE,
|
||||
Column.EPG_DESC) if txt in s.upper()), False)
|
||||
|
||||
def on_filter_toggled(self, app, value):
|
||||
if self._app.page is Page.EPG:
|
||||
active = not self._filter_button.get_active()
|
||||
self._filter_button.set_active(active)
|
||||
if active:
|
||||
self._filter_entry.grab_focus()
|
||||
|
||||
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
dst = view.get_dest_row_at_pos(x, y)
|
||||
if not dst:
|
||||
return False
|
||||
|
||||
path, pos = dst
|
||||
model = view.get_model()
|
||||
data = model[path][-1]
|
||||
desc = data.get("e2eventdescription", "") or ""
|
||||
ext_desc = data.get("e2eventdescriptionextended", "") or ""
|
||||
|
||||
tooltip.set_text(ext_desc if ext_desc else desc)
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
|
||||
return True
|
||||
|
||||
def on_multi_epg_toggled(self, button):
|
||||
self._model.clear()
|
||||
self._event_count_label.set_text("0")
|
||||
|
||||
if button.get_active():
|
||||
self.get_multi_epg()
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
if app.page is Page.EPG and self._multi_epg_button.get_active():
|
||||
self.get_multi_epg()
|
||||
|
||||
def get_multi_epg(self):
|
||||
if not self._current_bq:
|
||||
return
|
||||
|
||||
self._app.wait_dialog.show()
|
||||
bq = self._app.current_bouquet_files.get(self._current_bq, None)
|
||||
api = self._app.http_api
|
||||
|
||||
if bq and api:
|
||||
tm = datetime.now().timestamp()
|
||||
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"&time={tm}')
|
||||
api.send(HttpAPI.Request.EPG_MULTI, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_epg_data, timeout=15)
|
||||
|
||||
|
||||
class EpgDialog:
|
||||
|
||||
@@ -423,7 +813,8 @@ class EpgDialog:
|
||||
"on_field_icon_press": self.on_field_icon_press,
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_bq_cursor_changed": self.on_bq_cursor_changed,
|
||||
"on_source_view_query_tooltip": self.on_source_view_query_tooltip}
|
||||
"on_source_view_query_tooltip": self.on_source_view_query_tooltip,
|
||||
"on_paned_size_allocate": lambda p, a: p.set_position(0.5 * a.width)}
|
||||
|
||||
self._app = app
|
||||
self._ex_services = self._app.current_services
|
||||
@@ -455,7 +846,7 @@ class EpgDialog:
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_entry = builder.get_object("filter_entry")
|
||||
self._filter_auto_switch = builder.get_object("filter_auto_switch")
|
||||
self._filter_auto_button = builder.get_object("filter_auto_button")
|
||||
self._services_filter_model = builder.get_object("services_filter_model")
|
||||
self._services_filter_model.set_visible_func(self.services_filter_function)
|
||||
self._sat_pos_filter_model = builder.get_object("sat_pos_filter_model")
|
||||
@@ -478,7 +869,7 @@ class EpgDialog:
|
||||
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
|
||||
|
||||
if self._settings.use_header_bar:
|
||||
header_bar = HeaderBar(title="EPG", subtitle=get_message("List configuration"))
|
||||
header_bar = HeaderBar(title="EPG", subtitle=translate("List configuration"))
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
builder.get_object("left_action_box").reparent(header_bar)
|
||||
right_box = builder.get_object("right_action_box")
|
||||
@@ -618,8 +1009,8 @@ class EpgDialog:
|
||||
|
||||
if content_type != "application/gzip":
|
||||
self._download_xml_is_active = False
|
||||
raise ValueError("{} {} {}".format(get_message("Download XML file error."),
|
||||
get_message("Unsupported file type:"),
|
||||
raise ValueError("{} {} {}".format(translate("Download XML file error."),
|
||||
translate("Unsupported file type:"),
|
||||
content_type))
|
||||
|
||||
file_name = os.path.basename(url)
|
||||
@@ -645,7 +1036,7 @@ class EpgDialog:
|
||||
|
||||
path = tfp.name.rstrip(".gz")
|
||||
except (HTTPError, URLError) as e:
|
||||
raise ValueError(f"{get_message('Download XML file error.')} {e}")
|
||||
raise ValueError(f"{translate('Download XML file error.')} {e}")
|
||||
else:
|
||||
try:
|
||||
with open(path, "wb") as f_out:
|
||||
@@ -653,7 +1044,7 @@ class EpgDialog:
|
||||
shutil.copyfileobj(f, f_out)
|
||||
os.remove(tfp.name)
|
||||
except Exception as e:
|
||||
raise ValueError(f"{get_message('Unpacking data error.')} {e}")
|
||||
raise ValueError(f"{translate('Unpacking data error.')} {e}")
|
||||
finally:
|
||||
self._download_xml_is_active = False
|
||||
self.update_active_header_elements(True)
|
||||
@@ -662,7 +1053,7 @@ class EpgDialog:
|
||||
s_refs, info = ChannelsParser.get_refs_from_xml(path)
|
||||
yield True
|
||||
except Exception as e:
|
||||
raise ValueError(f"{get_message('XML parsing error:')} {e}")
|
||||
raise ValueError(f"{translate('XML parsing error:')} {e}")
|
||||
else:
|
||||
refs = refs or {}
|
||||
factor = self._app.DEL_FACTOR / 4
|
||||
@@ -699,7 +1090,7 @@ class EpgDialog:
|
||||
self.on_assign_ref()
|
||||
|
||||
def on_bq_cursor_changed(self, view):
|
||||
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
|
||||
if self._filter_bar.get_visible() and self._filter_auto_button.get_active():
|
||||
path, column = view.get_cursor()
|
||||
model = view.get_model()
|
||||
if path:
|
||||
@@ -718,7 +1109,7 @@ class EpgDialog:
|
||||
if self._refs_source is RefsSource.XML:
|
||||
text = f"ID = {ch_id}"
|
||||
else:
|
||||
text = f"{get_message('Service reference')}: {ch_id.rstrip('.png')}"
|
||||
text = f"{translate('Service reference')}: {ch_id.rstrip('.png')}"
|
||||
|
||||
tooltip.set_text(text)
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
@@ -742,7 +1133,7 @@ class EpgDialog:
|
||||
services.append(srv)
|
||||
|
||||
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
@run_idle
|
||||
def on_auto_configuration(self, item):
|
||||
@@ -785,8 +1176,8 @@ class EpgDialog:
|
||||
break
|
||||
|
||||
self.update_epg_count()
|
||||
self.show_info_message("{} {} {}".format(get_message("Done!"),
|
||||
get_message("Count of successfully configured services:"),
|
||||
self.show_info_message("{} {} {}".format(translate("Done!"),
|
||||
translate("Count of successfully configured services:"),
|
||||
success_count), Gtk.MessageType.INFO)
|
||||
|
||||
def assign_refs(self, model, paths, data):
|
||||
@@ -796,7 +1187,7 @@ class EpgDialog:
|
||||
def assign_data(self, row, data, show_error=False):
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
|
||||
if not show_error:
|
||||
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
|
||||
self.show_info_message(translate("Not allowed in this context!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
fav_id = row[Column.FAV_ID]
|
||||
@@ -814,8 +1205,8 @@ class EpgDialog:
|
||||
row[Column.FAV_LOCKED] = EPG_ICON
|
||||
|
||||
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}"
|
||||
src = f"{translate('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
|
||||
row[Column.FAV_TOOLTIP] = f"{translate('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
|
||||
|
||||
def on_filter_toggled(self, button):
|
||||
self._filter_bar.set_visible(button.get_active())
|
||||
@@ -874,9 +1265,7 @@ class EpgDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
@run_idle
|
||||
def update_source_info(self, info):
|
||||
@@ -888,7 +1277,7 @@ class EpgDialog:
|
||||
source_count = len(self._services_model)
|
||||
self._source_count_label.set_text(str(source_count))
|
||||
if self._enable_dat_filter and source_count == 0:
|
||||
msg = get_message("Current epg.dat file does not contains references for the services of this bouquet!")
|
||||
msg = translate("Current epg.dat file does not contains references for the services of this bouquet!")
|
||||
self.show_info_message(msg, Gtk.MessageType.WARNING)
|
||||
|
||||
@run_idle
|
||||
@@ -933,7 +1322,7 @@ class EpgDialog:
|
||||
if all(s_data[:-1]):
|
||||
data.set_text("::::".join(s_data), -1)
|
||||
else:
|
||||
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
|
||||
self.show_info_message(translate("Source error!"), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_drag_data_received(self, view, drag_context, x, y, data, info, time):
|
||||
path, pos = view.get_dest_row_at_pos(x, y)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2023 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,33 +26,33 @@ THE SOFTWARE.
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAdjustment" id="interval_adjustment">
|
||||
<property name="lower">3</property>
|
||||
<property name="upper">60</property>
|
||||
<property name="value">3</property>
|
||||
<property name="step_increment">1</property>
|
||||
<property name="page_increment">10</property>
|
||||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">10</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<object class="GtkLabel" id="src_label">
|
||||
<property name="visible" bind-source="source_selection_box" bind-property="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
@@ -65,22 +65,22 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="src_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="source_selection_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="http_src_button">
|
||||
<property name="label" translatable="yes">Receiver</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -93,10 +93,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkRadioButton" id="xml_src_button">
|
||||
<property name="label" translatable="yes">XML TV</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -108,12 +108,11 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="dat_src_button">
|
||||
<property name="label" translatable="yes">*.dat file</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">http_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -132,12 +131,12 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="interval_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Update interval (sec):</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -149,10 +148,10 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="max_width_chars">4</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="max-width-chars">4</property>
|
||||
<property name="adjustment">interval_adjustment</property>
|
||||
<property name="climb_rate">1</property>
|
||||
<property name="climb-rate">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="value">3</property>
|
||||
</object>
|
||||
@@ -172,15 +171,14 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="xml_source_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="sensitive" bind-source="xml_src_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="xml_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel" id="url_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Url to *.xml.gz file:</property>
|
||||
</object>
|
||||
@@ -191,15 +189,130 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<object class="GtkBox" id="url_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="input_purpose">url</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="url_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="can-focus">False</property>
|
||||
<signal name="focus-out-event" handler="on_url_entry_focus_out" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="url_action_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_url_button">
|
||||
<property name="visible" bind-source="apply_url_button" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Remove</property>
|
||||
<signal name="clicked" handler="on_remove_url" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-remove-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="add_url_button">
|
||||
<property name="visible" bind-source="apply_url_button" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add</property>
|
||||
<signal name="clicked" handler="on_add_url" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="add_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-add-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="cancel_url_button">
|
||||
<property name="visible" bind-source="apply_url_button" bind-property="visible">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Cancel</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="cancel_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-undo-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_url_button">
|
||||
<property name="visible" bind-source="url_entry" bind-property="has-focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add</property>
|
||||
<signal name="clicked" handler="on_apply_url" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="apply_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Apply</property>
|
||||
<property name="icon-name">insert-link-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
@@ -207,12 +320,12 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="download_interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="download_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Update:</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -224,11 +337,11 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="download_interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">daily</property>
|
||||
<property name="active-id">daily</property>
|
||||
<items>
|
||||
<item id="daily" translatable="yes">Daily</item>
|
||||
</items>
|
||||
@@ -243,7 +356,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -255,16 +368,14 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="dat_source_box">
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="sensitive" bind-source="dat_src_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="dat_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">STB path:</property>
|
||||
</object>
|
||||
@@ -277,9 +388,9 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="dat_path_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">/etc/enigma2</property>
|
||||
<property name="active-id">/etc/enigma2</property>
|
||||
<items>
|
||||
<item id="/etc/enigma2/">/etc/enigma2/</item>
|
||||
<item id="/media/hdd/">/media/hdd/</item>
|
||||
@@ -311,18 +422,18 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -335,8 +446,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label" translatable="yes">Close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2024 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
|
||||
@@ -62,148 +62,90 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_box">
|
||||
<object class="GtkViewport" id="viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_action_box">
|
||||
<object class="GtkBox" id="epg_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child type="center">
|
||||
<object class="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="label" translatable="yes">Multi EPG</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_action_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="src_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">EPG source</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<property name="active-id">0</property>
|
||||
<items>
|
||||
<item id="0" translatable="yes">Receiver</item>
|
||||
</items>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="name">header-entry</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="width-chars">10</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="GtkToggleButton" id="epg_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_epg_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="epg_filter_button_image">
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child type="center">
|
||||
<object class="GtkButtonBox" id="src_box">
|
||||
<property name="name">header-stack-switcher</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-replace-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">EPG source</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="src_receiver_button">
|
||||
<property name="label" translatable="yes">Receiver</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">src_xmltv_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="src_xmltv_button">
|
||||
<property name="label" translatable="yes">XML TV</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">src_receiver_button</property>
|
||||
<signal name="toggled" handler="on_xmltv_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_add_timer_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add timer</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_timer_add" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="add_timer_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">alarm-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>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_filter_entry">
|
||||
<property name="visible" bind-source="epg_filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">True</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_epg_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="fav_search_box">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_search_entry">
|
||||
<object class="GtkToggleButton" id="epg_filter_button">
|
||||
<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="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_epg_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="epg_filter_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -212,256 +154,420 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_down_button">
|
||||
<object class="GtkButton" id="epg_add_timer_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="sensitive" bind-source="src_xmltv_button" bind-property="active" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add timer</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_timer_add" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_down_arrow">
|
||||
<object class="GtkImage" id="add_timer_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">down</property>
|
||||
<property name="icon-name">alarm-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="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Multi EPG</property>
|
||||
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="multi_epg_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-select-all-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="epg_options_button">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Options</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="epg_options_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">applications-system-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_filter_entry">
|
||||
<property name="visible" bind-source="epg_filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">True</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_epg_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="fav_search_box">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_search_entry">
|
||||
<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>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_down_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="epg_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_up_button">
|
||||
<object class="GtkTreeView" id="epg_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="model">epg_sort_model</property>
|
||||
<property name="fixed-height-mode">True</property>
|
||||
<property name="rubber-banding">True</property>
|
||||
<property name="enable-grid-lines">both</property>
|
||||
<property name="tooltip-column">6</property>
|
||||
<signal name="button-press-event" handler="on_epg_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="epg_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_up_arrow">
|
||||
<object class="GtkTreeViewColumn" id="epg_service_column">
|
||||
<property name="visible" bind-source="multi_epg_button" bind-property="active">False</property>
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">40</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort-column-id">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_service_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort-column-id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_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="epg_start_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Start time</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort-column-id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_start_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_end_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">End time</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort-column-id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_end_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_length_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort-column-id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_length_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_desc_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort-column-id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_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">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="event_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</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="event_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width-chars">4</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_cache_info_box">
|
||||
<property name="visible" bind-source="src_xmltv_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Current EPG cache contents.</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">up</property>
|
||||
<property name="icon-name">edit-select-all-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="cache_info_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">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="epg_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="epg_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">epg_sort_model</property>
|
||||
<property name="fixed-height-mode">True</property>
|
||||
<property name="rubber-banding">True</property>
|
||||
<property name="enable-grid-lines">both</property>
|
||||
<property name="tooltip-column">6</property>
|
||||
<signal name="button-press-event" handler="on_epg_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="epg_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_service_column">
|
||||
<property name="visible" bind-source="multi_epg_button" bind-property="active">False</property>
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">40</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_service_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_start_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Start time</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_start_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_end_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">End time</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_end_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_length_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_length_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_desc_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_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">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="event_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</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="event_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width-chars">4</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="epg_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">EPG</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
0
app/ui/extensions/__init__.py
Normal file
0
app/ui/extensions/__init__.py
Normal file
348
app/ui/extensions/management.py
Normal file
348
app/ui/extensions/management.py
Normal file
@@ -0,0 +1,348 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2023-2024 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 json
|
||||
import os
|
||||
import shutil
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from gi.repository import Gtk, Gdk, GLib, Pango, GObject
|
||||
|
||||
from app.commons import log, run_task, run_idle
|
||||
from app.ui.dialogs import translate
|
||||
from app.ui.uicommons import HeaderBar
|
||||
|
||||
EXT_URL = "https://api.github.com/repos/DYefremov/demoneditor-extensions/contents/extensions/"
|
||||
EXT_LIST_FILE = "https://raw.githubusercontent.com/DYefremov/demoneditor-extensions/main/extensions/extension-list"
|
||||
# Config file name. The config file must be in json format!
|
||||
# E.g. -> {"EXT_URL": "repo URL", "EXT_LIST_FILE": "URL to 'extension-list' file."}
|
||||
EXT_CONFIG_FILE = "ext_sources"
|
||||
HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:112.0) Gecko/20100101 Firefox/112.0",
|
||||
"Accept": "application/json"}
|
||||
|
||||
|
||||
class ExtensionManager(Gtk.Window):
|
||||
ICON_INFO = "emblem-important-symbolic"
|
||||
ICON_UPDATE = "network-receive-symbolic"
|
||||
|
||||
class Column(IntEnum):
|
||||
TITLE = 0
|
||||
DESC = 1
|
||||
VER = 2
|
||||
INFO = 3
|
||||
STATUS = 4
|
||||
NAME = 5
|
||||
URL = 6
|
||||
PATH = 7
|
||||
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(title=translate("Extensions"), icon_name="demon-editor", application=app,
|
||||
transient_for=app.app_window, destroy_with_parent=True,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
default_width=560, default_height=320, modal=True, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._ext_path = f"{self._app.app_settings.default_data_path}tools{os.sep}extensions"
|
||||
|
||||
margin = {"margin_start": 5, "margin_end": 5, "margin_top": 5, "margin_bottom": 5}
|
||||
base_margin = {"margin_start": 10, "margin_end": 10, "margin_top": 10, "margin_bottom": 10}
|
||||
# Title, Description, Version, Info, Status, Name, URL, Path.
|
||||
self._model = Gtk.ListStore.new((str, str, str, str, bool, str, str, object))
|
||||
self._model.connect("row-deleted", self.on_model_changed)
|
||||
self._model.connect("row-inserted", self.on_model_changed)
|
||||
self._view = Gtk.TreeView(activate_on_single_click=True, enable_grid_lines=Gtk.TreeViewGridLines.BOTH)
|
||||
self._view.set_model(self._model)
|
||||
self._view.set_tooltip_column(self.Column.DESC)
|
||||
self._view.connect("row-activated", self.on_row_activated)
|
||||
# Title
|
||||
renderer = Gtk.CellRendererText(xalign=0.05, ellipsize=Pango.EllipsizeMode.END)
|
||||
column = Gtk.TreeViewColumn(title=translate("Title"), cell_renderer=renderer, text=self.Column.TITLE)
|
||||
column.set_alignment(0.5)
|
||||
column.set_min_width(170)
|
||||
column.set_resizable(True)
|
||||
self._view.append_column(column)
|
||||
# Description
|
||||
renderer = Gtk.CellRendererText(xalign=0.05, ellipsize=Pango.EllipsizeMode.END)
|
||||
column = Gtk.TreeViewColumn(title=translate("Description"), cell_renderer=renderer, text=self.Column.DESC)
|
||||
column.set_alignment(0.5)
|
||||
column.set_resizable(True)
|
||||
column.set_expand(True)
|
||||
self._view.append_column(column)
|
||||
# Version
|
||||
column = Gtk.TreeViewColumn(translate("Ver."))
|
||||
column.set_alignment(0.5)
|
||||
column.set_fixed_width(70)
|
||||
renderer = Gtk.CellRendererText(xalign=0.5)
|
||||
column.pack_start(renderer, True)
|
||||
column.add_attribute(renderer, "text", self.Column.VER)
|
||||
renderer = Gtk.CellRendererPixbuf()
|
||||
column.pack_start(renderer, True)
|
||||
column.add_attribute(renderer, "icon_name", self.Column.INFO)
|
||||
self._view.append_column(column)
|
||||
# Status
|
||||
renderer = Gtk.CellRendererToggle(xalign=0.5)
|
||||
column = Gtk.TreeViewColumn(title=translate("Installed"), cell_renderer=renderer, active=self.Column.STATUS)
|
||||
column.set_alignment(0.5)
|
||||
column.set_fixed_width(100)
|
||||
self._view.append_column(column)
|
||||
self._status_column = column
|
||||
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
frame = Gtk.Frame(shadow_type=Gtk.ShadowType.IN, **base_margin)
|
||||
frame.get_style_context().add_class("view")
|
||||
data_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL, **base_margin)
|
||||
data_box.set_margin_bottom(margin.get("margin_bottom", 5))
|
||||
# Status bar.
|
||||
status_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, margin_start=5, margin_end=5)
|
||||
count_icon = Gtk.Image.new_from_icon_name("document-properties", Gtk.IconSize.SMALL_TOOLBAR)
|
||||
status_box.pack_start(count_icon, False, False, 0)
|
||||
self._count_label = Gtk.Label(label="0", width_chars=4, xalign=0)
|
||||
status_box.pack_start(self._count_label, False, False, 0)
|
||||
status_box.show_all()
|
||||
load_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, margin_end=10, no_show_all=True)
|
||||
load_box.pack_start(Gtk.Label(label=translate("Loading data..."), visible=True), False, False, 0)
|
||||
self._load_spinner = Gtk.Spinner(visible=True)
|
||||
self._load_spinner.bind_property("active", load_box, "visible")
|
||||
self._load_spinner.bind_property("active", self._view, "sensitive", GObject.BindingFlags.INVERT_BOOLEAN)
|
||||
load_box.pack_end(self._load_spinner, False, False, 0)
|
||||
status_box.pack_end(load_box, False, False, 0)
|
||||
|
||||
data_box.pack_end(status_box, False, True, 0)
|
||||
scrolled = Gtk.ScrolledWindow(shadow_type=Gtk.ShadowType.IN)
|
||||
scrolled.add(self._view)
|
||||
data_box.pack_start(scrolled, True, True, 0)
|
||||
data_box.set_margin_start(10)
|
||||
frame.add(data_box)
|
||||
self.add(main_box)
|
||||
# Popup menu.
|
||||
menu = Gtk.Menu()
|
||||
item = Gtk.MenuItem.new_with_label(translate("Download"))
|
||||
item.connect("activate", self.on_download)
|
||||
menu.append(item)
|
||||
item = Gtk.MenuItem.new_with_label(translate("Remove"))
|
||||
item.connect("activate", self.on_remove)
|
||||
menu.append(item)
|
||||
menu.show_all()
|
||||
self._view.connect("button-press-event", self.on_view_popup_menu, menu)
|
||||
# Header and toolbar.
|
||||
download_button = Gtk.Button.new_from_icon_name("go-bottom-symbolic", Gtk.IconSize.BUTTON)
|
||||
download_button.set_label(translate("Download"))
|
||||
download_button.set_always_show_image(True)
|
||||
download_button.connect("clicked", self.on_download)
|
||||
remove_button = Gtk.Button.new_from_icon_name("user-trash-symbolic", Gtk.IconSize.BUTTON)
|
||||
remove_button.set_label(translate("Remove"))
|
||||
remove_button.set_always_show_image(True)
|
||||
remove_button.connect("clicked", self.on_remove)
|
||||
|
||||
if app.app_settings.use_header_bar:
|
||||
header = HeaderBar()
|
||||
header.pack_start(download_button)
|
||||
header.pack_start(remove_button)
|
||||
|
||||
self.set_titlebar(header)
|
||||
header.show_all()
|
||||
else:
|
||||
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
toolbar.get_style_context().add_class("primary-toolbar")
|
||||
button_box = Gtk.Box(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, **margin)
|
||||
button_box.pack_start(download_button, False, False, 0)
|
||||
button_box.pack_start(remove_button, False, False, 0)
|
||||
toolbar.pack_start(button_box, True, True, 0)
|
||||
main_box.pack_start(toolbar, False, False, 0)
|
||||
|
||||
main_box.pack_start(frame, True, True, 0)
|
||||
main_box.show_all()
|
||||
|
||||
ws_property = "extension_manager_window_size"
|
||||
window_size = self._app.app_settings.get(ws_property, None)
|
||||
if window_size:
|
||||
self.resize(*window_size)
|
||||
|
||||
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
|
||||
self.connect("realize", self.init)
|
||||
|
||||
def init(self, widget):
|
||||
self._load_spinner.start()
|
||||
scf = f"{os.path.dirname(__file__)}{os.sep}{EXT_CONFIG_FILE}"
|
||||
if os.path.isfile(scf):
|
||||
with (open(scf, "r", encoding="utf-8", errors="ignore") as cf):
|
||||
config = json.load(cf)
|
||||
global EXT_URL, EXT_LIST_FILE
|
||||
EXT_URL = config.get("EXT_URL", EXT_URL)
|
||||
EXT_LIST_FILE = config.get("EXT_LIST_FILE", EXT_LIST_FILE)
|
||||
|
||||
self.update()
|
||||
|
||||
def get_installed(self):
|
||||
import pkgutil
|
||||
from importlib.util import module_from_spec
|
||||
|
||||
ext_paths = [f"{os.path.dirname(__file__)}{os.sep}", self._ext_path, "extensions"]
|
||||
installed = {}
|
||||
|
||||
for importer, name, is_package in pkgutil.iter_modules(ext_paths):
|
||||
if is_package:
|
||||
spec = importer.find_spec(name)
|
||||
if spec is None:
|
||||
log(f"{self.__class__.__name__} [get installed]: Module {name} not found.")
|
||||
continue
|
||||
|
||||
m = module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
cls_name = name.capitalize()
|
||||
if hasattr(m, cls_name):
|
||||
cls = getattr(m, cls_name)
|
||||
path = Path(spec.origin).parent
|
||||
installed[name] = (cls, path)
|
||||
|
||||
return installed
|
||||
|
||||
@run_task
|
||||
def update(self):
|
||||
with requests.get(url=EXT_LIST_FILE, stream=True) as resp:
|
||||
error_msg = None
|
||||
if resp.status_code == 200:
|
||||
try:
|
||||
self.update_data(resp.json())
|
||||
except ValueError as e:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: {e}"
|
||||
else:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: {resp.reason}"
|
||||
|
||||
if error_msg:
|
||||
log(error_msg)
|
||||
GLib.idle_add(self._load_spinner.stop)
|
||||
GLib.idle_add(self._app.show_error_message, "Data loading error!")
|
||||
|
||||
@run_idle
|
||||
def update_data(self, data):
|
||||
self._model.clear()
|
||||
gen = self.append_data(data)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def append_data(self, data):
|
||||
installed = self.get_installed()
|
||||
for e, d in data.items():
|
||||
url = f"{EXT_URL}{d.get('ref', '')}"
|
||||
desc = d.get("description", "")
|
||||
ver = d.get("version", "1.0")
|
||||
info = self.ICON_UPDATE
|
||||
path = None
|
||||
|
||||
ext = installed.get(e)
|
||||
if ext:
|
||||
info = None
|
||||
ext_ver = ext[0].VERSION
|
||||
path = ext[1]
|
||||
if ext_ver < ver:
|
||||
ver = ext_ver
|
||||
info = self.ICON_INFO
|
||||
|
||||
yield self._model.append((d.get('label'), desc, ver, info, path, e, url, path))
|
||||
self._load_spinner.stop()
|
||||
|
||||
def on_remove(self, item=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
itr = model.get_iter(paths)
|
||||
path = model[itr][self.Column.PATH]
|
||||
if path:
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [remove] error: {e}")
|
||||
else:
|
||||
model.set(itr, {self.Column.INFO: self.ICON_UPDATE, self.Column.STATUS: None, self.Column.PATH: None})
|
||||
msg = translate('Restart the program to apply all changes.')
|
||||
self._app.show_info_message(msg, Gtk.MessageType.WARNING)
|
||||
|
||||
@run_task
|
||||
def on_download(self, item=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
itr = model.get_iter(paths)
|
||||
url = model[itr][self.Column.URL]
|
||||
ver = model[itr][self.Column.VER]
|
||||
if not url:
|
||||
return
|
||||
|
||||
GLib.idle_add(self._load_spinner.start)
|
||||
urls = {}
|
||||
with requests.get(url=url, headers=HEADERS, stream=True) as resp:
|
||||
if resp.status_code == 200:
|
||||
try:
|
||||
for f in resp.json():
|
||||
url = f.get("download_url", None)
|
||||
ver = f.get("version", ver)
|
||||
if url:
|
||||
urls[url] = f.get("name", None)
|
||||
except ValueError as e:
|
||||
log(f"{self.__class__.__name__} [download] error: {e}")
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {resp.reason}")
|
||||
|
||||
if urls:
|
||||
path = f"{self._ext_path}{os.sep}{model[paths][self.Column.NAME]}{os.sep}"
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
if all((self.download_file(u, f"{path}{n}") for u, n in urls.items())):
|
||||
data = {self.Column.VER: ver, self.Column.INFO: None, self.Column.STATUS: True, self.Column.PATH: path}
|
||||
GLib.idle_add(model.set, itr, data)
|
||||
msg = translate('Restart the program to apply all changes.')
|
||||
self._app.show_info_message(msg, Gtk.MessageType.WARNING)
|
||||
|
||||
GLib.idle_add(self._load_spinner.stop)
|
||||
|
||||
def download_file(self, url, path):
|
||||
with requests.get(url=url, headers=HEADERS, stream=True) as resp:
|
||||
if resp.status_code == 200:
|
||||
with open(path, mode="bw") as f:
|
||||
for data in resp.iter_content(chunk_size=1024):
|
||||
f.write(data)
|
||||
return True
|
||||
|
||||
def on_model_changed(self, model, path, itr=None):
|
||||
self._count_label.set_text(str(len(model)))
|
||||
|
||||
def on_row_activated(self, view, path, column):
|
||||
if column is self._status_column:
|
||||
self.on_remove() if view.get_model()[path][self.Column.STATUS] else self.on_download()
|
||||
|
||||
def on_view_popup_menu(self, view, event, menu):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1811
app/ui/ftp.glade
1811
app/ui/ftp.glade
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ from gi.repository import GLib
|
||||
from app.commons import log, run_task, run_idle, get_size_from_bytes
|
||||
from app.connections import UtfFTP
|
||||
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP, USE_HEADER_BAR
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder, get_message
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder, translate
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page
|
||||
|
||||
@@ -147,7 +147,7 @@ class AttributesDialog(BaseDialog):
|
||||
""" Dialog for editing file attributes (permissions). """
|
||||
|
||||
def __init__(self, attrs, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=get_message("Permissions"), use_header_bar=use_header_bar, *args, **kwargs)
|
||||
super().__init__(title=translate("Permissions"), use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
self.set_default_size(360, 100)
|
||||
self.set_resizable(False)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -35,8 +35,8 @@ 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, 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, get_iptv_data
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, translate, get_builder
|
||||
from app.ui.main_helper import on_popup_menu, get_iptv_data, show_info_bar_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, Page, HeaderBar
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ def import_bouquet(app, model, path, appender, file_path=None):
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
pattern = f".{bq_type.value}"
|
||||
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
|
||||
f_pattern = f"*{pattern}"
|
||||
elif profile is SettingsType.NEUTRINO_MP:
|
||||
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
|
||||
f_pattern = "bouquets.xml"
|
||||
@@ -96,10 +96,9 @@ def import_bouquet(app, model, path, appender, file_path=None):
|
||||
|
||||
|
||||
def get_enigma2_bouquet(path):
|
||||
path, sep, f_name = path.rpartition("userbouquet.")
|
||||
name, sep, suf = f_name.rpartition(".")
|
||||
bq = BouquetsReader.get_bouquet(path, name, suf)
|
||||
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
|
||||
p = Path(path)
|
||||
bq = BouquetsReader.get_bouquet(f"{p.parent}{SEP}", f"{p.stem}{p.suffix}", p.stem)
|
||||
bouquet = Bouquet(name=bq[0], type=BqType(p.suffix.lstrip(".")).value, services=bq[1], locked=None, hidden=None)
|
||||
return bouquet
|
||||
|
||||
|
||||
@@ -121,6 +120,7 @@ class ImportDialog:
|
||||
"on_resize": self.on_resize,
|
||||
"on_main_paned_realize": self.on_main_paned_realize,
|
||||
"on_visible_page": self.on_visible_page,
|
||||
"on_bouquets_only_switch": self.on_bouquets_only_switch,
|
||||
"on_key_press": self.on_key_press}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}imports.glade", handlers)
|
||||
@@ -144,7 +144,10 @@ class ImportDialog:
|
||||
self._dialog_window.set_transient_for(app.app_window)
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
# Options.
|
||||
self._replace_existing_switch = builder.get_object("replace_existing_switch")
|
||||
self._bouquets_only_switch = builder.get_object("bouquets_only_switch")
|
||||
self._bouquets_settings_box = builder.get_object("bouquets_settings_box")
|
||||
# Bouquets page.
|
||||
self._bq_model = builder.get_object("bq_list_store")
|
||||
self._bq_view = builder.get_object("bq_view")
|
||||
@@ -215,7 +218,7 @@ class ImportDialog:
|
||||
def on_import(self, item):
|
||||
if self._page is Page.SERVICES:
|
||||
if not any(r[-1] for r in self._bq_model):
|
||||
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
|
||||
self.show_info_message(translate("No selected item!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
|
||||
@@ -260,7 +263,12 @@ class ImportDialog:
|
||||
with suppress(ValueError):
|
||||
bq.remove(b)
|
||||
|
||||
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._ids, services)))
|
||||
if self._bouquets_only_switch.get_active():
|
||||
services = ()
|
||||
else:
|
||||
services = list(filter(lambda s: s.fav_id not in self._ids, services))
|
||||
|
||||
self._append(self._bouquets, services)
|
||||
|
||||
if self._replace_existing_switch.get_active():
|
||||
self._app.emit("services_update", {s.fav_id: s for s in filter(lambda s: s.fav_id in self._ids, services)})
|
||||
@@ -337,8 +345,8 @@ class ImportDialog:
|
||||
row = self._services_model[path][:]
|
||||
if row[1] == "IPTV":
|
||||
ref, url = get_iptv_data(row[-1])
|
||||
ref = f"{get_message('Service reference')}: {ref}"
|
||||
info = f"{get_message('Name')}: {row[0]}\n{ref}\nURL: {url}"
|
||||
ref = f"{translate('Service reference')}: {ref}"
|
||||
info = f"{translate('Name')}: {row[0]}\n{ref}\nURL: {url}"
|
||||
self._service_info_label.set_text(info)
|
||||
else:
|
||||
srv = self._services.get(row[-1], None)
|
||||
@@ -365,9 +373,7 @@ class ImportDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
@run_idle
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
@@ -408,6 +414,11 @@ class ImportDialog:
|
||||
|
||||
def on_visible_page(self, stack, param):
|
||||
self._page = Page(stack.get_visible_child_name())
|
||||
self._bouquets_settings_box.set_sensitive(self._page is Page.SERVICES)
|
||||
|
||||
def on_bouquets_only_switch(self, switch, state):
|
||||
if state:
|
||||
self._replace_existing_switch.set_active(False)
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
|
||||
1910
app/ui/iptv.glade
1910
app/ui/iptv.glade
File diff suppressed because it is too large
Load Diff
425
app/ui/iptv.py
425
app/ui/iptv.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -30,6 +30,8 @@ import concurrent.futures
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from datetime import date
|
||||
from itertools import groupby, chain
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlparse, unquote, quote
|
||||
from urllib.request import Request, urlopen
|
||||
@@ -38,19 +40,20 @@ import requests
|
||||
from gi.repository import GLib, Gio, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService, Service
|
||||
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
|
||||
parse_m3u, PICON_FORMAT)
|
||||
parse_m3u, PICON_FORMAT, export_to_m3u)
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
|
||||
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, translate, get_builder, BaseDialog
|
||||
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf, show_info_bar_message, gen_bouquet_name
|
||||
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon, HeaderBar)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
|
||||
_URL_PREFIXES = {"YT-DLP": "YT-DLP://", "YT-DL": "YT-DL://", "STREAMLINK": "streamlink://", "No": None}
|
||||
|
||||
|
||||
def is_data_correct(elems):
|
||||
@@ -81,6 +84,7 @@ class IptvDialog:
|
||||
handlers = {"on_response": self.on_response,
|
||||
"on_entry_changed": self.on_entry_changed,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
"on_url_paste": self.on_url_paste,
|
||||
"on_save": self.on_save,
|
||||
"on_stream_type_changed": self.on_stream_type_changed,
|
||||
"on_yt_quality_changed": self.on_yt_quality_changed,
|
||||
@@ -93,6 +97,7 @@ class IptvDialog:
|
||||
self._bouquet = bouquet
|
||||
self._yt_links = None
|
||||
self._yt_dl = None
|
||||
self._inserted_url = False
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
|
||||
@@ -102,7 +107,7 @@ class IptvDialog:
|
||||
self._name_entry = builder.get_object("name_entry")
|
||||
self._description_entry = builder.get_object("description_entry")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._reference_entry = builder.get_object("reference_entry")
|
||||
self._reference_label = builder.get_object("iptv_reference_label")
|
||||
self._srv_type_entry = builder.get_object("srv_type_entry")
|
||||
self._srv_id_entry = builder.get_object("srv_id_entry")
|
||||
self._sid_entry = builder.get_object("sid_entry")
|
||||
@@ -116,8 +121,10 @@ class IptvDialog:
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._yt_quality_box = builder.get_object("yt_iptv_quality_combobox")
|
||||
self._url_prefix_box = builder.get_object("iptv_url_prefix_box")
|
||||
self._url_prefix_combobox = builder.get_object("iptv_url_prefix_combobox")
|
||||
self._model, self._paths = view.get_selection().get_selected_rows()
|
||||
# style
|
||||
# Style.
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
|
||||
@@ -128,12 +135,13 @@ class IptvDialog:
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
|
||||
builder.get_object("iptv_type_label").set_visible(False)
|
||||
builder.get_object("reference_entry").set_visible(False)
|
||||
builder.get_object("iptv_reference_label").set_visible(False)
|
||||
builder.get_object("iptv_ref_box").set_visible(False)
|
||||
self._stream_type_combobox.set_visible(False)
|
||||
else:
|
||||
self._description_entry.set_visible(False)
|
||||
builder.get_object("iptv_description_label").set_visible(False)
|
||||
[self._url_prefix_combobox.append(v, k) for k, v in _URL_PREFIXES.items()]
|
||||
self._url_prefix_combobox.set_active(0)
|
||||
|
||||
if self._action is Action.ADD:
|
||||
self._save_button.set_visible(False)
|
||||
@@ -157,7 +165,14 @@ class IptvDialog:
|
||||
self.on_url_changed(self._url_entry)
|
||||
|
||||
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
|
||||
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
|
||||
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
url = self._url_entry.get_text()
|
||||
if all((self._url_prefix_box.get_visible(),
|
||||
self._url_prefix_combobox.get_active_id(),
|
||||
url.count("http") > 1 or urlparse(url).scheme.upper() in _URL_PREFIXES)):
|
||||
self.show_info_message("Invalid prefix for the given URL!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
@@ -194,7 +209,7 @@ class IptvDialog:
|
||||
elif stream_type is StreamType.E_SERVICE_HLS:
|
||||
self._stream_type_combobox.set_active(5)
|
||||
except ValueError:
|
||||
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
|
||||
self.show_info_message(f"Unknown stream type {s_type}", Gtk.MessageType.ERROR)
|
||||
|
||||
self._srv_id_entry.set_text(data[1])
|
||||
self._srv_type_entry.set_text(str(int(data[2], 16)))
|
||||
@@ -202,7 +217,17 @@ class IptvDialog:
|
||||
self._tr_id_entry.set_text(str(int(data[4], 16)))
|
||||
self._net_id_entry.set_text(str(int(data[5], 16)))
|
||||
self._namespace_entry.set_text(str(int(data[6], 16)))
|
||||
self._url_entry.set_text(unquote(data[10].strip()))
|
||||
# URL.
|
||||
url = unquote(data[10].strip())
|
||||
sch = urlparse(url).scheme.upper()
|
||||
if YouTube.get_yt_id(url) and sch in _URL_PREFIXES:
|
||||
active_prefix = _URL_PREFIXES.get(sch)
|
||||
url = re.sub(active_prefix, "", url, 1, re.IGNORECASE)
|
||||
self._url_prefix_combobox.set_active_id(active_prefix)
|
||||
else:
|
||||
self._url_prefix_combobox.set_active(len(_URL_PREFIXES) - 1)
|
||||
|
||||
self._url_entry.set_text(url)
|
||||
self.update_reference_entry()
|
||||
|
||||
def init_neutrino_data(self, fav_id):
|
||||
@@ -212,8 +237,7 @@ class IptvDialog:
|
||||
|
||||
def update_reference_entry(self):
|
||||
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
|
||||
self.on_url_changed(self._url_entry)
|
||||
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
|
||||
self._reference_label.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
|
||||
self._srv_id_entry.get_text(),
|
||||
int(self._srv_type_entry.get_text()),
|
||||
int(self._sid_entry.get_text()),
|
||||
@@ -242,15 +266,25 @@ class IptvDialog:
|
||||
if yt_id:
|
||||
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
|
||||
text = "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
|
||||
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
|
||||
entry.set_sensitive(False)
|
||||
gen = self.set_yt_url(entry, yt_id)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
if self._inserted_url and url_str.count("http") == 1:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
|
||||
entry.set_sensitive(False)
|
||||
gen = self.set_yt_url(entry, yt_id)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
else:
|
||||
self._url_prefix_box.set_visible(self._s_type is SettingsType.ENIGMA_2)
|
||||
else:
|
||||
self._url_prefix_box.set_visible(self._s_type is SettingsType.ENIGMA_2)
|
||||
self._inserted_url = False
|
||||
elif YouTube.is_yt_video_link(url_str):
|
||||
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
|
||||
else:
|
||||
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
|
||||
self._yt_quality_box.set_visible(False)
|
||||
self._url_prefix_box.set_visible(False)
|
||||
|
||||
def on_url_paste(self, entry):
|
||||
self._inserted_url = True
|
||||
self._yt_quality_box.set_visible(False)
|
||||
|
||||
def set_yt_url(self, entry, video_id):
|
||||
try:
|
||||
@@ -264,7 +298,7 @@ class IptvDialog:
|
||||
links, title = self._yt_dl.get_yt_link(video_id, entry.get_text())
|
||||
yield True
|
||||
except urllib.error.URLError as e:
|
||||
self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR)
|
||||
self.show_info_message(f"{translate('Getting link error:')} {e}", Gtk.MessageType.ERROR)
|
||||
return
|
||||
except YouTubeException as e:
|
||||
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
|
||||
@@ -279,7 +313,7 @@ class IptvDialog:
|
||||
entry.set_text(links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]])
|
||||
self._yt_links = links
|
||||
else:
|
||||
msg = get_message("Getting link error:") + " No link received for id: {}".format(video_id)
|
||||
msg = f"{translate('Getting link error:')} No link received for id: {video_id}"
|
||||
self.show_info_message(msg, Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
entry.set_sensitive(True)
|
||||
@@ -291,13 +325,25 @@ class IptvDialog:
|
||||
self.update_reference_entry()
|
||||
|
||||
def on_yt_quality_changed(self, box):
|
||||
if not self._yt_links:
|
||||
return
|
||||
|
||||
model = box.get_model()
|
||||
active = model.get_value(box.get_active_iter(), 0)
|
||||
if self._yt_links and active in self._yt_links:
|
||||
if active in self._yt_links:
|
||||
self._url_entry.set_text(self._yt_links[active])
|
||||
else:
|
||||
self._url_entry.set_text(self._yt_links.get(max(self._yt_links, default=None), ""))
|
||||
|
||||
def save_enigma2_data(self):
|
||||
name = self._name_entry.get_text().strip()
|
||||
if self._url_prefix_box.get_visible():
|
||||
prefix = self._url_prefix_combobox.get_active_id()
|
||||
url = self._url_entry.get_text().replace(':', '%3A', 1 if prefix else -1)
|
||||
url = f"{quote(prefix) if prefix else ''}{url}"
|
||||
else:
|
||||
url = quote(self._url_entry.get_text())
|
||||
|
||||
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
|
||||
self._srv_id_entry.get_text(),
|
||||
int(self._srv_type_entry.get_text()),
|
||||
@@ -305,8 +351,7 @@ class IptvDialog:
|
||||
int(self._tr_id_entry.get_text()),
|
||||
int(self._net_id_entry.get_text()),
|
||||
int(self._namespace_entry.get_text()),
|
||||
quote(self._url_entry.get_text()),
|
||||
name, name)
|
||||
url, name, name)
|
||||
|
||||
self.update_bouquet_data(name, fav_id)
|
||||
|
||||
@@ -321,7 +366,7 @@ class IptvDialog:
|
||||
self._dialog.destroy()
|
||||
|
||||
def update_bouquet_data(self, name, fav_id):
|
||||
picon_id = f"{self._reference_entry.get_text().replace(':', '_')}.png"
|
||||
picon_id = f"{self._reference_label.get_text().replace(':', '_')}.png"
|
||||
|
||||
if self._action is Action.EDIT:
|
||||
services = self._app.current_services
|
||||
@@ -347,9 +392,7 @@ class IptvDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
|
||||
class SearchUnavailableDialog:
|
||||
@@ -466,6 +509,7 @@ class IptvListDialog:
|
||||
self._data_box = builder.get_object("iptv_list_data_box")
|
||||
self._start_values_grid = builder.get_object("start_values_grid")
|
||||
self._info_bar = builder.get_object("list_configuration_info_bar")
|
||||
self._message_label = builder.get_object("list_configuration_message_label")
|
||||
self._reference_label = builder.get_object("reference_label")
|
||||
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
|
||||
self._id_default_check_button = builder.get_object("id_default_check_button")
|
||||
@@ -547,6 +591,10 @@ class IptvListDialog:
|
||||
for el in self._default_elems:
|
||||
el.set_active(True)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type=Gtk.MessageType.INFO):
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@@ -587,7 +635,7 @@ class IptvListConfigurationDialog(IptvListDialog):
|
||||
@run_idle
|
||||
def on_apply(self, item):
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
@@ -634,7 +682,7 @@ class IptvListConfigurationDialog(IptvListDialog):
|
||||
self._bouquet.clear()
|
||||
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._ok_button.set_visible(True)
|
||||
|
||||
|
||||
@@ -648,61 +696,77 @@ class M3uImportDialog(IptvListDialog):
|
||||
self._picons = app.picons
|
||||
self._pic_path = app._settings.profile_picons_path
|
||||
self._services = None
|
||||
self._epg_src = None
|
||||
self._url_count = 0
|
||||
self._errors_count = 0
|
||||
self._max_count = 0
|
||||
self._is_download = False
|
||||
self._cancellable = Gio.Cancellable()
|
||||
self._dialog.set_title(get_message("Playlist import"))
|
||||
self._dialog.set_title(translate("Playlist import"))
|
||||
self._dialog.connect("delete-event", self.on_close)
|
||||
self._apply_button.set_label(get_message("Import"))
|
||||
# Progress
|
||||
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
|
||||
self._spinner = Gtk.Spinner(active=False)
|
||||
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
|
||||
load_label = Gtk.Label(label=get_message("Loading data..."))
|
||||
self._spinner.bind_property("active", self._spinner, "visible")
|
||||
self._spinner.bind_property("visible", load_label, "visible")
|
||||
self._apply_button.set_label(translate("Import"))
|
||||
# Extra box.
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("import_m3u_box",))
|
||||
self._info_label = builder.get_object("info_label")
|
||||
self._progress_bar = builder.get_object("progress_bar")
|
||||
self._spinner = builder.get_object("spinner")
|
||||
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
|
||||
self._picon_switch = builder.get_object("picon_switch")
|
||||
self._picon_box = builder.get_object("picon_box")
|
||||
# Type import buttons.
|
||||
self._current_bq_button = builder.get_object("current_bq_button")
|
||||
self._single_bq_button = builder.get_object("single_bq_button")
|
||||
self._group_bq_button = builder.get_object("group_bq_button")
|
||||
self._sub_bq_button = builder.get_object("sub_bq_button")
|
||||
# EPG src.
|
||||
self._epg_links_button = builder.get_object("epg_links_box")
|
||||
self._add_epg_src_switch = builder.get_object("add_epg_src_switch")
|
||||
|
||||
progress_box = Gtk.HBox(visible=True, spacing=2)
|
||||
progress_box.add(self._progress_bar)
|
||||
progress_box.pack_end(self._spinner, False, False, 0)
|
||||
progress_box.pack_start(load_label, False, False, 0)
|
||||
# Picons
|
||||
self._picons_switch = Gtk.Switch(visible=True)
|
||||
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=5)
|
||||
self._picon_box.pack_end(self._picons_switch, False, False, 0)
|
||||
self._picon_box.pack_end(Gtk.Label(visible=True, label=get_message("Download picons")), False, False, 0)
|
||||
# Extra box
|
||||
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
|
||||
extra_box.set_center_widget(progress_box)
|
||||
extra_box.pack_start(self._info_label, False, False, 5)
|
||||
extra_box.pack_end(self._picon_box, True, True, 5)
|
||||
|
||||
frame = Gtk.Frame(visible=True, margin_bottom=5)
|
||||
frame.add(extra_box)
|
||||
self._data_box.add(frame)
|
||||
m3u_box = builder.get_object("import_m3u_box")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
self._data_box.add(m3u_box)
|
||||
else:
|
||||
self._data_box.set_visible(False)
|
||||
self._group_bq_button.set_sensitive(False)
|
||||
self._sub_bq_button.set_sensitive(False)
|
||||
m3u_box.set_margin_start(5)
|
||||
m3u_box.set_margin_end(5)
|
||||
area = self._dialog.get_content_area()
|
||||
area.pack_start(m3u_box, True, True, 0)
|
||||
area.reorder_child(m3u_box, 0)
|
||||
|
||||
self.get_m3u(m3_path, s_type)
|
||||
|
||||
@run_task
|
||||
def get_m3u(self, path, s_type):
|
||||
try:
|
||||
GLib.idle_add(self._spinner.set_property, "active", True)
|
||||
self._services = parse_m3u(path, s_type)
|
||||
GLib.idle_add(self._spinner.start)
|
||||
self._epg_src, self._services = parse_m3u(path, s_type)
|
||||
for s in self._services:
|
||||
if s.picon:
|
||||
GLib.idle_add(self._picon_box.set_sensitive, True)
|
||||
break
|
||||
finally:
|
||||
msg = f"{get_message('Streams detected:')} {len(self._services) if self._services else 0}."
|
||||
GLib.idle_add(self._info_label.set_text, msg)
|
||||
GLib.idle_add(self._spinner.set_property, "active", False)
|
||||
self.update_info()
|
||||
|
||||
@run_idle
|
||||
def update_info(self):
|
||||
msg = f"{translate('Streams detected:')} {len(self._services) if self._services else 0}."
|
||||
self._info_label.set_text(msg)
|
||||
self._spinner.stop()
|
||||
|
||||
if self._epg_src:
|
||||
self._epg_links_button.set_visible(True)
|
||||
[self._epg_links_button.append(u, u) for u in self._epg_src]
|
||||
self._epg_links_button.set_active(0)
|
||||
|
||||
def on_apply(self, item):
|
||||
if self._current_bq_button.get_active() and not self._app.current_bouquet:
|
||||
self.show_info_message("Error. No bouquet is selected!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
picons = {}
|
||||
@@ -732,18 +796,76 @@ class M3uImportDialog(IptvListDialog):
|
||||
|
||||
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
|
||||
|
||||
if self._picons_switch.get_active():
|
||||
if self._add_epg_src_switch.get_active():
|
||||
self.on_add_epg_source()
|
||||
|
||||
if self._picon_switch.get_active():
|
||||
if self.is_default_values():
|
||||
show_dialog(DialogType.ERROR, self._dialog,
|
||||
"Set values for TID, NID and Namespace for correct naming of the picons!")
|
||||
msg = "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
self.show_info_message(msg, Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
self.download_picons(picons)
|
||||
else:
|
||||
GLib.idle_add(self._ok_button.set_visible, True)
|
||||
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
|
||||
self.on_apply_done()
|
||||
|
||||
self._app.append_imported_services(services)
|
||||
self.import_services(services)
|
||||
|
||||
def import_services(self, services):
|
||||
if self._current_bq_button.get_active():
|
||||
self._app.append_imported_services(services)
|
||||
return
|
||||
|
||||
s_type = self._app.app_settings.setting_type
|
||||
model = self._app.bouquets_view.get_model()
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
itr = model.get_iter_first()
|
||||
else:
|
||||
# We will use the 'FAV' section for Neutrino!
|
||||
itr = model.get_iter(Gtk.TreePath.new_from_indices([1]))
|
||||
|
||||
bqs = self._app.current_bouquets
|
||||
bq_type = model.get_value(itr, Column.BQ_TYPE)
|
||||
def_bq_name = gen_bouquet_name(bqs, f"IPTV {date.today()} ", bq_type)
|
||||
|
||||
if self._single_bq_button.get_active():
|
||||
self.append_bouquet(def_bq_name, bq_type, bqs, model, itr, services)
|
||||
else:
|
||||
# Sub-bouquets.
|
||||
if self._sub_bq_button.get_active():
|
||||
itr = self.append_bouquet(gen_bouquet_name(bqs, def_bq_name, bq_type), bq_type, bqs, model, itr, ())
|
||||
# Generating groups with skipping markers.
|
||||
m_name = BqServiceType.MARKER.value
|
||||
def_bq_name = f"{def_bq_name} [No group]"
|
||||
gr = self.get_services_groups(filter(lambda s: s.service_type != m_name, services), def_bq_name)
|
||||
[self.append_bouquet(gen_bouquet_name(bqs, g, bq_type), bq_type, bqs, model, itr, s) for g, s in gr.items()]
|
||||
|
||||
def append_bouquet(self, bq_name, bq_type, bqs, model, itr, services):
|
||||
""" Adds new bouquet and returns iter of appended row. """
|
||||
cur_services = self._app.current_services
|
||||
bqs[f"{bq_name}:{bq_type}"] = [s.fav_id for s in services]
|
||||
cur_services.update({s.fav_id: s for s in services})
|
||||
bq = (bq_name, None, None, bq_type)
|
||||
return model.append(itr, bq)
|
||||
|
||||
def get_services_groups(self, services, def_gr_name="No group"):
|
||||
def grouper(s):
|
||||
return s.package or def_gr_name
|
||||
|
||||
return {k: list(v) for k, v in groupby(sorted(services, key=grouper), key=grouper)}
|
||||
|
||||
def on_add_epg_source(self):
|
||||
active_src = self._epg_links_button.get_active_id()
|
||||
settings = self._app.app_settings
|
||||
sources = settings.epg_xml_sources
|
||||
log(f"Adding an EPG source -> {active_src}")
|
||||
if active_src not in set(sources):
|
||||
sources.append(active_src)
|
||||
settings.epg_xml_sources = sources
|
||||
self._app.emit("epg-settings-changed", None)
|
||||
else:
|
||||
log(f"{translate('This URL already exists!')}")
|
||||
|
||||
@run_task
|
||||
def download_picons(self, picons):
|
||||
@@ -825,10 +947,15 @@ class M3uImportDialog(IptvListDialog):
|
||||
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
|
||||
yield True
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self._ok_button.set_visible(True)
|
||||
self.on_apply_done()
|
||||
yield True
|
||||
|
||||
@run_idle
|
||||
def on_apply_done(self):
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._ok_button.set_visible(True)
|
||||
self._picon_box.set_sensitive(False)
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return True
|
||||
@@ -847,6 +974,109 @@ class M3uImportDialog(IptvListDialog):
|
||||
return False
|
||||
|
||||
|
||||
class ExportM3uDialog(BaseDialog):
|
||||
def __init__(self, app, bouquets):
|
||||
super().__init__(app.app_window, "Export to m3u",
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK))
|
||||
self._app = app
|
||||
self._bouquets = bouquets
|
||||
self._url = None
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("export_m3u_box",))
|
||||
self._main_grid = builder.get_object("export_m3u_grid")
|
||||
self._port_entry = builder.get_object("export_port_entry")
|
||||
self._port_auto_button = builder.get_object("export_auto_button")
|
||||
self._all_type_button = builder.get_object("export_all_button")
|
||||
self._iptv_type_button = builder.get_object("export_iptv_button")
|
||||
self._grp_bq_button = builder.get_object("export_grp_bq_button")
|
||||
self._grp_marker_button = builder.get_object("export_grp_markers_button")
|
||||
self._bq_count_label = builder.get_object("export_bq_count_label")
|
||||
self._services_count_label = builder.get_object("export_services_count_label")
|
||||
self.get_content_area().pack_start(builder.get_object("export_m3u_box"), False, False, 0)
|
||||
|
||||
is_enigma = self._app.is_enigma
|
||||
self._port_auto_button.set_active(True) if is_enigma else self._main_grid.remove_row(0)
|
||||
self._grp_marker_button.set_visible(is_enigma)
|
||||
self._all_type_button.set_active(True) if is_enigma else self._iptv_type_button.set_active(True)
|
||||
self._all_type_button.set_sensitive(is_enigma)
|
||||
|
||||
self.connect("response", self.on_response)
|
||||
self.connect("realize", self.init)
|
||||
|
||||
def init(self, widget=None):
|
||||
self._bq_count_label.set_text(str(len(self._bouquets)))
|
||||
self._services_count_label.set_text(str(len(list(chain.from_iterable(self._bouquets.values())))))
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response != Gtk.ResponseType.OK:
|
||||
self.destroy()
|
||||
else:
|
||||
if self._app.is_enigma and self._port_auto_button.get_active():
|
||||
self.do_export_auto()
|
||||
else:
|
||||
self.do_export()
|
||||
return True
|
||||
|
||||
def do_export_auto(self, button=None):
|
||||
""" Retrieves streaming port from Receiver via HTTP API and starts export.
|
||||
|
||||
Since the streaming port can be changed by the user,
|
||||
we're getting base link to the stream -> http(s)://IP:PORT/
|
||||
"""
|
||||
from app.connections import HttpAPI
|
||||
|
||||
sent = self._app.send_http_request(HttpAPI.Request.STREAM, "", self.start_export)
|
||||
self._port_auto_button.set_active(sent)
|
||||
self._port_auto_button.set_sensitive(sent)
|
||||
|
||||
def start_export(self, data):
|
||||
self._port_auto_button.set_active("error_code" not in data)
|
||||
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
url = urlparse(url)
|
||||
if all((url.scheme, url.port)):
|
||||
self._url = url.geturl()
|
||||
self._port_entry.set_text(str(url.port))
|
||||
self.do_export()
|
||||
|
||||
@run_idle
|
||||
def do_export(self):
|
||||
self.destroy()
|
||||
|
||||
services = self._app.current_services
|
||||
|
||||
def get_service(fav_id, num=0):
|
||||
srv = services.get(fav_id, None)
|
||||
if srv:
|
||||
s_type = BqServiceType(srv.service_type)
|
||||
if s_type is BqServiceType.DEFAULT:
|
||||
srv = services.get(fav_id, None)
|
||||
s_data = srv.picon_id.rstrip(".png").replace("_", ":") if srv.picon_id else None
|
||||
return BouquetService(srv.service, s_type, s_data, num)
|
||||
return BouquetService(srv.service, s_type, fav_id, num)
|
||||
return BouquetService("N/A", BqServiceType.MARKER, fav_id, num)
|
||||
|
||||
# Preparing bouquets data.
|
||||
bouquets = {b[:b.rindex(":")]: [get_service(i) for i in s] for b, s in self._bouquets.items()}
|
||||
|
||||
bq_services = []
|
||||
s_types = {BqServiceType.IPTV}
|
||||
if self._all_type_button.get_active():
|
||||
s_types.add(BqServiceType.DEFAULT)
|
||||
|
||||
if self._grp_bq_button.get_active():
|
||||
for b, bs in bouquets.items():
|
||||
bq_services.append(BouquetService(b, BqServiceType.MARKER, None, 0))
|
||||
bq_services.extend(filter(lambda s: s.type in s_types, bs))
|
||||
elif self._grp_marker_button.get_active():
|
||||
bq_services = chain.from_iterable(bouquets.values())
|
||||
else:
|
||||
bq_services = filter(lambda s: s.type in s_types, chain.from_iterable(bouquets.values()))
|
||||
|
||||
file_name = f"{'_'.join(list(bouquets)[:10])}__{date.today().strftime('%Y_%m_%d')}"
|
||||
self._app.save_bouquet_to_m3u(bq_services, self._url, file_name)
|
||||
|
||||
|
||||
class YtListImportDialog:
|
||||
def __init__(self, app):
|
||||
handlers = {"on_import": self.on_import,
|
||||
@@ -887,12 +1117,17 @@ class YtListImportDialog:
|
||||
self._import_button = builder.get_object("yt_import_button")
|
||||
self._quality_box = builder.get_object("yt_quality_combobox")
|
||||
self._quality_model = builder.get_object("yt_quality_liststore")
|
||||
self._import_button.bind_property("visible", self._quality_box, "visible")
|
||||
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
|
||||
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
|
||||
self._extract_switch = builder.get_object("yt_extract_links_switch")
|
||||
|
||||
self._url_prefix_combobox = builder.get_object("yt_url_prefix_combobox")
|
||||
[self._url_prefix_combobox.append(v, k) for k, v in _URL_PREFIXES.items()]
|
||||
self._url_prefix_combobox.set_active(0)
|
||||
|
||||
builder.get_object("yt_extract_links_box").set_visible(self._s_type is SettingsType.ENIGMA_2)
|
||||
builder.get_object("yt_url_prefix_box").set_visible(self._s_type is SettingsType.ENIGMA_2)
|
||||
|
||||
if self._settings.use_header_bar:
|
||||
header_bar = HeaderBar(title="YouTube", subtitle=get_message("Playlist import"))
|
||||
header_bar = HeaderBar(title="YouTube", subtitle=translate("Playlist import"))
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
actions_box = builder.get_object("yt_actions_box")
|
||||
import_box = builder.get_object("yt_import_box")
|
||||
@@ -905,19 +1140,31 @@ class YtListImportDialog:
|
||||
window_size = self._settings.get("yt_import_dialog_size")
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
# Style
|
||||
# Style.
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
|
||||
@run_task
|
||||
def on_import(self, item):
|
||||
self.on_info_bar_close()
|
||||
self.update_active_elements(False)
|
||||
|
||||
if self._extract_switch.get_active():
|
||||
self.extract_direct_links()
|
||||
else:
|
||||
prefix = self._url_prefix_combobox.get_active_id()
|
||||
selected = filter(lambda r: r[2], self._model)
|
||||
prefix = quote(prefix) if prefix else ''
|
||||
links = [(f"{prefix}https{quote(':')}//www.youtube.com/watch?v={r[1]}", r[0]) for r in selected]
|
||||
self.append_services(links)
|
||||
self.update_active_elements(True)
|
||||
|
||||
@run_task
|
||||
def extract_direct_links(self):
|
||||
self._download_task = True
|
||||
|
||||
try:
|
||||
@@ -946,7 +1193,6 @@ class YtListImportDialog:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
if self._download_task:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
self.append_services([done_links[r] for r in rows])
|
||||
finally:
|
||||
self._download_task = False
|
||||
@@ -989,22 +1235,31 @@ class YtListImportDialog:
|
||||
aggr = [None] * 9
|
||||
srvs = []
|
||||
|
||||
if self._yt_list_title:
|
||||
if self._yt_list_title and self._s_type is SettingsType.ENIGMA_2:
|
||||
title = self._yt_list_title
|
||||
fav_id = MARKER_FORMAT.format(0, title, title)
|
||||
mk = Service(None, None, None, title, *aggr[0:3], BqServiceType.MARKER.name, *aggr, 0, fav_id, None)
|
||||
srvs.append(mk)
|
||||
|
||||
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
|
||||
extract = self._extract_switch.get_active()
|
||||
|
||||
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0) if extract else None
|
||||
for link in links:
|
||||
lnk, title = link or (None, None)
|
||||
if not lnk:
|
||||
continue
|
||||
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
|
||||
fav_id = get_fav_id(ln, title, self._s_type)
|
||||
|
||||
if extract:
|
||||
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
|
||||
else:
|
||||
ln = lnk
|
||||
|
||||
fav_id = get_fav_id(ln, title, self._s_type, force_quote=extract)
|
||||
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
|
||||
srvs.append(srv)
|
||||
|
||||
self.appender(srvs)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
|
||||
@run_idle
|
||||
def update_active_elements(self, sensitive):
|
||||
@@ -1035,9 +1290,7 @@ class YtListImportDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -36,41 +36,77 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkFrame" id="log_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>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<object class="GtkViewport">
|
||||
<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="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_box">
|
||||
<object class="GtkBox" id="main_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">2</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-clear</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="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Close</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="close_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-close</property>
|
||||
</object>
|
||||
</child>
|
||||
</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>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -80,65 +116,47 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Close</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="close_button_image">
|
||||
<object class="GtkTextView" id="log_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-close</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left-margin">5</property>
|
||||
<property name="right-margin">5</property>
|
||||
<property name="top-margin">5</property>
|
||||
<property name="bottom-margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="log_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="top_margin">5</property>
|
||||
<property name="bottom_margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="log_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">Logs</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
783
app/ui/m3u.glade
Normal file
783
app/ui/m3u.glade
Normal file
@@ -0,0 +1,783 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.40.0
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkBox" id="export_m3u_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkViewport" id="export_viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<!-- n-columns=2 n-rows=3 -->
|
||||
<object class="GtkGrid" id="export_m3u_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_port_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Port:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Export:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="export_types_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_all_button">
|
||||
<property name="label" translatable="yes">All types</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_iptv_button">
|
||||
<property name="label" translatable="yes">IPTV only</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">export_all_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="export_grp_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_grp_bq_button">
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_grp_markers_button">
|
||||
<property name="label" translatable="yes">Markers</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">export_grp_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_grp_no_button">
|
||||
<property name="label" translatable="yes">No</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">export_grp_markers_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_port_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="export_iptv_button" bind-property="active" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="export_port_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="export_auto_button" bind-property="active" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="width-chars">10</property>
|
||||
<property name="primary-icon-name">document-edit-symbolic</property>
|
||||
<property name="placeholder-text" translatable="yes">8001</property>
|
||||
<property name="input-purpose">digits</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="export_auto_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-synchronizing-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">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Auto</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_group_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Group by</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="export_info_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="icon-name">document-properties</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_bq_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_bq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_bq_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label">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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_service_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_services_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Services</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_services_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label">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">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</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>
|
||||
</object>
|
||||
<object class="GtkBox" id="import_m3u_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="import_m3u_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="label-xalign">0.02</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkViewport" id="import_viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="import_main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="import_type_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="current_bq_button">
|
||||
<property name="label" translatable="yes">Current bouquet</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">sub_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="single_bq_button">
|
||||
<property name="label" translatable="yes">Single bouquet</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">current_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="group_bq_button">
|
||||
<property name="label" translatable="yes">Split by groups</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">sub_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="sub_bq_button">
|
||||
<property name="label" translatable="yes">Create sub-bouquets</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">current_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="load_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">15</property>
|
||||
<child type="center">
|
||||
<object class="GtkProgressBar" id="progress_bar">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_label">
|
||||
<property name="visible" bind-source="spinner" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max-width-chars">30</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="load_label">
|
||||
<property name="visible" bind-source="spinner" bind-property="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Loading data...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="visible" bind-source="spinner" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="picon_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="picon_switch_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Download picons</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="picon_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</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="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_src_box">
|
||||
<property name="height-request">30</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="epg_source_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">EPG source</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="epg_links_box">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="epg_info_label">
|
||||
<property name="visible" bind-source="epg_links_box" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="label" translatable="yes">Not found.</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="add_epg_src_box">
|
||||
<property name="visible" bind-source="epg_links_box" bind-property="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="add_epg_src_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Add to EPG sources list</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="add_epg_src_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</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="pack-type">end</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="import_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">Import</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
6343
app/ui/main.glade
6343
app/ui/main.glade
File diff suppressed because it is too large
Load Diff
607
app/ui/main.py
607
app/ui/main.py
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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,24 +33,25 @@ __all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "
|
||||
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection", "get_service_reference",
|
||||
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
|
||||
"get_iptv_data", "update_entry_data", "append_text_to_tview", "on_popup_menu", "get_picon_file_name",
|
||||
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num")
|
||||
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num",
|
||||
"show_info_bar_message", "gen_bouquet_name")
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
from gi.repository import GdkPixbuf, GLib
|
||||
from gi.repository import GdkPixbuf, GLib, Gio
|
||||
|
||||
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, IS_DARWIN, IS_LINUX
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .dialogs import show_dialog, DialogType, translate
|
||||
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
|
||||
|
||||
|
||||
@@ -430,7 +431,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
|
||||
picons_files = []
|
||||
|
||||
if not src_path:
|
||||
dialog = get_picon_dialog(transient, get_message("Picon selection"), get_message("Open"), False)
|
||||
dialog = get_picon_dialog(transient, translate("Picon selection"), translate("Open"), False)
|
||||
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) or not dialog.get_filenames():
|
||||
return picons_files
|
||||
|
||||
@@ -546,13 +547,13 @@ def remove_picons(settings, picon_ids, picons):
|
||||
shutil.move(src, backup_path + p_id)
|
||||
|
||||
|
||||
def is_only_one_item_selected(paths, transient):
|
||||
def is_only_one_item_selected(paths, app):
|
||||
if len(paths) > 1:
|
||||
show_dialog(DialogType.ERROR, transient, "Please, select only one item!")
|
||||
app.show_error_message("Please, select only one item!")
|
||||
return False
|
||||
|
||||
if not paths:
|
||||
show_dialog(DialogType.ERROR, transient, "No selected item!")
|
||||
app.show_error_message("No selected item!")
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -565,6 +566,19 @@ def get_picon_pixbuf(path, size=32):
|
||||
pass # NOP
|
||||
|
||||
|
||||
def get_pixbuf_from_data(img_data, w=48, h=32):
|
||||
if img_data:
|
||||
f = Gio.MemoryInputStream.new_from_data(img_data)
|
||||
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
|
||||
|
||||
|
||||
def get_pixbuf_at_scale(path, width, height, p_ratio):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, p_ratio)
|
||||
except GLib.GError:
|
||||
pass
|
||||
|
||||
|
||||
@lru_cache(50)
|
||||
def get_picon_file_name(service_name):
|
||||
""" Returns picon file name by service name. """
|
||||
@@ -574,44 +588,89 @@ def get_picon_file_name(service_name):
|
||||
|
||||
# ***************** Bouquets ********************* #
|
||||
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
|
||||
def gen_bouquets(app, gen_type):
|
||||
""" Auto-generate and append list of bouquets. """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
single_types = (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE)
|
||||
if gen_type in single_types:
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
model, paths = app.services_view.get_selection().get_selected_rows()
|
||||
single_types = {BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE}
|
||||
if gen_type in single_types and not is_only_one_item_selected(paths, app):
|
||||
return
|
||||
|
||||
fav_id_index = Column.SRV_FAV_ID
|
||||
index = Column.SRV_TYPE
|
||||
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
|
||||
index = Column.SRV_PACKAGE
|
||||
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
|
||||
index = Column.SRV_POS
|
||||
|
||||
# Splitting services [caching] by column value.
|
||||
s_data = defaultdict(list)
|
||||
for row in model:
|
||||
s_data[row[index]].append(BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0))
|
||||
ids = {row[Column.SRV_FAV_ID] for row in model}
|
||||
services = [v for k, v in app.current_services.items() if k in ids]
|
||||
|
||||
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
|
||||
bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1
|
||||
bq_root_iter = bq_view.get_model().get_iter(bq_index)
|
||||
srv = Service(*model[paths][:Column.SRV_TOOLTIP])
|
||||
cond = srv.package if gen_type is BqGenType.PACKAGE else srv.pos if gen_type is BqGenType.SAT else srv.service_type
|
||||
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
|
||||
|
||||
if gen_type is BqGenType.TYPE and cond == "Data":
|
||||
msg = f"{translate('Selected type:')} '{cond}'\n\n{translate('Are you sure?')}"
|
||||
if show_dialog(DialogType.QUESTION, app.app_window, msg) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
def grouper(s):
|
||||
data = s[index]
|
||||
return data if data else "None"
|
||||
|
||||
services = {k: list(v) for k, v in groupby(sorted(services, key=grouper), key=grouper)}
|
||||
|
||||
bq_view = app.bouquets_view
|
||||
bq_type = BqType.TV.value if app.is_enigma else BqType.BOUQUET.value
|
||||
bq_index = 0 if app.is_enigma else 1
|
||||
bq_root_iter = bq_view.get_model().get_iter(bq_index)
|
||||
|
||||
bq_names = get_bouquets_names(bq_view.get_model())
|
||||
|
||||
if gen_type in single_types:
|
||||
if cond in bq_names:
|
||||
show_dialog(DialogType.ERROR, transient, "A bouquet with that name exists!")
|
||||
else:
|
||||
callback(Bouquet(cond, bq_type, s_data.get(cond)), bq_root_iter)
|
||||
app.show_error_message("A bouquet with that name exists!")
|
||||
return
|
||||
|
||||
bq_services = get_services_type_groups(services.get(cond, []))
|
||||
if app.is_enigma:
|
||||
if srv.service_type == "Radio":
|
||||
bq_index = 1
|
||||
bq_type = BqType.RADIO.value
|
||||
bq_root_iter = bq_view.get_model().get_iter(bq_index)
|
||||
bq_view.expand_row(Gtk.TreePath(bq_index), 1)
|
||||
bq_services = bq_services.get("Radio", [])
|
||||
else:
|
||||
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
|
||||
bq_services = bq_services.get("Data" if srv.service_type == "Data" else "TV", [])
|
||||
app.append_bouquet(Bouquet(cond, bq_type, get_bouquet_services(bq_services)), bq_root_iter)
|
||||
else:
|
||||
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
|
||||
# We add a bouquet only if the given name is missing [keys - names]!
|
||||
for name in sorted(s_data.keys() - bq_names):
|
||||
callback(Bouquet(name, BqType.TV.value, s_data.get(name)), bq_root_iter)
|
||||
if gen_type is BqGenType.EACH_SAT:
|
||||
bq_names = sorted(services.keys() - bq_names, key=get_pos_num, reverse=True)
|
||||
else:
|
||||
bq_names = sorted(services.keys() - bq_names)
|
||||
|
||||
tv_bqs = []
|
||||
radio_bqs = []
|
||||
for n in bq_names:
|
||||
bqs = services.get(n, [])
|
||||
# TV and Radio separation.
|
||||
bq_grp = get_services_type_groups(bqs)
|
||||
tv_bq = bq_grp.get("TV", [])
|
||||
tv_bqs.append(Bouquet(n, BqType.TV.value, get_bouquet_services(tv_bq))) if tv_bq else None
|
||||
radio_bq = bq_grp.get("Radio", [])
|
||||
radio_bqs.append(Bouquet(n, BqType.RADIO.value, get_bouquet_services(radio_bq))) if radio_bq else None
|
||||
|
||||
[app.append_bouquet(b, bq_root_iter) for b in tv_bqs]
|
||||
if app.is_enigma:
|
||||
bq_root_iter = bq_view.get_model().get_iter(bq_index + 1)
|
||||
bq_view.expand_row(Gtk.TreePath(bq_index + 1), 0)
|
||||
[app.append_bouquet(b, bq_root_iter) for b in radio_bqs]
|
||||
|
||||
|
||||
def get_bouquet_services(services):
|
||||
services.sort(key=lambda s: s.service)
|
||||
return [BouquetService(None, BqServiceType.DEFAULT, s.fav_id, 0) for s in services]
|
||||
|
||||
|
||||
def get_bouquets_names(model):
|
||||
@@ -627,12 +686,41 @@ def get_bouquets_names(model):
|
||||
return bouquets_names
|
||||
|
||||
|
||||
def gen_bouquet_name(bouquets, base_name, bq_type):
|
||||
""" Generates a name for new bouquets. """
|
||||
count = 0
|
||||
key = f"{base_name}:{bq_type}"
|
||||
bq_name = base_name
|
||||
while key in bouquets:
|
||||
count += 1
|
||||
bq_name = f"{base_name}{count}"
|
||||
key = f"{bq_name}:{bq_type}"
|
||||
|
||||
return bq_name
|
||||
|
||||
|
||||
def get_services_type_groups(services):
|
||||
""" Returns services grouped by main types [TV, Radio, Data]. -> dict """
|
||||
|
||||
def type_grouper(s):
|
||||
s_type = s.service_type
|
||||
|
||||
if s_type == "Data":
|
||||
return s_type
|
||||
elif s_type == "Radio":
|
||||
return s_type
|
||||
else:
|
||||
return "TV"
|
||||
|
||||
return {k: list(v) for k, v in groupby(sorted(services, key=type_grouper), key=type_grouper)}
|
||||
|
||||
|
||||
# ***************** Others ********************* #
|
||||
|
||||
def copy_reference(view, app):
|
||||
""" Copying picon id to clipboard. """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not is_only_one_item_selected(paths, app.app_window):
|
||||
if not is_only_one_item_selected(paths, app):
|
||||
return
|
||||
|
||||
target = app.get_target_view(view)
|
||||
@@ -745,13 +833,16 @@ def append_text_to_tview(char, view):
|
||||
|
||||
|
||||
def get_iptv_url(row, s_type, column=Column.FAV_ID):
|
||||
""" Returns url from iptv type row """
|
||||
""" Returns URL from IPTV type row. """
|
||||
data = row[column].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
data = list(filter(lambda x: "http" in x, data))
|
||||
|
||||
if data:
|
||||
url = data[0]
|
||||
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if len(data) > 10 and "http" in data[10]:
|
||||
url, sep, desc = data[10].partition("#DESCRIPTION")
|
||||
return unquote(url.strip())
|
||||
else:
|
||||
return data[0]
|
||||
|
||||
|
||||
def get_iptv_data(fav_id):
|
||||
@@ -759,7 +850,7 @@ def get_iptv_data(fav_id):
|
||||
data, sep, desc = fav_id.partition("#DESCRIPTION")
|
||||
data = data.split(":")
|
||||
if len(data) < 11:
|
||||
return None, None, desc
|
||||
return None, desc
|
||||
return ":".join(data[:10]), unquote(data[10].strip())
|
||||
|
||||
|
||||
@@ -769,5 +860,13 @@ def on_popup_menu(menu, event):
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
|
||||
def show_info_bar_message(bar, label, text, message_type=Gtk.MessageType.INFO):
|
||||
""" Shows a message for info bars. """
|
||||
bar.set_visible(False)
|
||||
label.set_text(translate(text))
|
||||
bar.set_message_type(message_type)
|
||||
bar.set_visible(True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
2781
app/ui/picons.glade
2781
app/ui/picons.glade
File diff suppressed because it is too large
Load Diff
109
app/ui/picons.py
109
app/ui/picons.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -30,10 +30,11 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
from enum import Enum
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from gi.repository import GLib, GdkPixbuf, Gio
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task, run_with_delay, log
|
||||
from app.connections import upload_data, DownloadType, download_data, remove_picons
|
||||
@@ -41,9 +42,9 @@ 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 .dialogs import show_dialog, DialogType, translate, get_builder, get_chooser_dialog
|
||||
from .main_helper import (scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog,
|
||||
get_picon_file_name)
|
||||
get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale)
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
|
||||
|
||||
|
||||
@@ -52,10 +53,11 @@ class PiconManager(Gtk.Box):
|
||||
LYNG_SAT = "lyngsat"
|
||||
PICON_CZ = "piconcz"
|
||||
|
||||
def __init__(self, app, settings, picon_ids, sat_positions, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, app, settings, picon_ids, sat_positions, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-open", self.on_open)
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
self._app.connect("data-send", self.on_send)
|
||||
self._app.connect("page-changed", self.update_picons_dest)
|
||||
@@ -192,6 +194,8 @@ class PiconManager(Gtk.Box):
|
||||
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)
|
||||
column = builder.get_object("dest_title_column")
|
||||
column.set_cell_data_func(builder.get_object("title_dest_renderer"), self.title_data_func)
|
||||
# Settings
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
@@ -201,8 +205,8 @@ class PiconManager(Gtk.Box):
|
||||
self.show()
|
||||
|
||||
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
|
||||
message = get_message("To automatically set the identifiers for picons,\n"
|
||||
"first load the required services list into the main application window.")
|
||||
message = translate("To automatically set the identifiers for picons,\n"
|
||||
"first load the required services list into the main application window.")
|
||||
self.show_info_message(message, Gtk.MessageType.WARNING)
|
||||
self._satellite_label.show()
|
||||
|
||||
@@ -228,8 +232,11 @@ class PiconManager(Gtk.Box):
|
||||
if is_explorer:
|
||||
self.update_picons_data(self._picons_dest_view)
|
||||
|
||||
def on_open(self):
|
||||
def on_open(self, app, page):
|
||||
""" Opens picons from local path [in src view]. """
|
||||
if page is not Page.PICONS:
|
||||
return
|
||||
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, title="Open folder")
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
@@ -291,7 +298,18 @@ class PiconManager(Gtk.Box):
|
||||
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))
|
||||
renderer.set_property("pixbuf", get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
|
||||
|
||||
def title_data_func(self, column, renderer, model, itr, data):
|
||||
srv = self._services.get(model[itr][1], None)
|
||||
if srv:
|
||||
renderer.set_property("markup", self.get_picon_info_markup(srv))
|
||||
|
||||
def get_picon_info_markup(self, srv):
|
||||
ext_info = "" if srv.service_type == "IPTV" else f" {srv.pos} {srv.freq}"
|
||||
return (f'{escape(srv.picon_id)}\n\n'
|
||||
f'<span size="small" weight="bold">{translate("Service")}: {escape(srv.service)}</span>\n'
|
||||
f'<span size="small" style="italic">{srv.service_type}{ext_info}</span>')
|
||||
|
||||
def update_picons_from_file(self, view, uri):
|
||||
""" Adds picons in the view on dragging from file system. """
|
||||
@@ -303,18 +321,12 @@ class PiconManager(Gtk.Box):
|
||||
model = get_base_model(view.get_model())
|
||||
|
||||
if path.is_file():
|
||||
p = self.get_pixbuf_at_scale(f_path, 72, 48, True)
|
||||
p = get_pixbuf_at_scale(f_path, 72, 48, True)
|
||||
if p:
|
||||
model.append((p, path.name, f_path))
|
||||
elif path.is_dir():
|
||||
self.update_picons_data(view, f_path)
|
||||
|
||||
def get_pixbuf_at_scale(self, path, width, height, p_ratio):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, p_ratio)
|
||||
except GLib.GError:
|
||||
pass
|
||||
|
||||
# ***************** Drag-and-drop ********************* #
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
@@ -386,7 +398,7 @@ class PiconManager(Gtk.Box):
|
||||
paths = {r[1]: r.iter for r in dest_model}
|
||||
|
||||
for p_path in picons:
|
||||
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
|
||||
p = get_pixbuf_at_scale(p_path, 72, 48, True)
|
||||
if p:
|
||||
p_name = Path(p_path).name
|
||||
itr = paths.get(p_name, None)
|
||||
@@ -424,8 +436,8 @@ class PiconManager(Gtk.Box):
|
||||
shutil.copy(src, dst)
|
||||
for row in get_base_model(self._picons_dest_view.get_model()):
|
||||
if name == row[1]:
|
||||
row[0] = self.get_pixbuf_at_scale(row[-1], 72, 48, True)
|
||||
img.set_from_pixbuf(self.get_pixbuf_at_scale(row[-1], 100, 60, True))
|
||||
row[0] = get_pixbuf_at_scale(row[-1], 72, 48, True)
|
||||
img.set_from_pixbuf(get_pixbuf_at_scale(row[-1], 100, 60, True))
|
||||
|
||||
gen = self.update_picon_in_lists(dst, fav_id)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
@@ -445,7 +457,7 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
def on_add(self, item):
|
||||
""" Adds (copies) picons from an external folder to the profile picons folder. """
|
||||
dialog = get_picon_dialog(self._app_window, get_message("Add picons"), get_message("Add"))
|
||||
dialog = get_picon_dialog(self._app_window, translate("Add picons"), translate("Add"))
|
||||
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
@@ -537,15 +549,16 @@ class PiconManager(Gtk.Box):
|
||||
settings = Settings(self._settings.settings)
|
||||
settings.profile_picons_path = f"{dest_path}{SEP}"
|
||||
settings.current_profile = self._settings.current_profile
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self.run_func(lambda: upload_data(settings=settings,
|
||||
download_type=DownloadType.PICONS,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"),
|
||||
done_callback=lambda: self.show_info_message(translate("Done!"),
|
||||
Gtk.MessageType.INFO),
|
||||
files_filter=files_filter))
|
||||
|
||||
def on_download(self, app, page):
|
||||
if page is Page.PICONS:
|
||||
self._app.picons.clear()
|
||||
self.on_picons_download()
|
||||
|
||||
def on_picons_download(self, item=None, files_filter=None, path=None):
|
||||
@@ -562,7 +575,7 @@ class PiconManager(Gtk.Box):
|
||||
return
|
||||
|
||||
self.run_func(lambda: remove_picons(settings=self._settings,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"),
|
||||
done_callback=lambda: self.show_info_message(translate("Done!"),
|
||||
Gtk.MessageType.INFO),
|
||||
files_filter=files_filter))
|
||||
|
||||
@@ -602,10 +615,10 @@ class PiconManager(Gtk.Box):
|
||||
if logo_url:
|
||||
pix_data = self._picon_cz_downloader.get_logo_data(logo_url)
|
||||
if pix_data:
|
||||
pix = self.get_pixbuf(pix_data)
|
||||
pix = get_pixbuf_from_data(pix_data)
|
||||
model.set_value(itr, 0, pix if pix else TV_ICON)
|
||||
size = self._settings.tooltip_logo_size
|
||||
tooltip.set_icon(self.get_pixbuf(pix_data, size, size))
|
||||
tooltip.set_icon(get_pixbuf_from_data(pix_data, size, size))
|
||||
else:
|
||||
self.update_logo_data(itr, model, logo_url)
|
||||
tooltip.set_text(model.get_value(itr, 1))
|
||||
@@ -616,7 +629,7 @@ class PiconManager(Gtk.Box):
|
||||
def update_logo_data(self, itr, model, url):
|
||||
pix_data = self._picon_cz_downloader.get_provider_logo(url)
|
||||
if pix_data:
|
||||
pix = self.get_pixbuf(pix_data)
|
||||
pix = get_pixbuf_from_data(pix_data)
|
||||
GLib.idle_add(model.set_value, itr, 0, pix if pix else TV_ICON)
|
||||
|
||||
@run_idle
|
||||
@@ -626,7 +639,7 @@ class PiconManager(Gtk.Box):
|
||||
tooltip = f"{link} (by Chocholoušek)"
|
||||
elif self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
link = "https://www.lyngsat.com"
|
||||
tooltip = f"{get_message('Providers')} [{link}]"
|
||||
tooltip = f"{translate('Providers')} [{link}]"
|
||||
else:
|
||||
link = ""
|
||||
tooltip = ""
|
||||
@@ -699,20 +712,15 @@ class PiconManager(Gtk.Box):
|
||||
def append_providers(self, providers, model):
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
for p in providers:
|
||||
model.append(p._replace(logo=self.get_pixbuf(p.logo) if p.logo else TV_ICON))
|
||||
model.append(p._replace(logo=get_pixbuf_from_data(p.logo) if p.logo else TV_ICON))
|
||||
elif self._download_src is self.DownloadSource.PICON_CZ:
|
||||
for p in providers:
|
||||
logo_data = self._picon_cz_downloader.get_logo_data(p.ssid)
|
||||
model.append(p._replace(logo=self.get_pixbuf(logo_data) if logo_data else TV_ICON))
|
||||
model.append(p._replace(logo=get_pixbuf_from_data(logo_data) if logo_data else TV_ICON))
|
||||
|
||||
self.update_receive_button_state()
|
||||
GLib.idle_add(self._satellite_label.set_visible, True)
|
||||
|
||||
def get_pixbuf(self, img_data, w=48, h=32):
|
||||
if img_data:
|
||||
f = Gio.MemoryInputStream.new_from_data(img_data)
|
||||
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
|
||||
|
||||
def on_receive(self, item):
|
||||
if self._is_downloading:
|
||||
self._app.show_error_message("The task is already running!")
|
||||
@@ -735,14 +743,14 @@ class PiconManager(Gtk.Box):
|
||||
for prv in providers:
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
|
||||
self.show_info_message(
|
||||
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
|
||||
translate("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
|
||||
scroll_to(prv.path, self._providers_view)
|
||||
return
|
||||
|
||||
try:
|
||||
picons_path = self._current_path_label.get_text()
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Please, wait..."), Gtk.MessageType.INFO)
|
||||
providers = (Provider(*p) for p in providers)
|
||||
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
@@ -784,7 +792,7 @@ class PiconManager(Gtk.Box):
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def get_picons_for_picon_cz(self, path, providers):
|
||||
p_ids = None
|
||||
@@ -800,7 +808,7 @@ class PiconManager(Gtk.Box):
|
||||
log(f"Error: {str(e)}\n")
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def get_bouquet_picon_ids(self):
|
||||
""" Returns picon ids for selected bouquet or None. """
|
||||
@@ -828,13 +836,13 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
@run_task
|
||||
def resize(self, path):
|
||||
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Resizing..."), Gtk.MessageType.INFO)
|
||||
|
||||
try:
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
except ImportError as e:
|
||||
self.show_info_message(f"{get_message('Conversion error.')} {e}", Gtk.MessageType.ERROR)
|
||||
self.show_info_message(f"{translate('Conversion error.')} {e}", Gtk.MessageType.ERROR)
|
||||
else:
|
||||
res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)
|
||||
|
||||
@@ -843,7 +851,7 @@ class PiconManager(Gtk.Box):
|
||||
img = img.resize(res, Image.ANTIALIAS)
|
||||
img.save(img_file, "PNG", optimize=True)
|
||||
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def on_cancel(self, item=None):
|
||||
if self._is_downloading and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
|
||||
@@ -855,7 +863,7 @@ class PiconManager(Gtk.Box):
|
||||
def terminate_task(self):
|
||||
self._terminate = True
|
||||
self._is_downloading = False
|
||||
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
|
||||
self.show_info_message(translate("The task is canceled!"), Gtk.MessageType.WARNING)
|
||||
|
||||
@run_task
|
||||
def run_func(self, func, update=False):
|
||||
@@ -949,7 +957,7 @@ class PiconManager(Gtk.Box):
|
||||
self.update_picon_info(name, path, srv)
|
||||
|
||||
def update_picon_info(self, name=None, path=None, srv=None):
|
||||
self._picon_info_image.set_from_pixbuf(self.get_pixbuf_at_scale(path, 100, 60, True) if path else None)
|
||||
self._picon_info_image.set_from_pixbuf(get_pixbuf_at_scale(path, 100, 60, True) if path else None)
|
||||
self._picon_info_label.set_text(self.get_service_info(srv))
|
||||
self._current_picon_info = (name, srv.fav_id) if srv else None
|
||||
|
||||
@@ -962,8 +970,8 @@ class PiconManager(Gtk.Box):
|
||||
return self._app.get_hint_for_srv_list(srv)
|
||||
|
||||
header, ref = self._app.get_hint_header_info(srv)
|
||||
return "{} {}: {}\n{}: {} {}: {}\n{}".format(header.rstrip(), get_message("Package"), srv.package,
|
||||
get_message("System"), srv.system, get_message("Freq"), srv.freq,
|
||||
return "{} {}: {}\n{}: {} {}: {}\n{}".format(header.rstrip(), translate("Package"), srv.package,
|
||||
translate("System"), srv.system, translate("Freq"), srv.freq,
|
||||
ref)
|
||||
|
||||
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
@@ -1013,7 +1021,7 @@ class PiconManager(Gtk.Box):
|
||||
convert_to(src_path=picons_path,
|
||||
dest_path=save_path,
|
||||
s_type=SettingsType.ENIGMA_2,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
|
||||
done_callback=lambda: self.show_info_message(translate("Done!"), Gtk.MessageType.INFO))
|
||||
|
||||
@run_idle
|
||||
def update_receive_button_state(self):
|
||||
@@ -1031,12 +1039,7 @@ class PiconManager(Gtk.Box):
|
||||
show_dialog(dialog_type, self._app_window, message)
|
||||
|
||||
def get_picons_format(self):
|
||||
picon_format = SettingsType.ENIGMA_2
|
||||
|
||||
if self._neutrino_mp_radio_button.get_active():
|
||||
picon_format = SettingsType.NEUTRINO_MP
|
||||
|
||||
return picon_format
|
||||
return SettingsType.NEUTRINO_MP if self._neutrino_mp_radio_button.get_active() else SettingsType.ENIGMA_2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,241 +1,370 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkEventBox" id="event_box">
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<signal name="button-press-event" handler="on_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_realize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="GtkDrawingArea" id="playback_area">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<signal name="draw" handler="on_draw" swapped="no"/>
|
||||
<signal name="realize" handler="on_realize" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">playback</property>
|
||||
<property name="title" translatable="yes">Playback</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">load</property>
|
||||
<property name="title" translatable="yes">Load</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="playback"/>
|
||||
</style>
|
||||
</object>
|
||||
<object class="GtkToolbar" id="tool_bar">
|
||||
<object class="GtkBox" id="tool_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="prev_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-previous</property>
|
||||
<object class="GtkButton" id="prev_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Previous stream in the list</property>
|
||||
<signal name="clicked" handler="on_previous" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="play_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Play</property>
|
||||
<property name="action_name">app.on_play</property>
|
||||
<property name="stock_id">gtk-media-play</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Stop playback</property>
|
||||
<property name="action_name">app.on_stop</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-stop</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="next_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-next</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="player_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="rewind_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scale">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="restrict_to_fill_level">False</property>
|
||||
<property name="fill_level">0</property>
|
||||
<property name="draw_value">False</property>
|
||||
<property name="has_origin">False</property>
|
||||
<signal name="change-value" handler="on_rewind" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="full_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<object class="GtkImage" id="prev_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-skip-backward-symbolic</property>
|
||||
<property name="icon_size">2</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="play_button">
|
||||
<property name="visible" bind-source="stop_button" bind-property="visible" bind-flags="invert-boolean">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_play" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="play_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-playback-start-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="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Stop playback</property>
|
||||
<signal name="clicked" handler="on_stop" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="stop_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-playback-stop-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="pause_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Pause</property>
|
||||
<signal name="clicked" handler="on_pause" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="pause_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-playback-pause-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="next_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Next stream in the list</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="next_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-skip-forward-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="rewind_box">
|
||||
<property name="width-request">175</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<attributes>
|
||||
<attribute name="foreground" value="#ffffffffffff"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scale">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="restrict-to-fill-level">False</property>
|
||||
<property name="fill-level">0</property>
|
||||
<property name="draw-value">False</property>
|
||||
<property name="has-origin">False</property>
|
||||
<signal name="change-value" handler="on_rewind" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="full_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<attributes>
|
||||
<attribute name="foreground" value="#ffffffffffff"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="extras_item">
|
||||
<object class="GtkBox" id="extras_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="extras_box">
|
||||
<object class="GtkMenuButton" id="audio_menu_button">
|
||||
<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="spacing">2</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Audio Track</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="audio_menu_button">
|
||||
<object class="GtkImage" id="audio_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="audio_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Audio Track</property>
|
||||
<property name="icon_name">audio-volume-high</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">audio-card-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="video_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Aspect ratio</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="video_menu_button">
|
||||
<object class="GtkImage" id="video_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="video_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Aspect ratio</property>
|
||||
<property name="icon_name">view-restore</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">zoom-best-fit-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="subtitle_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Subtitle Track</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="subtitle_menu_button">
|
||||
<object class="GtkImage" id="subtitle_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="subtitle_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Subtitle Track</property>
|
||||
<property name="icon_name">format-text-underline</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">format-text-underline-symbolic</property>
|
||||
<property name="icon_size">2</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">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="full_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Toggle in fullscreen</property>
|
||||
<signal name="clicked" handler="on_full_screen" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="full_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">view-fullscreen-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="full_button">
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-fullscreen</property>
|
||||
<signal name="clicked" handler="on_full_screen" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Close playback</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-close</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Close playback</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="close_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">window-close-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
|
||||
""" Additional module for playback. """
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
|
||||
from gi.repository import GLib, GObject, Gio
|
||||
@@ -34,14 +35,20 @@ from gi.repository import GLib, GObject, Gio
|
||||
from app.commons import run_idle, run_with_delay
|
||||
from app.connections import HttpAPI
|
||||
from app.eparser.ecommons import BqServiceType
|
||||
from app.settings import PlayStreamsMode, IS_DARWIN, SettingsType, USE_HEADER_BAR
|
||||
from app.settings import PlayStreamsMode, PlaybackMode, IS_DARWIN, SettingsType, USE_HEADER_BAR
|
||||
from app.tools.media import Player
|
||||
from app.ui.dialogs import get_builder, get_message
|
||||
from app.ui.dialogs import get_builder, translate
|
||||
from app.ui.main_helper import get_iptv_url
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, Page
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, Page
|
||||
|
||||
|
||||
class PlayerBox(Gtk.Box):
|
||||
class PlayerBox(Gtk.Overlay):
|
||||
class Page(str, Enum):
|
||||
LOAD = "load"
|
||||
PLAYBACK = "playback"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -64,35 +71,48 @@ class PlayerBox(Gtk.Box):
|
||||
self._app.connect("page-changed", self.on_page_changed)
|
||||
self._app.connect("play-current", self.on_play_current)
|
||||
self._app.connect("play-recording", self.on_play_recording)
|
||||
|
||||
self._s_type = self._app.app_settings.setting_type
|
||||
self._fav_view = app.fav_view
|
||||
self._page = None
|
||||
self._player = None
|
||||
self._current_mrl = None
|
||||
self._full_screen = False
|
||||
self._playback_window = None
|
||||
self._audio_track_menu = None
|
||||
self._subtitle_track_menu = None
|
||||
self._play_mode = self._app.app_settings.play_streams_mode
|
||||
self._is_cursor_visible = True
|
||||
self._play_mode = PlayStreamsMode(self._app.app_settings.play_streams_mode)
|
||||
|
||||
handlers = {"on_realize": self.on_realize,
|
||||
"on_draw": self.on_draw,
|
||||
"on_mouse_motion": self.on_mouse_motion,
|
||||
"on_press": self.on_press,
|
||||
"on_play": self.on_play,
|
||||
"on_pause": self.on_pause,
|
||||
"on_stop": self.on_stop,
|
||||
"on_next": self.on_next,
|
||||
"on_previous": self.on_previous,
|
||||
"on_rewind": self.on_rewind,
|
||||
"on_full_screen": self.on_full_screen,
|
||||
"on_close": self.on_close}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
|
||||
self.set_spacing(5)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self._event_box = builder.get_object("event_box")
|
||||
self.pack_start(self._event_box, True, True, 0)
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}playback.glade", handlers)
|
||||
self._stack = builder.get_object("stack")
|
||||
self._playback_area = builder.get_object("playback_area")
|
||||
self._playback_area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
self.connect("motion-notify-event", self.on_mouse_motion)
|
||||
self.add(self._stack)
|
||||
|
||||
if not IS_DARWIN:
|
||||
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
|
||||
self.add_overlay(builder.get_object("tool_bar"))
|
||||
self._scale = builder.get_object("scale")
|
||||
self._full_time_label = builder.get_object("full_time_label")
|
||||
self._current_time_label = builder.get_object("current_time_label")
|
||||
self._rewind_box = builder.get_object("rewind_box")
|
||||
self._tool_bar = builder.get_object("tool_bar")
|
||||
self.bind_property("is_cursor_visible", self._tool_bar, "visible")
|
||||
self._stop_button = builder.get_object("stop_button")
|
||||
self._prev_button = builder.get_object("prev_button")
|
||||
self._next_button = builder.get_object("next_button")
|
||||
self._audio_menu_button = builder.get_object("audio_menu_button")
|
||||
@@ -103,21 +123,28 @@ class PlayerBox(Gtk.Box):
|
||||
|
||||
self.connect("delete-event", self.on_delete)
|
||||
self.connect("show", self.set_player_area_size)
|
||||
self.connect("unrealize", self.on_unrealize)
|
||||
|
||||
@property
|
||||
def playback_widget(self):
|
||||
return self._playback_area
|
||||
|
||||
@GObject.Property(type=bool, default=True)
|
||||
def is_cursor_visible(self):
|
||||
return self._is_cursor_visible
|
||||
|
||||
@is_cursor_visible.setter
|
||||
def is_cursor_hidden(self, value):
|
||||
self._is_cursor_visible = value
|
||||
|
||||
def on_fav_clicked(self, app, mode):
|
||||
if mode is not FavClickMode.STREAM and not self._app.http_api:
|
||||
if mode is not PlaybackMode.STREAM and not self._app.http_api:
|
||||
return
|
||||
|
||||
if len(self._fav_view.get_model()) == 0:
|
||||
return
|
||||
|
||||
self._fav_view.set_sensitive(False)
|
||||
if mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
elif mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
self.start_playback(mode)
|
||||
|
||||
def on_srv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
@@ -131,18 +158,7 @@ class PlayerBox(Gtk.Box):
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref_data(srv)
|
||||
s_type = self._app.app_settings.setting_type
|
||||
error_msg = "No connection to the receiver!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
def zap(rq):
|
||||
self.on_watch() if rq and rq.get("e2state", False) else self.on_error(None, error_msg)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zap)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
def zap(rq):
|
||||
self.on_watch() if rq and rq.get("data", None) == "ok" else self.on_error(None, error_msg)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zap)
|
||||
self.zap(ref, self.play_current)
|
||||
|
||||
def on_iptv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
@@ -156,29 +172,34 @@ class PlayerBox(Gtk.Box):
|
||||
self.play(url, row[Column.IPTV_SERVICE]) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_current(self, app, url):
|
||||
self.on_watch()
|
||||
self.play_current()
|
||||
|
||||
def on_play_recording(self, app, url):
|
||||
self.play(url)
|
||||
|
||||
def on_page_changed(self, app, page):
|
||||
self.on_close()
|
||||
self.set_visible(False)
|
||||
self._page = page
|
||||
if self._player and self.is_visible():
|
||||
self.update_buttons() if not IS_DARWIN else None
|
||||
self.on_close()
|
||||
self.set_visible(False)
|
||||
|
||||
def on_realize(self, box):
|
||||
def on_realize(self, area):
|
||||
if not self._player:
|
||||
settings = self._app.app_settings
|
||||
self._stack.set_visible_child_name(self.Page.LOAD)
|
||||
try:
|
||||
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
|
||||
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self)
|
||||
except (ImportError, NameError) as e:
|
||||
self._app.show_error_message(str(e))
|
||||
return True
|
||||
else:
|
||||
self.init_playback_elements()
|
||||
self.emit("play", self._current_mrl)
|
||||
finally:
|
||||
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.set_player_area_size(box)
|
||||
self.on_play()
|
||||
|
||||
def on_unrealize(self, box):
|
||||
if self._player:
|
||||
self._player.release()
|
||||
|
||||
def init_playback_elements(self):
|
||||
self._player.connect("error", self.on_error)
|
||||
@@ -187,7 +208,7 @@ class PlayerBox(Gtk.Box):
|
||||
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
|
||||
self._app.app_window.connect("key-press-event", self.on_key_press)
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}app_menu.ui")
|
||||
self._audio_track_menu = builder.get_object("audio_track_menu")
|
||||
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
|
||||
audio_menu = builder.get_object("audio_menu")
|
||||
@@ -221,18 +242,28 @@ class PlayerBox(Gtk.Box):
|
||||
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
|
||||
self._app.add_action(subtitle_track_action)
|
||||
|
||||
@run_idle
|
||||
def on_play(self, action=None, value=None):
|
||||
self.emit("play", None)
|
||||
self._stack.set_visible_child_name(self.Page.LOAD)
|
||||
self.emit("play", self._current_mrl)
|
||||
|
||||
def on_pause(self, action=None, value=None):
|
||||
self.emit("pause", None)
|
||||
|
||||
def on_stop(self, action=None, value=None):
|
||||
self._stop_button.set_visible(False) if not IS_DARWIN else None
|
||||
self.emit("stop", None)
|
||||
|
||||
def on_next(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
|
||||
self.set_player_action()
|
||||
self.switch_service(1)
|
||||
|
||||
def on_previous(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
|
||||
self.switch_service(-1)
|
||||
|
||||
def switch_service(self, count):
|
||||
self._fav_view.grab_focus()
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, count):
|
||||
self.update_buttons() if not IS_DARWIN else None
|
||||
self.set_player_action()
|
||||
|
||||
def on_rewind(self, scale, scroll_type, value):
|
||||
@@ -241,11 +272,8 @@ class PlayerBox(Gtk.Box):
|
||||
def on_full_screen(self, item=None):
|
||||
self._full_screen = not self._full_screen
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self.emit("playback-full-screen", not self._full_screen)
|
||||
elif self._playback_window:
|
||||
if not IS_DARWIN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
|
||||
|
||||
def on_close(self, action=None, value=None):
|
||||
@@ -253,6 +281,9 @@ class PlayerBox(Gtk.Box):
|
||||
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
|
||||
self._playback_window.hide()
|
||||
|
||||
if self._full_screen:
|
||||
GLib.idle_add(self.on_full_screen)
|
||||
|
||||
self.on_stop()
|
||||
self.hide()
|
||||
self.emit("playback-close", None)
|
||||
@@ -306,21 +337,18 @@ class PlayerBox(Gtk.Box):
|
||||
|
||||
@run_with_delay(1)
|
||||
def set_player_action(self):
|
||||
click_mode = self._app.app_settings.fav_click_mode
|
||||
self._fav_view.set_sensitive(False)
|
||||
if click_mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
elif click_mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
self.start_playback(PlaybackMode(self._app.app_settings.fav_click_mode))
|
||||
|
||||
def update_buttons(self):
|
||||
if self._player:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
current_index = path[0]
|
||||
self._player_prev_button.set_sensitive(current_index != 0)
|
||||
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
|
||||
if path:
|
||||
current_index = path[0]
|
||||
self._prev_button.set_sensitive(current_index != 0)
|
||||
self._next_button.set_sensitive(len(self._fav_view.get_model()) != current_index + 1)
|
||||
|
||||
self._prev_button.set_visible(self._page is Page.SERVICES)
|
||||
self._next_button.set_visible(self._page is Page.SERVICES)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def on_duration_changed(self, duration):
|
||||
@@ -345,6 +373,7 @@ class PlayerBox(Gtk.Box):
|
||||
def set_player_area_size(self, widget):
|
||||
w, h = self._app.app_window.get_size()
|
||||
widget.set_size_request(w * 0.6, -1)
|
||||
self._stack.set_visible_child_name(self.Page.PLAYBACK)
|
||||
|
||||
@run_idle
|
||||
def show_playback_window(self, title=None):
|
||||
@@ -363,7 +392,7 @@ class PlayerBox(Gtk.Box):
|
||||
|
||||
self._playback_window.connect("delete-event", self.on_close)
|
||||
self._playback_window.connect("key-press-event", self.on_key_press)
|
||||
self._playback_window.bind_property("visible", self._event_box, "visible")
|
||||
self._playback_window.bind_property("visible", self._stack, "visible")
|
||||
|
||||
if not IS_DARWIN:
|
||||
self._prev_button.set_visible(False)
|
||||
@@ -382,8 +411,21 @@ class PlayerBox(Gtk.Box):
|
||||
if path:
|
||||
return f"DemonEditor [{self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]}]"
|
||||
else:
|
||||
return f"DemonEditor [{get_message('Recordings')}]"
|
||||
return f"DemonEditor [{get_message('Playback')}]"
|
||||
return f"DemonEditor [{translate('Recordings')}]"
|
||||
return f"DemonEditor [{translate('Playback')}]"
|
||||
|
||||
def start_playback(self, mode):
|
||||
self.on_stop() if mode is not PlaybackMode.ZAP else None
|
||||
self._stack.set_visible_child_name(self.Page.LOAD)
|
||||
|
||||
if mode is PlaybackMode.PLAY:
|
||||
self.on_play_service()
|
||||
elif mode is PlaybackMode.ZAP:
|
||||
self.on_zap()
|
||||
elif mode is PlaybackMode.ZAP_PLAY:
|
||||
self.on_zap(self.play_current)
|
||||
elif mode is PlaybackMode.STREAM:
|
||||
self.on_play_stream()
|
||||
|
||||
def on_play_stream(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
@@ -397,34 +439,60 @@ class PlayerBox(Gtk.Box):
|
||||
self.play(url) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_service(self, item=None):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._app.http_api:
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref(path)
|
||||
""" Playback without switching channel on the Box [returns current reference]"""
|
||||
ref, path = self.get_ref()
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._player and self._player.is_playing():
|
||||
self.emit("stop", None)
|
||||
|
||||
s_type = self._app.app_settings.setting_type
|
||||
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
|
||||
self._app.http_api.send(req, ref, self.watch)
|
||||
return ref
|
||||
|
||||
def on_watch(self, item=None):
|
||||
""" Switch to the channel and watch in the player. """
|
||||
s_type = self._app.app_settings.setting_type
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
|
||||
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
|
||||
self.watch))
|
||||
def on_zap(self, callback=None):
|
||||
""" Switch(zap) the channel. """
|
||||
ref, path = self.get_ref()
|
||||
if not ref:
|
||||
return
|
||||
|
||||
# IPTV type checking
|
||||
row = self._fav_view.get_model()[path][:]
|
||||
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name and callback:
|
||||
callback = self.play(get_iptv_url(row, self._s_type))
|
||||
|
||||
self.zap(ref, callback)
|
||||
|
||||
def get_ref(self):
|
||||
""" Returns reference and currently selected path as a tuple. """
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._app.http_api:
|
||||
return
|
||||
return self._app.get_service_ref(path), path
|
||||
|
||||
def zap(self, ref, callback=None):
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
def zp(rq):
|
||||
if rq and rq.get("e2state", False):
|
||||
if callback:
|
||||
callback()
|
||||
else:
|
||||
self._app.show_error_message("No connection to the receiver!")
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zp)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
def zp(rq):
|
||||
if rq and rq.get("data", None) == "ok":
|
||||
if callback:
|
||||
callback()
|
||||
else:
|
||||
self._app.show_error_message("No connection to the receiver!")
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zp)
|
||||
else:
|
||||
self._app.show_error_message("This type of settings is not supported!")
|
||||
|
||||
def watch(self, data):
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
|
||||
self.play(self._app.get_url_from_m3u(data))
|
||||
|
||||
def play(self, url, title=None):
|
||||
if self._play_mode is PlayStreamsMode.M3U:
|
||||
@@ -440,21 +508,49 @@ class PlayerBox(Gtk.Box):
|
||||
elif self._play_mode is PlayStreamsMode.WINDOW:
|
||||
self.show_playback_window(title)
|
||||
|
||||
self._current_mrl = url
|
||||
if self._player:
|
||||
self.emit("play", url)
|
||||
else:
|
||||
self._current_mrl = url
|
||||
|
||||
@run_idle
|
||||
def play_current(self):
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
|
||||
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
|
||||
self.watch))
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_played(self, player, duration):
|
||||
self._fav_view.set_sensitive(True)
|
||||
self._stack.set_visible_child_name(self.Page.PLAYBACK)
|
||||
if not IS_DARWIN:
|
||||
self._stop_button.set_visible(True)
|
||||
self.on_duration_changed(duration)
|
||||
|
||||
@run_idle
|
||||
def on_error(self, player, msg):
|
||||
self._app.show_error_message(msg)
|
||||
self._fav_view.set_sensitive(True)
|
||||
self._stack.set_visible_child_name(self.Page.PLAYBACK)
|
||||
|
||||
def on_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
def on_mouse_motion(self, widget, event):
|
||||
display = widget.get_display()
|
||||
window = widget.get_window()
|
||||
cursor = Gdk.Cursor.new_from_name(display, "default")
|
||||
window.set_cursor(cursor)
|
||||
|
||||
self.hide_mouse_cursor(window, display)
|
||||
self.is_cursor_visible = True
|
||||
|
||||
@run_with_delay(3)
|
||||
def hide_mouse_cursor(self, window, display):
|
||||
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
|
||||
window.set_cursor(cursor)
|
||||
self.is_cursor_visible = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -36,30 +36,30 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="play_menu_item">
|
||||
<property name="label">gtk-media-play</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">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_play" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">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_recording_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
@@ -96,7 +96,7 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkTreeModelFilter" id="recordings_filter_model">
|
||||
<property name="child_model">recordings_model</property>
|
||||
<property name="child-model">recordings_model</property>
|
||||
</object>
|
||||
<object class="GtkTreeModelSort" id="recordings_sort_model">
|
||||
<property name="model">recordings_filter_model</property>
|
||||
@@ -105,121 +105,55 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkBox" id="recordings_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="recordings_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="wide-handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="recordings_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>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_main_box">
|
||||
<object class="GtkViewport" id="recordings_viewport">
|
||||
<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="orientation">vertical</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_header_box">
|
||||
<object class="GtkBox" id="recordings_main_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="spacing">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="recordings_filter_button">
|
||||
<object class="GtkBox" id="recordings_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_recordings_filter_toggled" swapped="no"/>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_filter_button_image">
|
||||
<object class="GtkToggleButton" id="recordings_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace-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="recordings_remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="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">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_filter_entry">
|
||||
<property name="can_focus">True</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>
|
||||
<property name="visible" bind-source="recordings_filter_button" bind-property="active"/>
|
||||
<signal name="search-changed" handler="on_recordings_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_search_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_search_entry">
|
||||
<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="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_recordings_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_filter_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -228,227 +162,27 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_down_button">
|
||||
<object class="GtkButton" id="recordings_remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">False</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="GtkArrow" id="recordings_down_arrow">
|
||||
<object class="GtkImage" id="remove_recording_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">down</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">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="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_sort_model</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">both</property>
|
||||
<property name="tooltip_column">6</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_recordings_key_press" swapped="no"/>
|
||||
<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="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="rec_log_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ypad">2</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_service_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_time_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">180</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Time</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_time_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_len_column">
|
||||
<property name="min_width">100</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_len_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_file_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</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>
|
||||
<property name="sort_column_id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_file_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_desc_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">6</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">6</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_status_box">
|
||||
<property name="height_request">26</property>
|
||||
<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="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -457,10 +191,96 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="recordings_count_label">
|
||||
<object class="GtkBox" id="recordings_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label">0</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_filter_entry">
|
||||
<property name="visible" bind-source="recordings_filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">True</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_recordings_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_search_box">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_search_entry">
|
||||
<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>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_down_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -469,22 +289,210 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<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_sort_model</property>
|
||||
<property name="fixed-height-mode">True</property>
|
||||
<property name="rubber-banding">True</property>
|
||||
<property name="enable-grid-lines">both</property>
|
||||
<property name="tooltip-column">6</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_recordings_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_service_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">150</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort-column-id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="rec_log_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ypad">2</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_service_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort-column-id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_time_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">180</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Time</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort-column-id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_time_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_len_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort-column-id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_len_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_file_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</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>
|
||||
<property name="sort-column-id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_file_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_desc_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort-column-id">6</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">6</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</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">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="recordings_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">Recordings</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -496,72 +504,81 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkFrame" id="recordings_paths_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>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
|
||||
<property name="width_request">250</property>
|
||||
<object class="GtkViewport" id="paths_viewport">
|
||||
<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>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="recordings_paths_view">
|
||||
<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="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>
|
||||
<signal name="button-press-event" handler="on_path_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_path_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="rec_paths_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="min-content-height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_paths_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Paths</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="clickable">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<object class="GtkTreeView" id="recordings_paths_view">
|
||||
<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>
|
||||
<signal name="button-press-event" handler="on_path_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_path_activated" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<object class="GtkTreeViewColumn" id="rec_paths_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Paths</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="clickable">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort-column-id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="recordings_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">Paths</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -46,15 +46,16 @@ class RecordingsTool(Gtk.Box):
|
||||
ROOT = ".."
|
||||
DEFAULT_PATH = "/hdd"
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("layout-changed", self.on_layout_changed)
|
||||
self._app.connect("data-receive", self.on_data_receive)
|
||||
self._app.connect("profile-changed", self.init)
|
||||
self._app.connect("filter-toggled", self.on_filter_toggled)
|
||||
|
||||
self._settings = settings
|
||||
self._settings = app.app_settings
|
||||
self._ftp = None
|
||||
self._logos = {}
|
||||
# Icon.
|
||||
@@ -82,6 +83,7 @@ class RecordingsTool(Gtk.Box):
|
||||
self._filter_model = builder.get_object("recordings_filter_model")
|
||||
self._filter_model.set_visible_func(self.recordings_filter_function)
|
||||
self._filter_entry = builder.get_object("recordings_filter_entry")
|
||||
self._recordings_filter_button = builder.get_object("recordings_filter_button")
|
||||
self._recordings_count_label = builder.get_object("recordings_count_label")
|
||||
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
|
||||
self._rec_view.get_model().set_sort_func(3, self.time_sort_func, 3)
|
||||
@@ -92,7 +94,7 @@ class RecordingsTool(Gtk.Box):
|
||||
renderer.set_fixed_size(size, size * 0.65)
|
||||
srv_column.set_cell_data_func(renderer, self.logo_data_func)
|
||||
|
||||
if settings.alternate_layout:
|
||||
if self._settings.alternate_layout:
|
||||
self.on_layout_changed(app, True)
|
||||
|
||||
self.init()
|
||||
@@ -293,9 +295,12 @@ class RecordingsTool(Gtk.Box):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr, 1, 2, 3, 5, 6) if s and txt in s.upper()), False)
|
||||
|
||||
def on_filter_toggled(self, app, value):
|
||||
if self._app.page is Page.RECORDINGS:
|
||||
self._recordings_filter_button.set_active(not self._recordings_filter_button.get_active())
|
||||
|
||||
def on_recordings_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
self._filter_entry.grab_focus() if button.get_active() else self._filter_entry.set_text("")
|
||||
|
||||
def on_recordings_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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
|
||||
@@ -97,7 +97,7 @@ class ServiceDetailsDialog:
|
||||
self._DIGIT_PATTERN = re.compile("\\D")
|
||||
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
|
||||
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
|
||||
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{1,4})(,c:[0-9]{2}[0-9a-fA-F]{1,4})*?")
|
||||
# Buttons
|
||||
self._apply_button = builder.get_object("apply_button")
|
||||
self._create_button = builder.get_object("create_button")
|
||||
@@ -140,7 +140,7 @@ class ServiceDetailsDialog:
|
||||
self._srv_type_entry = self._non_empty_elements.get("srv_type_entry")
|
||||
self._service_type_combo_box = builder.get_object("service_type_combo_box")
|
||||
self._cas_entry = builder.get_object("cas_entry")
|
||||
self._reference_entry = builder.get_object("reference_entry")
|
||||
self._reference_label = builder.get_object("reference_label")
|
||||
self._keep_check_button = builder.get_object("keep_check_button")
|
||||
self._hide_check_button = builder.get_object("hide_check_button")
|
||||
self._use_pids_check_button = builder.get_object("use_pids_check_button")
|
||||
@@ -159,7 +159,6 @@ class ServiceDetailsDialog:
|
||||
self._pilot_combo_box = builder.get_object("pilot_combo_box")
|
||||
self._pls_mode_combo_box = builder.get_object("pls_mode_combo_box")
|
||||
self._tr_edit_switch = builder.get_object("tr_edit_switch")
|
||||
self._tr_extra_expander = builder.get_object("tr_extra_expander")
|
||||
|
||||
self._DVB_S2_ELEMENTS = (self._mod_combo_box, self._rolloff_combo_box, self._pilot_combo_box,
|
||||
self._pls_mode_combo_box, self._pls_code_entry, self._stream_id_entry)
|
||||
@@ -186,7 +185,6 @@ class ServiceDetailsDialog:
|
||||
elem.set_text(" ")
|
||||
elem.set_text("")
|
||||
self._new_check_button.set_active(True)
|
||||
self._tr_extra_expander.activate()
|
||||
self._service_type_combo_box.set_active(0)
|
||||
self._pol_combo_box.set_active(0)
|
||||
self._fec_combo_box.set_active(0)
|
||||
@@ -366,8 +364,7 @@ class ServiceDetailsDialog:
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(7)
|
||||
tr_grid.set_margin_bottom(5)
|
||||
self._builder.get_object("tr_extra_expander").set_visible(False)
|
||||
self._builder.get_object("srv_separator").set_visible(False)
|
||||
self._builder.get_object("extra_transponder_grid").set_visible(False)
|
||||
self._package_entry.set_sensitive(False)
|
||||
|
||||
# ***************** Init Sat positions *********************#
|
||||
@@ -527,7 +524,7 @@ class ServiceDetailsDialog:
|
||||
package=self._package_entry.get_text(),
|
||||
service_type=SERVICE_TYPE.get(self._srv_type_entry.get_text(), SERVICE_TYPE["3"]),
|
||||
picon=self._old_service.picon,
|
||||
picon_id=self._reference_entry.get_text().replace(":", "_") + ".png",
|
||||
picon_id=self._reference_label.get_text().replace(":", "_") + ".png",
|
||||
ssid="{:04x}".format(int(self._sid_entry.get_text())),
|
||||
freq=freq,
|
||||
rate=rate,
|
||||
@@ -798,9 +795,9 @@ class ServiceDetailsDialog:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
on_id = int(self._namespace_entry.get_text())
|
||||
ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
|
||||
self._reference_entry.set_text(ref)
|
||||
self._reference_label.set_text(ref)
|
||||
else:
|
||||
self._reference_entry.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
|
||||
self._reference_label.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
|
||||
|
||||
def update_ui_for_terrestrial(self):
|
||||
tr_grid = self.get_transponder_grid_for_non_satellite()
|
||||
@@ -891,7 +888,6 @@ class ServiceDetailsDialog:
|
||||
# FEC
|
||||
fec_model.append(("None",))
|
||||
# Extra
|
||||
tr_box.remove(self._tr_extra_expander)
|
||||
tr_grid.set_margin_bottom(5)
|
||||
self._freq_entry.set_width_chars(10)
|
||||
self._freq_entry.set_max_width_chars(10)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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,10 +32,10 @@ from collections import Counter
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, HeaderBar
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, PlaybackMode, IS_LINUX, SEP, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, translate, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf, show_info_bar_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, DEFAULT_ICON, APP_FONT, HeaderBar
|
||||
|
||||
|
||||
class SettingsDialog:
|
||||
@@ -53,7 +53,6 @@ class SettingsDialog:
|
||||
"on_force_bq_name": self.on_force_bq_name,
|
||||
"on_http_mode_switch": self.on_http_mode_switch,
|
||||
"on_experimental_switch": self.on_experimental_switch,
|
||||
"on_yt_dl_switch": self.on_yt_dl_switch,
|
||||
"on_default_path_mode_switch": self.on_default_path_mode_switch,
|
||||
"on_profile_add": self.on_profile_add,
|
||||
"on_profile_edit": self.on_profile_edit,
|
||||
@@ -129,13 +128,13 @@ class SettingsDialog:
|
||||
self._backup_path_field = builder.get_object("backup_path_field")
|
||||
self._recordings_path_field = builder.get_object("recordings_path_field")
|
||||
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
|
||||
self._default_data_paths_switch.bind_property("active", builder.get_object("picons_path_box"), "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", builder.get_object("backup_path_box"), "sensitive", 4)
|
||||
self._use_common_picon_path_switch = builder.get_object("use_common_picon_path_switch")
|
||||
# Info bar.
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._test_spinner = builder.get_object("test_spinner")
|
||||
# Settings type.
|
||||
self._settings_type_box = builder.get_object("settings_type_combo_box")
|
||||
self._enigma_radio_button = builder.get_object("enigma_radio_button")
|
||||
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
|
||||
# Streaming.
|
||||
@@ -187,18 +186,10 @@ class SettingsDialog:
|
||||
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
|
||||
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
|
||||
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
|
||||
# Enigma2 only.
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("allow_double_click_box"), "sensitive")
|
||||
# Profiles.
|
||||
self._profile_view = builder.get_object("profile_tree_view")
|
||||
self._profile_add_button = builder.get_object("profile_add_button")
|
||||
self._profile_remove_button = builder.get_object("profile_remove_button")
|
||||
# Network.
|
||||
# Separated due to a bug with response (presumably in the builder) in ubuntu 18.04 and derivatives.
|
||||
builder.get_object("network_settings_frame").add(builder.get_object("network_grid"))
|
||||
# Style.
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
|
||||
@@ -223,29 +214,27 @@ class SettingsDialog:
|
||||
|
||||
if not IS_LINUX:
|
||||
# Themes.
|
||||
builder.get_object("style_frame").set_visible(IS_WIN)
|
||||
builder.get_object("themes_support_frame").set_visible(True)
|
||||
self._layout_switch = builder.get_object("layout_switch")
|
||||
self._layout_switch.set_active(self._ext_settings.alternate_layout)
|
||||
self._theme_frame = builder.get_object("theme_frame")
|
||||
self._theme_frame.set_visible(True)
|
||||
builder.get_object("dark_mode_box").set_visible(IS_WIN)
|
||||
builder.get_object("style_box_view").set_visible(True)
|
||||
self._theme_view = builder.get_object("theme_view")
|
||||
self._theme_view.set_visible(True)
|
||||
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
|
||||
self._theme_combo_box = builder.get_object("theme_combo_box")
|
||||
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
|
||||
self._dark_mode_switch = builder.get_object("dark_mode_switch")
|
||||
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
|
||||
self._themes_support_switch = builder.get_object("themes_support_switch")
|
||||
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
|
||||
self._themes_support_switch.bind_property("active", self._theme_view, "sensitive")
|
||||
self.init_themes()
|
||||
|
||||
def init_ui_elements(self):
|
||||
is_enigma_profile = self._s_type is SettingsType.ENIGMA_2
|
||||
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_picon_paths()
|
||||
self.update_title()
|
||||
self._dialog.set_title(f"{translate('Options')} [{self._settings_type_box.get_active_text()}]")
|
||||
self._lang_combo_box.set_active_id(self._ext_settings.language)
|
||||
self.on_info_bar_close() if is_enigma_profile else self.show_info_message(
|
||||
is_enigma = self._s_type is SettingsType.ENIGMA_2
|
||||
self.on_info_bar_close() if is_enigma else self.show_info_message(
|
||||
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
|
||||
self._epg_dat_box.set_sensitive(is_enigma)
|
||||
|
||||
def init_profiles(self):
|
||||
p_def = self._settings.default_profile
|
||||
@@ -261,13 +250,6 @@ class SettingsDialog:
|
||||
def init_element_style(self, elem, screen, provider):
|
||||
elem.get_style_context().add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def update_title(self):
|
||||
title = "{} [{}]"
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
|
||||
|
||||
def update_picon_paths(self):
|
||||
model = self._picons_paths_box.get_model()
|
||||
model.clear()
|
||||
@@ -292,7 +274,7 @@ class SettingsDialog:
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
def on_settings_type_changed(self, item):
|
||||
s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
|
||||
s_type = SettingsType(int(self._settings_type_box.get_active_id()))
|
||||
if s_type is not self._s_type:
|
||||
self._settings.setting_type = s_type
|
||||
self._s_type = s_type
|
||||
@@ -334,6 +316,7 @@ class SettingsDialog:
|
||||
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
|
||||
self._services_hints_switch.set_active(self._settings.show_srv_hints)
|
||||
self._default_data_paths_switch.set_active(self._settings.profile_folder_is_default)
|
||||
self._use_common_picon_path_switch.set_active(self._settings.use_common_picon_path)
|
||||
self._transcoding_switch.set_active(self._settings.activate_transcoding)
|
||||
self._presets_combo_box.set_active_id(self._settings.active_preset)
|
||||
self.on_transcoding_preset_changed(self._presets_combo_box)
|
||||
@@ -363,17 +346,14 @@ class SettingsDialog:
|
||||
self._new_color_button.set_rgba(new_rgb)
|
||||
self._extra_color_button.set_rgba(extra_rgb)
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._enigma_radio_button.activate()
|
||||
else:
|
||||
self._neutrino_radio_button.activate()
|
||||
self._settings_type_box.set_active_id(str(self._s_type.value))
|
||||
|
||||
def on_apply_profile_settings(self, item=None):
|
||||
if not self.is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
|
||||
self._s_type = SettingsType(int(self._settings_type_box.get_active_id()))
|
||||
self._settings.setting_type = self._s_type
|
||||
self._settings.host = self._host_field.get_text()
|
||||
self._settings.hosts = [h[1] for h in self._hosts_box.get_model()]
|
||||
@@ -406,6 +386,7 @@ class SettingsDialog:
|
||||
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
|
||||
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
|
||||
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
|
||||
self._ext_settings.use_common_picon_path = self._use_common_picon_path_switch.get_active()
|
||||
self._ext_settings.default_data_path = self._data_path_field.get_text()
|
||||
self._ext_settings.default_backup_path = self._backup_path_field.get_text()
|
||||
self._ext_settings.default_picon_path = self._picons_path_field.get_text()
|
||||
@@ -419,7 +400,6 @@ class SettingsDialog:
|
||||
|
||||
if not IS_LINUX:
|
||||
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
|
||||
self._ext_settings.alternate_layout = self._layout_switch.get_active()
|
||||
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
|
||||
self._ext_settings.theme = self._theme_combo_box.get_active_id()
|
||||
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
|
||||
@@ -495,10 +475,7 @@ class SettingsDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(False)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(get_message(text))
|
||||
self._info_bar.set_visible(True)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
@run_idle
|
||||
def show_spinner(self, show):
|
||||
@@ -532,11 +509,8 @@ class SettingsDialog:
|
||||
else:
|
||||
self.on_info_bar_close()
|
||||
|
||||
def on_yt_dl_switch(self, switch, state):
|
||||
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_default_path_mode_switch(self, switch, state):
|
||||
self._settings.profile_folder_is_default = state
|
||||
self._use_common_picon_path_switch.set_active(False) if state else None
|
||||
|
||||
def on_profile_add(self, item):
|
||||
model = self._profile_view.get_model()
|
||||
@@ -661,7 +635,7 @@ class SettingsDialog:
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_remove_picon_path(self, button):
|
||||
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{get_message('Are you sure?')}"
|
||||
msg = f"{translate('This may change the settings of other profiles!')}\n\n\t\t{translate('Are you sure?')}"
|
||||
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
@@ -694,17 +668,17 @@ class SettingsDialog:
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
return
|
||||
|
||||
mode = FavClickMode(int(self._double_click_combo_box.get_active_id()))
|
||||
if mode is FavClickMode.PLAY:
|
||||
mode = PlaybackMode(int(self._double_click_combo_box.get_active_id()))
|
||||
if mode is PlaybackMode.PLAY:
|
||||
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
|
||||
elif mode is FavClickMode.STREAM:
|
||||
elif mode is PlaybackMode.STREAM:
|
||||
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING)
|
||||
elif mode is FavClickMode.DISABLED:
|
||||
elif mode is PlaybackMode.DISABLED:
|
||||
self._allow_main_list_playback_switch.set_active(False)
|
||||
else:
|
||||
self.on_info_bar_close()
|
||||
|
||||
self._allow_main_list_playback_switch.set_sensitive(mode is not FavClickMode.DISABLED)
|
||||
self._allow_main_list_playback_switch.set_sensitive(mode is not PlaybackMode.DISABLED)
|
||||
|
||||
def on_play_mode_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
@@ -799,7 +773,7 @@ class SettingsDialog:
|
||||
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
self._theme_frame.set_sensitive(False)
|
||||
self._theme_view.set_sensitive(False)
|
||||
self.unpack_theme(response, path, button)
|
||||
|
||||
@run_task
|
||||
@@ -829,7 +803,7 @@ class SettingsDialog:
|
||||
button.append(theme, theme)
|
||||
button.set_active_id(theme)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._theme_frame.set_sensitive(True)
|
||||
self._theme_view.set_sensitive(True)
|
||||
|
||||
@run_idle
|
||||
def remove_theme(self, button, path):
|
||||
|
||||
@@ -104,3 +104,8 @@ paned.vertical > separator {
|
||||
padding-right: 5px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.playback {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
from app.ui.dialogs import get_message
|
||||
from app.ui.dialogs import translate
|
||||
from .uicommons import Gtk, GLib
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class BGTaskWidget(Gtk.Box):
|
||||
super().__init__(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.CENTER)
|
||||
self._app = app
|
||||
|
||||
self._label = Gtk.Label(get_message(text))
|
||||
self._label = Gtk.Label(translate(text))
|
||||
self.pack_start(self._label, False, False, 0)
|
||||
|
||||
self._spinner = Gtk.Spinner(active=True)
|
||||
@@ -46,7 +46,7 @@ class BGTaskWidget(Gtk.Box):
|
||||
close_button = Gtk.Button.new_from_icon_name("window-close", Gtk.IconSize.MENU)
|
||||
close_button.set_relief(Gtk.ReliefStyle.NONE)
|
||||
close_button.set_valign(Gtk.Align.CENTER)
|
||||
close_button.set_tooltip_text(get_message("Cancel"))
|
||||
close_button.set_tooltip_text(translate("Cancel"))
|
||||
close_button.set_name("task-button")
|
||||
close_button.connect("clicked", lambda b: self._app.emit("task-cancel", self))
|
||||
self.pack_start(close_button, False, False, 0)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -41,43 +41,98 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkTextBuffer" id="text_buffer">
|
||||
<property name="tag_table">tag_table</property>
|
||||
<property name="tag-table">tag_table</property>
|
||||
</object>
|
||||
<object class="GtkFrame" id="telnet_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>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="telnet_main_box">
|
||||
<object class="GtkViewport" id="viewport">
|
||||
<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="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="commands_entry">
|
||||
<object class="GtkBox" id="telnet_main_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">2</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<object class="GtkBox" id="commands_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
<property name="can-focus">True</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">True</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="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -87,90 +142,49 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="name">textview-large</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="wrap-mode">char</property>
|
||||
<property name="left-margin">5</property>
|
||||
<property name="right-margin">5</property>
|
||||
<property name="buffer">text_buffer</property>
|
||||
<property name="overwrite">True</property>
|
||||
<property name="input-hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
<property name="monospace">True</property>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</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>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="name">textview-large</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wrap_mode">char</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="buffer">text_buffer</property>
|
||||
<property name="overwrite">True</property>
|
||||
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
<property name="monospace">True</property>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">Telnet</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
2064
app/ui/timers.glade
2064
app/ui/timers.glade
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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,7 +33,7 @@ from urllib.parse import quote
|
||||
|
||||
from app.settings import USE_HEADER_BAR
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .dialogs import get_builder, get_message, show_dialog, DialogType
|
||||
from .dialogs import get_builder, translate, show_dialog, DialogType
|
||||
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, MOD_MASK
|
||||
from ..commons import run_idle, log
|
||||
from ..connections import HttpAPI
|
||||
@@ -71,7 +71,7 @@ class TimerTool(Gtk.Box):
|
||||
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
|
||||
"min_begins_adjustment"))
|
||||
|
||||
self.set_title(get_message("Timer"))
|
||||
self.set_title(translate("Timer"))
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
@@ -111,7 +111,7 @@ class TimerTool(Gtk.Box):
|
||||
self._timer_desc_entry.drag_dest_unset()
|
||||
self._timer_service_entry.drag_dest_unset()
|
||||
|
||||
self.add_buttons(get_message("Cancel"), Gtk.ResponseType.CANCEL, get_message("Save"), Gtk.ResponseType.OK)
|
||||
self.add_buttons(translate("Cancel"), Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK)
|
||||
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
|
||||
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
@@ -236,15 +236,15 @@ class TimerTool(Gtk.Box):
|
||||
TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons)
|
||||
|
||||
def set_timer_from_event_data(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", ""))
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", ""))
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", None) or "")
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", None) or "")
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", None) or "")
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", None) or "")
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", None) or "")
|
||||
self._timer_action_combo_box.set_active_id("1")
|
||||
self._timer_after_combo_box.set_active_id("3")
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0"))
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0")))
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0") or "0")
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0") or "0"))
|
||||
|
||||
def set_time_data(self, start_time, end_time):
|
||||
""" Sets values for time widgets. """
|
||||
@@ -292,7 +292,7 @@ class TimerTool(Gtk.Box):
|
||||
"on_timer_remove": self.on_timer_remove,
|
||||
"on_model_changed": self.on_model_changed,
|
||||
"on_timers_press": self.on_timers_press,
|
||||
"on_timers_key_press": self.on_timers_key_press,
|
||||
"on_timers_key_release": self.on_timers_key_release,
|
||||
"on_timer_cursor_changed": self.on_timer_cursor_changed,
|
||||
"on_timers_drag_data_received": self.on_timers_drag_data_received}
|
||||
|
||||
@@ -465,7 +465,7 @@ class TimerTool(Gtk.Box):
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_timers_key_press(self, view, event):
|
||||
def on_timers_key_release(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
@@ -475,10 +475,10 @@ class TimerTool(Gtk.Box):
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_timer_remove()
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_timer_add()
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_timer_edit()
|
||||
elif ctrl and key is KeyboardKey.INSERT:
|
||||
self.on_timer_add()
|
||||
|
||||
def on_timer_cursor_changed(self, view):
|
||||
path, column = view.get_cursor()
|
||||
@@ -489,8 +489,8 @@ class TimerTool(Gtk.Box):
|
||||
self._info_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
|
||||
self._ref_info_label.set_text(timer.get("e2servicereference", ""))
|
||||
self._event_id_info_label.set_text(timer.get("e2eit", ""))
|
||||
self._action_info_label.set_text(get_message(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
|
||||
self._after_info_label.set_text(get_message(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0")))
|
||||
self._action_info_label.set_text(translate(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
|
||||
self._after_info_label.set_text(translate(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0")))
|
||||
self._begins_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))))
|
||||
self._ends_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timeend", "0")))))
|
||||
self.set_repetition_flags(int(timer.get("e2repeated", "0")), self._days_buttons)
|
||||
|
||||
@@ -37,7 +37,7 @@ gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN, IS_LINUX, GTK_PATH
|
||||
|
||||
# Setting mod mask for keyboard depending on platform
|
||||
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
|
||||
@@ -186,15 +186,6 @@ class Page(Enum):
|
||||
CONTROL = "control"
|
||||
|
||||
|
||||
class FavClickMode(IntEnum):
|
||||
""" Double click mode on the service in the bouquet(FAV) list. """
|
||||
DISABLED = 0
|
||||
STREAM = 1
|
||||
PLAY = 2
|
||||
ZAP = 3
|
||||
ZAP_PLAY = 4
|
||||
|
||||
|
||||
class ViewTarget(Enum):
|
||||
""" Used for set target view. """
|
||||
BOUQUET = 0
|
||||
|
||||
@@ -22,3 +22,7 @@ grid > button {
|
||||
popover .view {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
headerbar .titlebutton > image {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -30,6 +30,8 @@ import concurrent.futures
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from itertools import groupby
|
||||
from math import fabs
|
||||
|
||||
from gi.repository import GLib
|
||||
@@ -38,36 +40,32 @@ from app.commons import run_idle, run_task, log
|
||||
from app.eparser import Satellite, Transponder
|
||||
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, POLARIZATION, FEC, SYSTEM, MODULATION, Terrestrial, Cable,
|
||||
T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC, GUARD_INTERVAL, TRANSMISSION_MODE,
|
||||
HIERARCHY, Inversion, C_MODULATION, FEC_DEFAULT, TerTransponder, CableTransponder)
|
||||
from app.settings import USE_HEADER_BAR
|
||||
HIERARCHY, Inversion, C_MODULATION, FEC_DEFAULT, TerTransponder, CableTransponder,
|
||||
Bouquet, BouquetService, BqServiceType, Bouquets, BqType)
|
||||
from app.eparser.satxml import get_pos_str
|
||||
from app.settings import Settings, CONFIG_PATH
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
|
||||
from ..dialogs import show_dialog, DialogType, get_message, get_builder
|
||||
from ..main_helper import append_text_to_tview, get_base_model, on_popup_menu
|
||||
from ..dialogs import show_dialog, BaseDialog, DialogType, translate, get_builder
|
||||
from ..main_helper import append_text_to_tview, get_base_model, on_popup_menu, get_services_type_groups
|
||||
from ..search import SearchProvider
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HeaderBar
|
||||
|
||||
_DIALOGS_UI_PATH = f"{UI_RESOURCES_PATH}xml{os.sep}dialogs.glade"
|
||||
|
||||
|
||||
class DVBDialog(Gtk.Dialog):
|
||||
class DVBDialog(BaseDialog):
|
||||
""" Base dialog class for editing DVB (-> *.xml) data. """
|
||||
|
||||
def __init__(self, parent, title, data=None, *args, **kwargs):
|
||||
super().__init__(transient_for=parent,
|
||||
title=get_message(title),
|
||||
modal=True,
|
||||
resizable=False,
|
||||
default_width=320,
|
||||
skip_taskbar_hint=True,
|
||||
skip_pager_hint=True,
|
||||
destroy_with_parent=True,
|
||||
use_header_bar=USE_HEADER_BAR,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK),
|
||||
*args, **kwargs)
|
||||
super().__init__(parent=parent, title=title, *args, **kwargs)
|
||||
|
||||
self.frame = Gtk.Frame(margin=5, label_xalign=0.02)
|
||||
self.get_content_area().pack_start(self.frame, True, True, 0)
|
||||
self._viewport = Gtk.Viewport(margin_top=2)
|
||||
self._viewport.get_style_context().add_class("view")
|
||||
self._frame = Gtk.Frame(margin=5, label_xalign=0.02, shadow_type=Gtk.ShadowType.NONE)
|
||||
self._label = Gtk.Label(margin_bottom=2, use_markup=True)
|
||||
self._frame.set_label_widget(self._label)
|
||||
self._frame.add(self._viewport)
|
||||
self.get_content_area().pack_start(self._frame, True, True, 0)
|
||||
|
||||
self._data = data
|
||||
|
||||
@@ -75,13 +73,19 @@ class DVBDialog(Gtk.Dialog):
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
def set_content(self, widget):
|
||||
self._viewport.add(widget)
|
||||
|
||||
def set_label_text(self, text):
|
||||
self._label.set_markup(f"<b>{text}</b>")
|
||||
|
||||
|
||||
class TransponderDialog(DVBDialog):
|
||||
""" Base transponder dialog class. """
|
||||
|
||||
def __init__(self, parent, title, data=None, *args, **kwargs):
|
||||
super().__init__(parent, title, data, *args, **kwargs)
|
||||
self.frame.set_label(get_message("Transponder properties:"))
|
||||
self.set_label_text(translate("Transponder properties:"))
|
||||
# Pattern for digits entries.
|
||||
self.digit_pattern = re.compile(r"\D")
|
||||
# Style
|
||||
@@ -120,8 +124,8 @@ class TCDialog(DVBDialog):
|
||||
super().__init__(parent, title, data, *args, **kwargs)
|
||||
|
||||
self._entry = Gtk.Entry(margin=5)
|
||||
self.frame.add(self._entry)
|
||||
self.frame.set_label(get_message("Name:"))
|
||||
self.set_content(self._entry)
|
||||
self.set_label_text(translate("Name:"))
|
||||
self.show_all()
|
||||
|
||||
if data:
|
||||
@@ -136,8 +140,8 @@ class SatelliteDialog(DVBDialog):
|
||||
builder = get_builder(_DIALOGS_UI_PATH, use_str=True,
|
||||
objects=("sat_dialog_box", "side_store", "pos_adjustment"))
|
||||
|
||||
self.frame.add(builder.get_object("sat_dialog_box"))
|
||||
self.frame.set_label(get_message("Satellite properties:"))
|
||||
self.set_content(builder.get_object("sat_dialog_box"))
|
||||
self.set_label_text(translate("Satellite properties:"))
|
||||
self._sat_name = builder.get_object("sat_name_entry")
|
||||
self._sat_position = builder.get_object("sat_position_button")
|
||||
self._side = builder.get_object("side_box")
|
||||
@@ -192,7 +196,7 @@ class SatTransponderDialog(TransponderDialog):
|
||||
objects = ("sat_tr_box", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
|
||||
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=objects)
|
||||
|
||||
self.frame.add(builder.get_object("sat_tr_box"))
|
||||
self.set_content(builder.get_object("sat_tr_box"))
|
||||
self._freq_entry = builder.get_object("freq_entry")
|
||||
self._rate_entry = builder.get_object("rate_entry")
|
||||
self._pol_box = builder.get_object("pol_box")
|
||||
@@ -264,7 +268,7 @@ class TerTransponderDialog(TransponderDialog):
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("ter_tr_box",))
|
||||
|
||||
self.frame.add(builder.get_object("ter_tr_box"))
|
||||
self.set_content(builder.get_object("ter_tr_box"))
|
||||
self._freq_entry = builder.get_object("ter_freq_entry")
|
||||
self._sys_box = builder.get_object("ter_sys_box")
|
||||
self._bandwidth_box = builder.get_object("ter_bandwidth_box")
|
||||
@@ -342,7 +346,7 @@ class CableTransponderDialog(TransponderDialog):
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("cable_tr_box",))
|
||||
|
||||
self.frame.add(builder.get_object("cable_tr_box"))
|
||||
self.set_content(builder.get_object("cable_tr_box"))
|
||||
|
||||
self._freq_entry = builder.get_object("cable_freq_entry")
|
||||
self._rate_entry = builder.get_object("cable_rate_entry")
|
||||
@@ -408,14 +412,13 @@ class UpdateDialog:
|
||||
self._settings = settings
|
||||
self._download_task = False
|
||||
self._parser = None
|
||||
self._size_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}_window_size".lower()
|
||||
self._selected_satellites = set()
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}xml{os.sep}update.glade", handlers)
|
||||
|
||||
self._window = builder.get_object("satellites_update_window")
|
||||
self._window.set_transient_for(transient)
|
||||
if title:
|
||||
self._window.set_title(title)
|
||||
self._window.set_title(title if title else "")
|
||||
|
||||
self._transponder_paned = builder.get_object("sat_update_tr_paned")
|
||||
self._sat_view = builder.get_object("sat_update_tree_view")
|
||||
@@ -435,6 +438,8 @@ class UpdateDialog:
|
||||
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
|
||||
self._sat_view.bind_property("sensitive", self._receive_button, "sensitive")
|
||||
self._receive_button.bind_property("visible", update_button, "visible")
|
||||
self._left_action_box = builder.get_object("sat_update_left_action_box")
|
||||
self._right_action_box = builder.get_object("sat_update_right_action_box")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("sat_update_filter_bar")
|
||||
self._from_pos_button = builder.get_object("from_pos_button")
|
||||
@@ -456,6 +461,12 @@ class UpdateDialog:
|
||||
builder.get_object("sat_update_search_down_button"),
|
||||
builder.get_object("sat_update_search_up_button"))
|
||||
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
|
||||
# Satellite lists init on dialog start.
|
||||
self._sat_view.connect("realize", self.on_update_satellites_list)
|
||||
# Options.
|
||||
self._general_options_box = builder.get_object("general_options_box")
|
||||
self._save_sat_selection_switch = builder.get_object("save_sat_selection_switch")
|
||||
self._skip_c_band_switch = builder.get_object("skip_c_band_switch")
|
||||
|
||||
if self._settings.use_header_bar:
|
||||
header_bar = HeaderBar()
|
||||
@@ -463,15 +474,23 @@ class UpdateDialog:
|
||||
header_box = builder.get_object("satellites_update_header_box")
|
||||
header_box.remove(self._source_box)
|
||||
header_bar.pack_start(self._source_box)
|
||||
action_box = builder.get_object("sat_update_left_action_box")
|
||||
header_box.remove(action_box)
|
||||
header_bar.pack_start(action_box)
|
||||
action_box = builder.get_object("sat_update_right_action_box")
|
||||
header_box.remove(action_box)
|
||||
header_bar.pack_end(action_box)
|
||||
header_box.remove(self._left_action_box)
|
||||
header_bar.pack_start(self._left_action_box)
|
||||
header_box.remove(self._right_action_box)
|
||||
header_bar.pack_end(self._right_action_box)
|
||||
self._window.set_titlebar(header_bar)
|
||||
|
||||
window_size = self._settings.get(self._size_name)
|
||||
# Dialog settings.
|
||||
self._dialog_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}".lower()
|
||||
self._dialog_settings = self._settings.get(self._dialog_name, {})
|
||||
self._source_box.set_active(self._dialog_settings.get("source", 1))
|
||||
self._save_sat_selection_switch.set_active(self._dialog_settings.get("save_sat_selection", False))
|
||||
self._skip_c_band_switch.set_active(self._dialog_settings.get("skip_c_band", False))
|
||||
|
||||
if self._save_sat_selection_switch.get_active():
|
||||
self._selected_satellites.update(self.get_selected_satellites())
|
||||
|
||||
window_size = self._dialog_settings.get("window_size", None)
|
||||
if window_size:
|
||||
self._window.resize(*window_size)
|
||||
|
||||
@@ -497,11 +516,11 @@ class UpdateDialog:
|
||||
|
||||
self.is_download = True
|
||||
self._sat_view.set_sensitive(False)
|
||||
src = self._source_box.get_active()
|
||||
|
||||
if not self._parser:
|
||||
self._parser = SatellitesParser()
|
||||
|
||||
self.get_sat_list(src, self.append_satellites)
|
||||
self.get_sat_list(self._source_box.get_active(), self.append_satellites)
|
||||
|
||||
def clear_data(self):
|
||||
get_base_model(self._sat_view.get_model()).clear()
|
||||
@@ -526,11 +545,14 @@ class UpdateDialog:
|
||||
@run_idle
|
||||
def append_satellites(self, sats):
|
||||
model = get_base_model(self._sat_view.get_model())
|
||||
|
||||
for sat in sats:
|
||||
model.append(sat)
|
||||
itr = model.append(sat)
|
||||
model[itr][-1] = sat[-2] in self._selected_satellites
|
||||
|
||||
self._sat_view.set_sensitive(True)
|
||||
self._satellites_count_label.set_text(str(len(model)))
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
@@ -571,12 +593,6 @@ class UpdateDialog:
|
||||
def update_receive_button_state(self, model):
|
||||
self._receive_button.set_sensitive((any(r[4] for r in model)))
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._sat_update_info_bar.set_visible(True)
|
||||
self._sat_update_info_bar.set_message_type(message_type)
|
||||
self._info_bar_message_label.set_text(text)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._sat_update_info_bar.set_visible(False)
|
||||
|
||||
@@ -625,10 +641,33 @@ class UpdateDialog:
|
||||
itr = self._filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(model.get_iter(path)))
|
||||
self._filter_model.get_model().set_value(itr, 4, select)
|
||||
|
||||
if self._save_sat_selection_switch.get_active():
|
||||
sat = model[path][-2]
|
||||
self._selected_satellites.add(sat) if select else self._selected_satellites.discard(sat)
|
||||
|
||||
def on_quit(self, window, event):
|
||||
self._settings.add(self._size_name, window.get_size())
|
||||
self.save_settings()
|
||||
self.is_download = False
|
||||
|
||||
def save_settings(self):
|
||||
self._dialog_settings["window_size"] = self._window.get_size()
|
||||
self._dialog_settings["source"] = self._source_box.get_active()
|
||||
self._dialog_settings["save_sat_selection"] = self._save_sat_selection_switch.get_active()
|
||||
self._dialog_settings["skip_c_band"] = self._skip_c_band_switch.get_active()
|
||||
self._settings.add(self._dialog_name, self._dialog_settings)
|
||||
self.save_selected_satellites()
|
||||
|
||||
def get_selected_satellites(self):
|
||||
""" Returns selected satellites set from the last session. """
|
||||
c_file = f"{CONFIG_PATH}{self._dialog_name}_satellites"
|
||||
return Settings.get_settings(c_file, default_settings=[])
|
||||
|
||||
def save_selected_satellites(self):
|
||||
""" Saves current selected satellites to a file. """
|
||||
if self._save_sat_selection_switch.get_active():
|
||||
c_file = f"{CONFIG_PATH}{self._dialog_name}_satellites"
|
||||
Settings.write_settings(list(self._selected_satellites), config_file=c_file)
|
||||
|
||||
|
||||
class SatellitesUpdateDialog(UpdateDialog):
|
||||
""" Dialog for update satellites from the Web. """
|
||||
@@ -638,6 +677,16 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
|
||||
self._main_model = main_model
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
# Options.
|
||||
self._merge_sat_switch = Gtk.Switch(active=self._dialog_settings.get("merge_satellites", False))
|
||||
self._merge_sat_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"merge_satellites": s}))
|
||||
box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
box.pack_start(Gtk.Label(translate("Merge satellites by positions")), False, True, 0)
|
||||
box.pack_end(self._merge_sat_switch, False, True, 0)
|
||||
self._general_options_box.pack_start(box, True, True, 0)
|
||||
self._general_options_box.show_all()
|
||||
|
||||
self._skip_c_band_switch.get_parent().set_visible(False)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
@@ -653,6 +702,7 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
self.update_log_visibility()
|
||||
model = self._sat_view.get_model()
|
||||
start = time.time()
|
||||
_len = 75
|
||||
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
|
||||
text = "Processing: {}\n"
|
||||
@@ -672,10 +722,39 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
appender.send(text.format(data[0]))
|
||||
sats.append(data)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send("-" * _len + "\n")
|
||||
sat_count = len(sats)
|
||||
|
||||
sats = {s[0]: s for s in sats} # key = name, v = satellite
|
||||
if self._merge_sat_switch.get_active():
|
||||
def grouper(sat):
|
||||
try:
|
||||
return int(sat.position)
|
||||
except ValueError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
sat_groups = groupby(sorted(sats, key=grouper, reverse=True), key=grouper)
|
||||
sats = {}
|
||||
for pos, satellites in sat_groups:
|
||||
satellites = list(satellites)
|
||||
if len(satellites) > 1:
|
||||
position = get_pos_str(pos)
|
||||
appender.send(f"Merging satellites for position: {position}\n")
|
||||
names = []
|
||||
transponders = []
|
||||
for s in satellites:
|
||||
names.append(s.name.lstrip(position).strip().split())
|
||||
transponders.extend(s.transponders)
|
||||
|
||||
transponders.sort(key=lambda t: int(t.frequency))
|
||||
sat = Satellite(self.get_grouped_satellite_name(names, position), "0", str(pos), transponders)
|
||||
sats[sat.name] = sat
|
||||
else:
|
||||
sat = satellites.pop()
|
||||
sats[sat.name] = sat
|
||||
appender.send("-" * _len + "\n")
|
||||
else:
|
||||
sats = {s.name: s for s in sats} # key = name, v = satellite
|
||||
|
||||
for row in self._main_model:
|
||||
pos = row[0]
|
||||
@@ -688,11 +767,39 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
appender.send(f"Adding satellite: {s.name}\n")
|
||||
self.append_satellite(s)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send("-" * _len + "\n")
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {sat_count} satellites received.\n")
|
||||
appender.close()
|
||||
self.is_download = False
|
||||
|
||||
def get_grouped_satellite_name(self, sat_names, pos):
|
||||
""" Forms name for merged satellites. """
|
||||
|
||||
def name_grouper(nd):
|
||||
if nd:
|
||||
return nd[0]
|
||||
return ""
|
||||
|
||||
name_groups = groupby(sorted(sat_names, key=name_grouper), key=name_grouper)
|
||||
names = []
|
||||
for s, s_names in name_groups:
|
||||
tk = set()
|
||||
name = s
|
||||
for i, n_data in enumerate(s_names):
|
||||
if i == 0:
|
||||
name = " ".join(n_data)
|
||||
tk.update(n_data)
|
||||
else:
|
||||
for n in n_data:
|
||||
if n in tk:
|
||||
continue
|
||||
name = f"{name}/{n}"
|
||||
tk.add(n)
|
||||
|
||||
names.append(name)
|
||||
|
||||
return f"{pos} {' & '.join(names)}"
|
||||
|
||||
@run_idle
|
||||
def append_satellite(self, sat):
|
||||
self._main_model.append(sat)
|
||||
@@ -701,22 +808,22 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
class ServicesUpdateDialog(UpdateDialog):
|
||||
""" Dialog for updating services from the Web. """
|
||||
|
||||
def __init__(self, transient, settings, callback):
|
||||
super().__init__(transient=transient, settings=settings, title="Services update")
|
||||
def __init__(self, app):
|
||||
super().__init__(transient=app.app_window, settings=app.app_settings, title="Services update")
|
||||
|
||||
self._callback = callback
|
||||
self._callback = app.on_import_data_from_web
|
||||
self._satellite_paths = {}
|
||||
self._transponders = {}
|
||||
self._services = {}
|
||||
self._selected_transponders = set()
|
||||
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
|
||||
# Transponder view popup menu
|
||||
# Transponder view popup menu.
|
||||
tr_popup_menu = Gtk.Menu()
|
||||
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
|
||||
select_all_item.connect("activate", lambda w: self.update_transponder_selection(True))
|
||||
tr_popup_menu.append(select_all_item)
|
||||
remove_selection_item = Gtk.ImageMenuItem.new_from_stock("gtk-undo")
|
||||
remove_selection_item.set_label(get_message("Remove selection"))
|
||||
remove_selection_item.set_label(translate("Remove selection"))
|
||||
remove_selection_item.connect("activate", lambda w: self.update_transponder_selection(False))
|
||||
tr_popup_menu.append(remove_selection_item)
|
||||
tr_popup_menu.show_all()
|
||||
@@ -728,6 +835,24 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
|
||||
self._transponder_paned.set_visible(True)
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
self._source_box.connect("changed", self.on_source_changed)
|
||||
# Options for KingOfSat source.
|
||||
self._kos_bq_groups_switch = Gtk.Switch(active=self._dialog_settings.get("kos_bq_groups", False))
|
||||
self._kos_bq_groups_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"kos_bq_groups": s}))
|
||||
self._kos_bq_lang_switch = Gtk.Switch(active=self._dialog_settings.get("kos_bq_lang", False))
|
||||
self._kos_bq_lang_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"kos_bq_lang": s}))
|
||||
self._kos_options_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5, margin_top=5)
|
||||
box.pack_start(Gtk.Label(translate("Create Category bouquets")), False, True, 0)
|
||||
box.pack_end(self._kos_bq_groups_switch, False, True, 0)
|
||||
self._kos_options_box.add(box)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5, margin_bottom=5)
|
||||
box.pack_start(Gtk.Label(translate("Create Regional bouquets")), False, True, 0)
|
||||
box.pack_end(self._kos_bq_lang_switch, False, True, 0)
|
||||
self._kos_options_box.add(box)
|
||||
self._kos_options_box.connect("realize", self.on_source_changed)
|
||||
self._general_options_box.pack_start(self._kos_options_box, True, True, 0)
|
||||
self._general_options_box.show_all()
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
@@ -737,6 +862,14 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
|
||||
self.receive_services()
|
||||
|
||||
def on_source_changed(self, item):
|
||||
is_kos = self._source_box.get_active_id() == SatelliteSource.KINGOFSAT.name
|
||||
self._kos_options_box.set_sensitive(is_kos)
|
||||
if not is_kos:
|
||||
self._kos_bq_groups_switch.set_active(False)
|
||||
self._kos_bq_lang_switch.set_active(False)
|
||||
self._kos_options_box.set_tooltip_text(None if is_kos else translate("KingOfSat only!"))
|
||||
|
||||
@run_task
|
||||
def receive_services(self):
|
||||
self.is_download = True
|
||||
@@ -805,6 +938,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
log(f"Getting services error: {e} [{t_names.get(futures[future])}]")
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
services = OrderedDict({s.fav_id: s for s in services}).values()
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
|
||||
|
||||
try:
|
||||
@@ -815,10 +949,47 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
except ValueError as e:
|
||||
log(f"ServicesUpdateDialog [on receive data] error: {e}")
|
||||
else:
|
||||
self._callback(srvs)
|
||||
bouquets = None
|
||||
if self._source_box.get_active_id() == SatelliteSource.KINGOFSAT.name:
|
||||
bouquets = self.get_bouquets([srv._replace(fav_id=srvs[i].fav_id) for i, srv in enumerate(services)])
|
||||
|
||||
def c_filter(s):
|
||||
try:
|
||||
return int(s.freq) > 10000
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
self._callback(filter(c_filter, srvs) if self._skip_c_band_switch.get_active() else srvs, bouquets)
|
||||
|
||||
self.is_download = False
|
||||
|
||||
def get_bouquets(self, services):
|
||||
type_groups = get_services_type_groups(services)
|
||||
tv_bouquets, radio_bouquets = [], []
|
||||
|
||||
tv_services = sorted(type_groups.get("TV", []), key=lambda s: s.service)
|
||||
rd_services = sorted(type_groups.get("Radio", []), key=lambda s: s.service)
|
||||
no_lb = "No Category"
|
||||
|
||||
if self._kos_bq_groups_switch.get_active():
|
||||
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or no_lb)
|
||||
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or no_lb, bq_type=BqType.RADIO.value)
|
||||
|
||||
if self._kos_bq_lang_switch.get_active():
|
||||
lb = "" if no_lb in {b.name for b in tv_bouquets} else "No Region"
|
||||
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or lb)
|
||||
lb = "" if no_lb in {b.name for b in radio_bouquets} else "No Region"
|
||||
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or lb, bq_type=BqType.RADIO.value)
|
||||
|
||||
return Bouquets("", BqType.TV.value, tv_bouquets), Bouquets("", BqType.RADIO.value, radio_bouquets)
|
||||
|
||||
def gen_bouquet_group(self, services, bouquets, grouper, bq_type=BqType.TV.value):
|
||||
""" Generates bouquets depending on <grouper>. """
|
||||
s_type = BqServiceType.DEFAULT
|
||||
[bouquets.append(Bouquet(name=g[0], type=bq_type,
|
||||
services=[BouquetService(None, s_type, s.fav_id, 0) for s in g[1]])) for g in
|
||||
groupby(sorted(services, key=grouper), key=grouper) if g[0]]
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
@@ -831,10 +1002,9 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self.is_download = False
|
||||
|
||||
def on_satellite_toggled(self, toggle, path):
|
||||
model = self._sat_view.get_model()
|
||||
self.update_state(model, path, not toggle.get_active())
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
super().on_satellite_toggled(toggle, path)
|
||||
|
||||
model = self._sat_view.get_model()
|
||||
url = model.get_value(model.get_iter(path), 3)
|
||||
selected = toggle.get_active()
|
||||
transponders = self._transponders.get(url, None)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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,10 +37,10 @@ from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from app.eparser.ecommons import (POLARIZATION, FEC, SYSTEM, MODULATION, T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC,
|
||||
GUARD_INTERVAL, TRANSMISSION_MODE, HIERARCHY, Inversion, FEC_DEFAULT, C_MODULATION,
|
||||
Terrestrial, Cable, CableTransponder, TerTransponder)
|
||||
from app.eparser.satxml import get_terrestrial, get_cable, write_terrestrial, write_cable
|
||||
from .dialogs import SatelliteDialog, SatellitesUpdateDialog, TerrestrialDialog, CableDialog, SatTransponderDialog, \
|
||||
CableTransponderDialog, TerTransponderDialog
|
||||
from ..dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from app.eparser.satxml import get_terrestrial, get_cable, write_terrestrial, write_cable, get_pos_str
|
||||
from .dialogs import (SatelliteDialog, SatellitesUpdateDialog, TerrestrialDialog, CableDialog, SatTransponderDialog,
|
||||
CableTransponderDialog, TerTransponderDialog)
|
||||
from ..dialogs import show_dialog, DialogType, get_chooser_dialog, translate, get_builder
|
||||
from ..main_helper import move_items, on_popup_menu, scroll_to
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK, Page
|
||||
|
||||
@@ -56,10 +56,11 @@ class SatellitesTool(Gtk.Box):
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, app, settings, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-open", self.on_open)
|
||||
self._app.connect("data-save", self.on_save)
|
||||
self._app.connect("data-save-as", self.on_save_as)
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
@@ -162,8 +163,7 @@ class SatellitesTool(Gtk.Box):
|
||||
|
||||
def sat_pos_func(self, column, renderer, model, itr, data):
|
||||
""" Converts and sets the satellite position value to a readable format. """
|
||||
pos = int(model.get_value(itr, 2))
|
||||
renderer.set_property("text", f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}")
|
||||
renderer.set_property("text", get_pos_str(int(model.get_value(itr, 2))))
|
||||
|
||||
def sat_pol_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", POLARIZATION.get(model.get_value(itr, 2), None))
|
||||
@@ -369,7 +369,7 @@ class SatellitesTool(Gtk.Box):
|
||||
data = func(path)
|
||||
yield True
|
||||
except FileNotFoundError as e:
|
||||
msg = get_message("Please, download files from receiver or setup your path for read data!")
|
||||
msg = translate("Please, download files from receiver or setup your path for read data!")
|
||||
self._app.show_error_message(f"{e}\n{msg}")
|
||||
except ExpatError as e:
|
||||
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
|
||||
@@ -523,8 +523,10 @@ class SatellitesTool(Gtk.Box):
|
||||
return self._ter_tr_view
|
||||
return self._cable_tr_view
|
||||
|
||||
@run_idle
|
||||
def on_open(self):
|
||||
def on_open(self, app, page):
|
||||
if page is not Page.SATELLITE:
|
||||
return
|
||||
|
||||
xml_file = "satellites.xml"
|
||||
if self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
xml_file = "terrestrial.xml"
|
||||
@@ -555,18 +557,26 @@ class SatellitesTool(Gtk.Box):
|
||||
@run_idle
|
||||
def on_save(self, app, page):
|
||||
if page is Page.SATELLITE and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
f"{self._settings.profile_data_path}satellites.xml")
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()),
|
||||
f"{self._settings.profile_data_path}terrestrial.xml")
|
||||
else:
|
||||
write_cable((Cable(*r) for r in self._cable_view.get_model()),
|
||||
f"{self._settings.profile_data_path}cables.xml")
|
||||
self.save_data(self._settings.profile_data_path)
|
||||
|
||||
def save_data(self, path):
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()), f"{path}satellites.xml")
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()), f"{path}terrestrial.xml")
|
||||
else:
|
||||
write_cable((Cable(*r) for r in self._cable_view.get_model()), f"{path}cables.xml")
|
||||
|
||||
def on_save_as(self, app, page):
|
||||
self._app.show_error_message("Not implemented yet!")
|
||||
if page is not Page.SATELLITE:
|
||||
return
|
||||
|
||||
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, buttons=buttons)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
self.save_data(response)
|
||||
|
||||
def on_download(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
||||
#!/bin/bash
|
||||
VER="3.4.2_Beta"
|
||||
VER="3.9.0_Alpha"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
mkdir -p $B_PATH
|
||||
cp -TRv deb $B_PATH
|
||||
|
||||
rsync -arv ../../app/ui/lang/* "$B_PATH/usr/share/locale"
|
||||
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
|
||||
rsync --exclude=__pycache__ -arv ../../extensions $DEB_PATH
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: demon-editor
|
||||
Version: 3.4.2-Beta
|
||||
Version: 3.9.0-Alpha
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
|
||||
@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
|
||||
Files: *
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2023 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
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
python3 /usr/share/demoneditor/start.py $1
|
||||
python3 /usr/share/demoneditor/start.py $@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,7 +32,7 @@ a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=None,
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid'],
|
||||
hiddenimports=['fileinput', 'uuid', 'asyncio'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
hooksconfig={
|
||||
@@ -81,7 +81,7 @@ app = BUNDLE(coll,
|
||||
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
|
||||
'LSApplicationCategoryType': 'public.app-category.utilities',
|
||||
'LSMinimumSystemVersion': '10.13',
|
||||
'CFBundleShortVersionString': f"3.4.2.{BUILD_DATE} Beta",
|
||||
'CFBundleShortVersionString': f"3.9.0.{BUILD_DATE} Alpha",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2023, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false',
|
||||
'NSHighResolutionCapable': 'true'
|
||||
|
||||
@@ -30,7 +30,7 @@ a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=[],
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes'],
|
||||
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes', 'asyncio'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
hooksconfig={
|
||||
|
||||
@@ -47,6 +47,9 @@ class BaseExtension(metaclass=Singleton):
|
||||
# The label that will be displayed in the "Tools" menu.
|
||||
LABEL = "Base extension"
|
||||
VERSION = "1.0"
|
||||
# Additional flags.
|
||||
EMBEDDED = False
|
||||
SWITCHABLE = False
|
||||
|
||||
_LOGGER_NAME = "main_logger"
|
||||
|
||||
@@ -65,6 +68,10 @@ class BaseExtension(metaclass=Singleton):
|
||||
"""
|
||||
self.app.show_info_message(f"Hello from {self.__class__.__name__} class!")
|
||||
|
||||
def stop(self):
|
||||
""" Stops (terminates) the task or the extension itself. """
|
||||
self.log("Terminating a task...")
|
||||
|
||||
def log(self, message, level=logging.ERROR):
|
||||
""" Shows log messages. """
|
||||
logging.getLogger(self._LOGGER_NAME).log(level, f"[{self.__class__.__name__}] {message}")
|
||||
|
||||
@@ -1358,8 +1358,8 @@ msgstr "Усе букеты"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Прайграванне з асноўнага спіса"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Улучае аналіз URL-адрасоў з дапамогай youtube-dl для атрымання прамых спасылак на медыя."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Улучае аналіз URL-адрасоў з дапамогай yt-dlp для атрымання прамых спасылак на медыя."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Дазволы..."
|
||||
@@ -1446,3 +1446,60 @@ msgstr "Выдаліць дублікаты"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Выдалена"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Улучае перазапіс існых сэрвісаў асноўнага спіса."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Улучае пропуск імпарту сэрвісаў з lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Толькі дадзеныя букетаў"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Стварыць букеты па катэгорыях"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Стварыць букеты па рэгіёнах"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "Прапусціць C-дыяпазон"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Аўтаматычная ўсталёўка імя, абранага ў спісе букетаў."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Аб'яднаць спадарожнікі па пазіцыях"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Захоўваць выбар спадарожнікаў"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Выняць простыя спасылкі"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Префикс URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Недапушчальны прэфікс для дадзенага URL!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Альтэрнатыўны загаловак акна"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Абраны тып:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Менеджар пашырэнняў"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Вер."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Усталявана"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Скарыстаць агульную тэчку для піконаў"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Актывуе выкарыстанне адзінай тэчкі для некалькіх профіляў."
|
||||
|
||||
6
po/build.sh
Normal file → Executable file
6
po/build.sh
Normal file → Executable file
@@ -1,3 +1,7 @@
|
||||
#!/bin/bash
|
||||
#xgettext --keyword=translatable --sort-output -L Glade -o po/demon-editor.po app/ui/main_window.glade
|
||||
#msgfmt demon-editor.po -o demon-editor.mo
|
||||
|
||||
for dir in */;
|
||||
do
|
||||
msgfmt $dir* -o ../app/ui/lang/${dir%/}/LC_MESSAGES/demon-editor.mo
|
||||
done
|
||||
@@ -797,7 +797,7 @@ msgid "Apply profile settings"
|
||||
msgstr "Profileinstellungen anwenden"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Art der Einstellungen:"
|
||||
msgstr "Einstellungstyp:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Standard wiederherstellen"
|
||||
@@ -1372,8 +1372,8 @@ msgstr "Alle Bouquets"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Wiedergabe aus der Hauptliste"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Aktiviert URL-Parsing mit youtube-dl, um direkte Links zu Medien zu erhalten."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Aktiviert URL-Parsing mit yt-dlp, um direkte Links zu Medien zu erhalten."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Berechtigungen..."
|
||||
@@ -1460,3 +1460,60 @@ msgstr "Duplikate entfernen"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Entfernt"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Ermöglicht das Überschreiben vorhandener Hauptlistendienste."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Ermöglicht das Überspringen von Dienstimporten aus lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Nur Bouquets-Daten"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Erstellen Kategories-Bouquets"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Erstellen Regionale-Bouquets"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "C-Band überspringen"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Stellen in der Bouquet-Liste ausgewählten Namen automatisch ein."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Satelliten nach Pos. zusammenführen"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Satellitenauswahl speichern"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Direktlinks extrahieren"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "URL-Präfix:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Ungültiges Präfix für die angegebene URL!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Alternativer Fenstertitel"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Ausgewählt Typ:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Erweiterungs-Manager"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Installiert"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Gemeinsamen Ordner für Picons verwenden"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Aktiviert die Verwendung eines einzelnen Ordners für mehrere Profile."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2022 Víctor Pont
|
||||
# Copyright (C) 2018-2023 Frank Neirynck
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Frank Neirynck <frank@insink.be>, 2018-2020.
|
||||
# Frank Neirynck <frank@insink.be>, 2018-2023.
|
||||
# Víctor Pont <victor@pont.cat>, 2021-2022.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
@@ -1386,8 +1386,8 @@ msgstr "Todos los bouquets"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Reproducción desde la lista principal"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Habilita el análisis de URL usando youtube-dl para obtener enlaces directos a los medios."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Habilita el análisis de URL usando yt-dlp para obtener enlaces directos a los medios."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Permisos..."
|
||||
@@ -1441,3 +1441,87 @@ msgstr "Habilita la carga como un archivo si se selecciona una gran cantidad de
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Limpiar \"Nuevo\" flag"
|
||||
|
||||
msgid "Group by"
|
||||
msgstr "Agrupar por"
|
||||
|
||||
msgid "Replace existing"
|
||||
msgstr "Sustituir los existentes"
|
||||
|
||||
msgid "Already exists"
|
||||
msgstr "Ya existe"
|
||||
|
||||
msgid "Enable unlimited copy buffer"
|
||||
msgstr "Activar búfer de copia ilimitado"
|
||||
|
||||
msgid "Enables unlimited copy buffer for the bouquets tab."
|
||||
msgstr "Activa el búfer de copia ilimitado para la pestaña de bouquets."
|
||||
|
||||
msgid "Start time"
|
||||
msgstr "Hora de inicio"
|
||||
|
||||
msgid "End time"
|
||||
msgstr "Hora final"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Activar el soporte de extensiones"
|
||||
|
||||
msgid "After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr "Después de cargar los cambios, es posible que tenga que reiniciar completamente el receptor."
|
||||
|
||||
msgid "Remove duplicates"
|
||||
msgstr "Eliminar duplicados"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Eliminado"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Permite sobrescribir los servicios existentes de la lista principal."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Permite omitir la importación de servicios desde lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Sólo datos de bouquets"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Crear categoría de bouquets"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Crear bouquets regionales"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "Omitir banda C"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Establece automáticamente el nombre seleccionado en la lista de bouquets."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Combinar satélites por posiciones"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Guardar selección de satélites"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Extraer enlaces directos"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Prefijo URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "¡Prefijo inválido para la URL dada!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Título alternativo de ventana"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Tipo elegido:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Gestor de extensiones"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Instalado"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Copyright (C) 2018-2022 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
# Massimo Pissarello <mapi68@gmail.com>, 2022, 2023.
|
||||
# SPDX-FileCopyrightText: 2022, 2023 Massimo Pissarello <mapi68@gmail.com>
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"PO-Revision-Date: 2023-02-12 07:10+0100\n"
|
||||
"PO-Revision-Date: 2023-12-18 05:40+0100\n"
|
||||
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
|
||||
"Language-Team: Italian <>\n"
|
||||
"Language: it\n"
|
||||
@@ -14,10 +13,12 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Lokalize 22.12.2\n"
|
||||
"X-Generator: Lokalize 23.08.4\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "Massimo Pissarello"
|
||||
msgstr ""
|
||||
"Nicola Fanghella\n"
|
||||
"Massimo Pissarello"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -294,10 +295,10 @@ msgstr "Chiudi"
|
||||
|
||||
# Picons dialog
|
||||
msgid "Load providers"
|
||||
msgstr "Carica provider"
|
||||
msgstr "Carica fornitori"
|
||||
|
||||
msgid "Providers"
|
||||
msgstr "Provider"
|
||||
msgstr "Fornitori"
|
||||
|
||||
msgid "Receive picons"
|
||||
msgstr "Scarica picon"
|
||||
@@ -336,16 +337,16 @@ msgid "Path to Enigma2 picons:"
|
||||
msgstr "Persorso picon su Enigma2:"
|
||||
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Specifica il valore di posizione corretto per il provider!"
|
||||
msgstr "Specifica il valore di posizione corretto per il fornitore!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "Convertitore tra formati di nome"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Scarica picon per provider"
|
||||
msgstr "Scarica picon per fornitore"
|
||||
|
||||
msgid "Load satellite providers."
|
||||
msgstr "Carica provider satellitari"
|
||||
msgstr "Carica fornitori satellitari"
|
||||
|
||||
msgid ""
|
||||
"To automatically set the identifiers for picons,\n"
|
||||
@@ -411,7 +412,7 @@ msgid "Reference"
|
||||
msgstr "Riferimento"
|
||||
|
||||
msgid "Namespace"
|
||||
msgstr "Spazio dei nomi"
|
||||
msgstr "Namespace"
|
||||
|
||||
msgid "Flags:"
|
||||
msgstr "Flag:"
|
||||
@@ -512,8 +513,7 @@ msgstr "Non consentito in questo contesto!"
|
||||
|
||||
msgid "Please, download files from receiver or setup your path for read data!"
|
||||
msgstr ""
|
||||
"Per favore, scarica i file dal ricevitore o imposta il percorso da cui"
|
||||
" leggere i dati!"
|
||||
"Scarica i file dal ricevitore o imposta il percorso da cui leggere i dati!"
|
||||
|
||||
msgid "Reading data error!"
|
||||
msgstr "Errore lettura dati!"
|
||||
@@ -528,7 +528,7 @@ msgid "The text of marker is empty, please try again!"
|
||||
msgstr "Il testo del marcatore è vuoto, riprova!"
|
||||
|
||||
msgid "Please, select only one item!"
|
||||
msgstr "Per favore, seleziona solo un elemento!"
|
||||
msgstr "Seleziona solo un elemento!"
|
||||
|
||||
msgid "No png file is selected!"
|
||||
msgstr "Nessun file png selezionato!"
|
||||
@@ -549,7 +549,7 @@ msgid "Done!"
|
||||
msgstr "Fatto!"
|
||||
|
||||
msgid "Please, wait..."
|
||||
msgstr "Attendere prego..."
|
||||
msgstr "Attendi..."
|
||||
|
||||
msgid "Resizing..."
|
||||
msgstr "Ridimensionamento..."
|
||||
@@ -561,10 +561,10 @@ msgid "No satellite is selected!"
|
||||
msgstr "Nessun satellite selezionato!"
|
||||
|
||||
msgid "Please, select only one satellite!"
|
||||
msgstr "Per favore, seleziona solo un satellite!"
|
||||
msgstr "Seleziona solo un satellite!"
|
||||
|
||||
msgid "Please check your parameters and try again."
|
||||
msgstr "Per favore, controlla i tuoi parametri e riprova di nuovo."
|
||||
msgstr "Controlla i parametri e riprova di nuovo."
|
||||
|
||||
msgid "No satellites.xml file is selected!"
|
||||
msgstr "Nessun file satellites.xml selezionato!"
|
||||
@@ -580,7 +580,7 @@ msgstr "VLC non trovato! Controlla che sia installato!"
|
||||
|
||||
# Search unavailable streams dialog
|
||||
msgid "Please wait, streams testing in progress..."
|
||||
msgstr "Attendere prego, test degli stream in corso..."
|
||||
msgstr "Test degli stream in corso..."
|
||||
|
||||
msgid "Found"
|
||||
msgstr "Trovato"
|
||||
@@ -592,7 +592,7 @@ msgid "No changes required!"
|
||||
msgstr "Non sono richiesti cambiamenti!"
|
||||
|
||||
msgid "This list does not contains IPTV streams!"
|
||||
msgstr "L'elenco non contiene stream IPTV!"
|
||||
msgstr "Questo elenco non contiene stream IPTV!"
|
||||
|
||||
msgid "New empty configuration"
|
||||
msgstr "Nuova configurazione vuota"
|
||||
@@ -728,7 +728,7 @@ msgid "XML file"
|
||||
msgstr "File XML"
|
||||
|
||||
msgid "Use web source"
|
||||
msgstr "Utilizza fonte web"
|
||||
msgstr "Usa fonte web"
|
||||
|
||||
msgid "Url to *.xml.gz file:"
|
||||
msgstr "Da URL a file *.xml.gz:"
|
||||
@@ -780,7 +780,7 @@ msgstr ""
|
||||
" bouquet!"
|
||||
|
||||
msgid "Use HTTP"
|
||||
msgstr "Utilizza HTTP"
|
||||
msgstr "Usa HTTP"
|
||||
|
||||
msgid "Close playback"
|
||||
msgstr "Ferma riproduzione"
|
||||
@@ -817,7 +817,7 @@ msgid "Language:"
|
||||
msgstr "Lingua:"
|
||||
|
||||
msgid "Load the last open configuration at program startup"
|
||||
msgstr "Carica l'ultima configurazione aperta all'avvio del programma"
|
||||
msgstr "Carica ultima configurazione aperta all'avvio del programma"
|
||||
|
||||
msgid "Enable direct playback bar"
|
||||
msgstr "Abilita barra di riproduzione diretta"
|
||||
@@ -962,7 +962,7 @@ msgstr ""
|
||||
" dell'elenco dei preferiti!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Funziona in modalità standby o transponder attivo corrente!"
|
||||
msgstr "Funziona in modalità standby o con il transponder attualmente attivo!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Nessuna connessione con il ricevitore!"
|
||||
@@ -1230,6 +1230,9 @@ msgstr "Dimensione"
|
||||
msgid "Date"
|
||||
msgstr "Data"
|
||||
|
||||
msgid "Attr."
|
||||
msgstr "Attr"
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Commuta posizione visualizzazione"
|
||||
|
||||
@@ -1258,7 +1261,7 @@ msgstr ""
|
||||
" picon!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Rilevati stream:"
|
||||
msgstr "Stream rilevati:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Scarica picon"
|
||||
@@ -1285,7 +1288,7 @@ msgid "Mark duplicates"
|
||||
msgstr "Contrassegna duplicati"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Scarica solo per i bouquet selezionati"
|
||||
msgstr "Carica solo per i bouquet selezionati"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "L'attività è stata annullata!"
|
||||
@@ -1303,13 +1306,13 @@ msgid "Help"
|
||||
msgstr "Aiuto"
|
||||
|
||||
msgid "HTTP API is not activated. Check your settings!"
|
||||
msgstr "API HTTP non attivata. Controlla le tue impostazioni!"
|
||||
msgstr "API HTTP non attivata. Controlla le impostazioni!"
|
||||
|
||||
msgid "Add picons"
|
||||
msgstr "Aggiungi picon"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Log"
|
||||
msgstr "Registri"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
@@ -1407,9 +1410,9 @@ msgstr "Tutti i bouquet"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Riproduci dall'elenco principale"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr ""
|
||||
"Abilita l'analisi degli URL utilizzando youtube-dl per ottenere collegamenti"
|
||||
"Abilita l'analisi degli URL usando yt-dlp per ottenere collegamenti"
|
||||
" diretti ai media."
|
||||
|
||||
msgid "Permissions..."
|
||||
@@ -1422,7 +1425,7 @@ msgid "EPG *.dat file:"
|
||||
msgstr "File EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Utilizza HTTP per ricaricare i dati nel ricevitore"
|
||||
msgstr "Usa HTTP per ricaricare i dati nel ricevitore"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Abilita compressione picon"
|
||||
@@ -1456,7 +1459,7 @@ msgid "Region"
|
||||
msgstr "Regione"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Provider"
|
||||
msgstr "Fornitore"
|
||||
|
||||
msgid ""
|
||||
"Enables upload as an archive if a large number of picon (> 1000) is"
|
||||
@@ -1492,3 +1495,74 @@ msgstr "Ora di fine"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Abilita supporto estensioni"
|
||||
|
||||
msgid ""
|
||||
"After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr ""
|
||||
"Dopo aver caricato le modifiche potrebbe essere necessario riavviare"
|
||||
" completamente il ricevitore!"
|
||||
|
||||
msgid "Remove duplicates"
|
||||
msgstr "Rimuovi duplicati"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Rimosso"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Consente di sovrascrivere i servizi esistenti dell'elenco principale."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Consente di saltare l'importazione dei servizi da lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Bouquet solo dati"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Crea bouquet categorie"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Crea bouquet regionali"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "Salta banda C"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Imposta automaticamente il nome selezionato nell'elenco dei bouquet."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Unisci i satelliti per posizione"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Salva la selezione dei satelliti"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Estrai link diretti"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Prefisso URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Prefisso non valido per l'URL specificata!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Titolo alternativo della finestra"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Tipo selezionato:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Gestore estensioni"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Installato"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Usa la cartella comune per i picon"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Attiva l'uso di una singola cartella per diversi profili."
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 Frank Neirynck
|
||||
# Copyright (C) 2018-2023 Frank Neirynck
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Frank Neirynck <frank@insink.be>, 2018-2022.
|
||||
@@ -1346,8 +1346,8 @@ msgstr "Alle boeketten"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Afspelen vanuit de hoofdlijst"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Schakelt URL-parsing met behulp van youtube-dl in, om directe links naar media te krijgen."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Schakelt URL-parsing met behulp van yt-dlp in, om directe links naar media te krijgen."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Rechten..."
|
||||
@@ -1401,3 +1401,87 @@ msgstr "Maakt uploaden als archief mogelijk als een groot aantal pictogrammen (>
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Opruimen \"Niew\" flag"
|
||||
|
||||
msgid "Group by"
|
||||
msgstr "Groeperen per"
|
||||
|
||||
msgid "Replace existing"
|
||||
msgstr "Vervang bestaande"
|
||||
|
||||
msgid "Already exists"
|
||||
msgstr "Bestaat reeds"
|
||||
|
||||
msgid "Enable unlimited copy buffer"
|
||||
msgstr "Onbeperkte kopieerbuffer inschakelen"
|
||||
|
||||
msgid "Enables unlimited copy buffer for the bouquets tab."
|
||||
msgstr "Schakelt een onbeperkte kopieerbuffer in voor het boeketten tabblad."
|
||||
|
||||
msgid "Start time"
|
||||
msgstr "Starttijd"
|
||||
|
||||
msgid "End time"
|
||||
msgstr "Eindtijd"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Ondersteuning voor extensies inschakelen"
|
||||
|
||||
msgid "After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr "Na het uploaden van de wijzigingen kan het nodig zijn de ontvanger volledig opnieuw op te starten!"
|
||||
|
||||
msgid "Remove duplicates"
|
||||
msgstr "Duplicaten verwijderen"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Verwijderd"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Maakt het overschrijven van bestaande hoofdlijst diensten mogelijk."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Maakt het overslaan van dienstenimport uit lamedb mogelijk."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Alleen gegevens over boeketten"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Maak categorie boeketten"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Maak regionale boeketten"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "C-band overslaan"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Stel automatisch de naam in die is geselecteerd in de boeketlijst."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Satellieten samenvoegen op basis van positie"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Satellietselectie opslaan"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Directe links uitpakken"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Prefix URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Ongeldige prefix voor de opgegeven URL!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Alternatieve venstertitel"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Geselecteerde type:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Uitbreidings Manager"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Geïnstalleerd"
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: lareq <lareq@lareq.eu>\n"
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: acypaczom\n"
|
||||
"Language-Team: \n"
|
||||
"Language: pl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr ""
|
||||
@@ -34,7 +34,7 @@ msgid "Picon"
|
||||
msgstr "Pikon"
|
||||
|
||||
msgid "Freq"
|
||||
msgstr "Freq"
|
||||
msgstr "Częst"
|
||||
|
||||
msgid "Rate"
|
||||
msgstr "Rate"
|
||||
@@ -453,7 +453,7 @@ msgid "Preferences"
|
||||
msgstr "Preferencje"
|
||||
|
||||
msgid "Profile:"
|
||||
msgstr "Profile:"
|
||||
msgstr "Profil:"
|
||||
|
||||
msgid "Timeout between commands in seconds"
|
||||
msgstr "Limit czasu między poleceniami w sekundach"
|
||||
@@ -1375,3 +1375,136 @@ msgstr "Wszystkie bukiety"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Odtwarzanie z listy głównej"
|
||||
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Włącza parsowanie adresów URL przy użyciu yt-dlp w celu uzyskania bezpośrednich linków do multimediów."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Uprawnienia..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "Wyświetl EPG na liście bukietów"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "Plik EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Użyj protokołu HTTP, aby ponownie załadować dane do odbiornika"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Włącz kompresję picon"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Interwał aktualizacji (sec):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Aktualizacja:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Codziennie"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Przypisz referencję"
|
||||
|
||||
msgid "Specify hostname or IP address"
|
||||
msgstr "Określ nazwę hosta lub adres IP"
|
||||
|
||||
msgid "Default selection"
|
||||
msgstr "Domyślny wybór"
|
||||
|
||||
msgid "Don't change power state"
|
||||
msgstr "Nie zmieniaj stanu zasilania"
|
||||
|
||||
msgid "Don't toggle standby mode when updating bouquets and services."
|
||||
msgstr "Nie przełączaj w tryb czuwania podczas aktualizowania bukietów i usług."
|
||||
|
||||
msgid "Region"
|
||||
msgstr "Region"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Dostawca"
|
||||
|
||||
msgid ""
|
||||
"Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr ""
|
||||
"Umożliwia przesyłanie jako archiwum, jeśli wybrano dużą liczbę ikon (> 1000).\n"
|
||||
" Zalecane tylko w przypadku posiadania pamięci zewnętrznej."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Wyczyść znacznik „Nowy”."
|
||||
|
||||
msgid "Group by"
|
||||
msgstr "Grupuj według"
|
||||
|
||||
msgid "Replace existing"
|
||||
msgstr "Zastąp istniejące"
|
||||
|
||||
msgid "Already exists"
|
||||
msgstr "Już istnieje"
|
||||
|
||||
msgid "Enable unlimited copy buffer"
|
||||
msgstr "Włącz nieograniczony bufor kopiowania"
|
||||
|
||||
msgid "Enables unlimited copy buffer for the bouquets tab."
|
||||
msgstr "Włącza nieograniczony bufor kopii dla zakładki bukiety."
|
||||
|
||||
msgid "Start time"
|
||||
msgstr "Czas początku"
|
||||
|
||||
msgid "End time"
|
||||
msgstr "Czas końca"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Włącz obsługę rozszerzeń"
|
||||
|
||||
msgid "After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr "Po załadowaniu zmian może być konieczne ponowne uruchomienie odbiornika!"
|
||||
|
||||
msgid "Remove duplicates"
|
||||
msgstr "Usuń duplikaty"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Usunięty"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "zezwól na nadpisanie istniejących usług głównych serwisów ."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Umożliwia pomijanie importu usług z lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Tylko dane dotyczące bukietów"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Utwórz bukiety kategorii"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Twórz regionalne bukiety"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "Pomiń pasmo C"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Ustaw automatycznie wybraną nazwę na liście bukietów."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Połącz satelity według pozycji"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Zapisz wybór satelit"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Wyodrębnij bezpośrednie linki"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Prefiks adresu URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Nieprawidłowy prefiks dla podanego adresu URL!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Alternatywny tytuł okna"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Wybrany typ:"
|
||||
|
||||
@@ -1010,3 +1010,476 @@ msgstr "Captura de pantalla"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Vidео"
|
||||
|
||||
msgid "The Neutrino has only experimental support. Not all features are supported!"
|
||||
msgstr "Neutrino имеет только экспериментальную поддержку. Поддерживаются не все функции!"
|
||||
|
||||
msgid "Enable experimental features"
|
||||
msgstr "Ativar funcionalidades experimentais"
|
||||
|
||||
msgid "Can't Playback!"
|
||||
msgstr "Não consigo reproduzir!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Ativar o modo escuro"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Extrair..."
|
||||
|
||||
msgid "Unsupported format!"
|
||||
msgstr "Formato não suportado!"
|
||||
|
||||
msgid "Combine with the current data?"
|
||||
msgstr "Combinar com os dados actuais?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "Importação de dados concluída!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Serviço atual"
|
||||
|
||||
msgid "Open folder"
|
||||
msgstr "Abrir pasta"
|
||||
|
||||
msgid "Open archive"
|
||||
msgstr "Abrir ficheiro"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Importar da Web"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Control"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Temporizadores"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Temporizador"
|
||||
|
||||
msgid "Add timer"
|
||||
msgstr "Adicionar temporizador"
|
||||
|
||||
msgid "Hr."
|
||||
msgstr "Hr."
|
||||
|
||||
msgid "Min."
|
||||
msgstr "Min."
|
||||
|
||||
msgid "Power"
|
||||
msgstr "Ligar"
|
||||
|
||||
msgid "Standby"
|
||||
msgstr "Em espera"
|
||||
|
||||
msgid "Wake Up"
|
||||
msgstr "Despertar"
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Reinicialização"
|
||||
|
||||
msgid "Restart GUI"
|
||||
msgstr "Reiniciar a GUI"
|
||||
|
||||
msgid "Shutdown"
|
||||
msgstr "Desativação"
|
||||
|
||||
msgid "Shut down"
|
||||
msgstr "Desativar"
|
||||
|
||||
msgid "Do Nothing"
|
||||
msgstr "Não fazer nada"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Captura de ecrã"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Ativado:"
|
||||
|
||||
msgid "Name:"
|
||||
msgstr "Nome:"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "Descrição:"
|
||||
|
||||
msgid "Service:"
|
||||
msgstr "Serviço:"
|
||||
|
||||
msgid "Service reference:"
|
||||
msgstr "Referência do serviço:"
|
||||
|
||||
msgid "Event ID:"
|
||||
msgstr "ID do evento:"
|
||||
|
||||
msgid "Begins:"
|
||||
msgstr "Começa:"
|
||||
|
||||
msgid "Ends:"
|
||||
msgstr "Termina:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Repetido:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Ação:"
|
||||
|
||||
msgid "After event:"
|
||||
msgstr "Após o evento:"
|
||||
|
||||
msgid "Location:"
|
||||
msgstr "Localização:"
|
||||
|
||||
msgid "Mo"
|
||||
msgstr "Seg"
|
||||
|
||||
msgid "Tu"
|
||||
msgstr "Ter"
|
||||
|
||||
msgid "We"
|
||||
msgstr "Qua"
|
||||
|
||||
msgid "Th"
|
||||
msgstr "Qui"
|
||||
|
||||
msgid "Fr"
|
||||
msgstr "Sex"
|
||||
|
||||
msgid "Sa"
|
||||
msgstr "Sáb"
|
||||
|
||||
msgid "Dom"
|
||||
msgstr "Вс"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Set"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Atualização de serviços"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Criar pasta"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "Cliente FTP"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "O ficheiro é demasiado grande!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Ligar"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Desligar"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Tamanho"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Data"
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Alternar a posição de visualização"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternativas"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Adicionar alternativas"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "Apenas DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "Um serviço semelhante já consta desta lista!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "O modo de reprodução foi alterado!\nReinicie o programa para aplicar as definições."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Definir valores para TID, NID e Namespace para nomear corretamente os picons!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Fluxos detectados:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Descarregar picons"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Erros:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Utilizar para reproduzir fluxos:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Fonte nas listas:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Tamanho dos picões nas listas:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Tamanho do logótipo nas dicas de ferramenta:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Salvar como"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Marcar duplicações"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Carregar apenas para o bouquet selecionado"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "A tarefa foi cancelada!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Carregamento de dados em curso!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Gravações"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Gravações:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Ajuda"
|
||||
|
||||
msgid "HTTP API is not activated. Check your settings!"
|
||||
msgstr "A API HTTP não está activada. Verifique as suas definições!"
|
||||
|
||||
msgid "Add picons"
|
||||
msgstr "Adicionar picons"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Registos"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Tempo"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Duração"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Fonte complementar"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Definir automaticamente o nome selecionado na lista de favoritos."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Reprodução"
|
||||
|
||||
msgid "Playback:"
|
||||
msgstr "Reprodução:"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Áudio"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Pista áudio"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Legenda"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Faixa de legenda"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "Rácio de aspeto"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "Isto pode alterar as definições de outros perfis!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Arraste os serviços para o picon pretendido ou picon para a lista de serviços seleccionados."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Define a pasta de perfil como predefinida para armazenar picons, cópias de segurança, etc."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Novo sub-bouquet"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Marca não apresentada em bouquets"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "Não em bouquets"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "Não mostrar serviços presentes em bouquets."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Apenas serviços IPTV"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Mostrar picons"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Esquema alternativo"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "O esquema dos elementos foi alterado!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Reinicie o programa para aplicar todas as alterações."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Novo ficheiro"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Renomear"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Marcadores"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Adicionar marcador"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Todos os bouquets"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Reprodução da lista principal"
|
||||
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Permite a análise de URLs usando yt-dlp para obter links diretos para mídia."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Permissões..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "Apresentar EPG na lista de bouquet"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "Ficheiro EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Utilizar HTTP para recarregar dados no recetor"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Ativar a compressão de picons"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Intervalo de atualização (seg):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Atualização:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Diário"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Atribuir referência"
|
||||
|
||||
msgid "Specify hostname or IP address"
|
||||
msgstr "Especificar o nome do anfitrião ou o endereço IP"
|
||||
|
||||
msgid "Default selection"
|
||||
msgstr "Seleção predefinida"
|
||||
|
||||
msgid "Don't change power state"
|
||||
msgstr "Não alterar o estado de alimentação"
|
||||
|
||||
msgid "Don't toggle standby mode when updating bouquets and services."
|
||||
msgstr "Não alterne o modo de espera ao atualizar bouquets e serviços."
|
||||
|
||||
msgid "Region"
|
||||
msgstr "Região"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Provedor"
|
||||
|
||||
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr "Permite o carregamento como um arquivo se for selecionado um grande número de picons (> 1000).\n"
|
||||
" Recomendado apenas se tiver armazenamento externo."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Apagar a bandeira \"Novo\""
|
||||
|
||||
msgid "Group by"
|
||||
msgstr "Agrupar por"
|
||||
|
||||
msgid "Replace existing"
|
||||
msgstr "Substituir o existente"
|
||||
|
||||
msgid "Already exists"
|
||||
msgstr "Já existe"
|
||||
|
||||
msgid "Enable unlimited copy buffer"
|
||||
msgstr "Ativar o buffer de cópia ilimitado"
|
||||
|
||||
msgid "Enables unlimited copy buffer for the bouquets tab."
|
||||
msgstr "Ativa a memória intermédia de cópia ilimitada para o separador bouquets."
|
||||
|
||||
msgid "Start time"
|
||||
msgstr "Hora de início"
|
||||
|
||||
msgid "End time"
|
||||
msgstr "Hora de fim"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Ativar o suporte de extensões"
|
||||
|
||||
msgid "After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr "Depois de carregar as alterações, poderá ser necessário reiniciar completamente o recetor!"
|
||||
|
||||
msgid "Remove duplicates"
|
||||
msgstr "Eliminar duplicados"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Removido"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Permite substituir os serviços da lista principal existentes."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Permite saltar a importação de serviços de lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Apenas dados de bouquets"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Criar bouquets de categoria"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Criar bouquets regionais"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "Saltar a banda C"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Definir automaticamente o nome selecionado na lista de bouquets."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Unir satélites por posições"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Guardar a seleção de satélite"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Extrair ligações directas"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Prefixo URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Prefixo inválido para o URL indicado!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Título alternativo da janela"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Tipo selecionado:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Gestor de extensões"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Instalado"
|
||||
|
||||
@@ -1355,8 +1355,8 @@ msgstr "Все букеты"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Воспроизведение из основного списка"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Включает анализ URL-адресов с помощью youtube-dl для получения прямых ссылок на медиа."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Включает анализ URL-адресов с помощью yt-dlp для получения прямых ссылок на медиа."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Разрешения..."
|
||||
@@ -1443,3 +1443,60 @@ msgstr "Удалить дубликаты"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Удалено"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Включает перезапись существующих сервисов основного списка."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Включает пропуск импорта сервисов из lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Только данные букетов"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Создать букеты по категориям"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Создать букеты по регионам"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "Пропустить C-диапазон"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Автоматическая установка имени, выбранного в списке букетов."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Объединять спутники по позициям"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Сохранять выбор спутников"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Извлечь прямые ссылки"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "Префикс URL:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Недопустимый префикс для данного URL!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Альтернативный заголовок окна"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Выбран тип:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Менеджер расширений"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Вер."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Установлено"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Использовать общую папку для пиконов"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Активирует использование одной папки для нескольких профилей."
|
||||
|
||||
@@ -3,15 +3,15 @@ msgstr ""
|
||||
"Project-Id-Version: DemonEditor\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
|
||||
"PO-Revision-Date: 2023-02-23 14:22+0300\n"
|
||||
"PO-Revision-Date: 2023-10-08 13:43+0300\n"
|
||||
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language-Team: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language: tr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 3.2.2\n"
|
||||
"X-Generator: Poedit 3.3.2\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "audi06_19 <info@dreamosat-forum.com>"
|
||||
@@ -1387,8 +1387,8 @@ msgstr "Tüm buketler"
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Ana listeden oynatma"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Medyaya doğrudan bağlantılar almak için youtube-dl kullanarak URL ayrıştırmayı etkinleştirir."
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr "Medyaya doğrudan bağlantılar almak için yt-dlp kullanarak URL ayrıştırmayı etkinleştirir."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "İzinler..."
|
||||
@@ -1477,3 +1477,60 @@ msgstr "Kopyaları kaldır"
|
||||
|
||||
msgid "Removed"
|
||||
msgstr "Kaldırıldı"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Mevcut ana liste hizmetlerinin üzerine yazılmasını sağlar."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Lamedb'den içe aktarılan hizmetlerin atlanmasını sağlar."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Yalnızca buket verileri"
|
||||
|
||||
msgid "Create Category bouquets"
|
||||
msgstr "Kategori buketleri oluştur"
|
||||
|
||||
msgid "Create Regional bouquets"
|
||||
msgstr "Bölgesel buketler oluşturun"
|
||||
|
||||
msgid "Skip C-band"
|
||||
msgstr "C-band atla"
|
||||
|
||||
msgid "Automatically set the name selected in the bouquet list."
|
||||
msgstr "Buket listesinde seçilen isimleri otomatik olarak ayarlayın."
|
||||
|
||||
msgid "Merge satellites by positions"
|
||||
msgstr "Uyduları konumlara göre birleştir"
|
||||
|
||||
msgid "Save satellite selection"
|
||||
msgstr "Uydu seçimini kaydet"
|
||||
|
||||
msgid "Extract direct links"
|
||||
msgstr "Doğrudan bağlantıları ayıklayın"
|
||||
|
||||
msgid "URL prefix:"
|
||||
msgstr "URL öneki:"
|
||||
|
||||
msgid "Invalid prefix for the given URL!"
|
||||
msgstr "Belirtilen URL için geçersiz önek!"
|
||||
|
||||
msgid "Alternate window title"
|
||||
msgstr "Alternatif pencere başlığı"
|
||||
|
||||
msgid "Selected type:"
|
||||
msgstr "Seçilen tip:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Eklenti Yöneticisi"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Yüklenmiş"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Piconlar için ortak klasörü kullan"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Birden fazla profil için tek klasör kullanımını etkinleştirir."
|
||||
|
||||
Reference in New Issue
Block a user