Compare commits

...

157 Commits

Author SHA1 Message Date
DYefremov
43bf5ac44b bump version 2023-04-25 20:53:42 +03:00
DYefremov
e714b10431 changed columns for filtering 2023-04-25 20:52:44 +03:00
DYefremov
f0d0813e75 refactoring of getting picon pixbuf 2023-04-25 01:04:38 +03:00
DYefremov
9dc4df73c4 added piconSNPblack logo 2023-04-25 00:32:44 +03:00
DYefremov
f2da1e4cd4 minor refactoring of player 2023-04-25 00:12:29 +03:00
DYefremov
3d4588833b vlc module update 2023-04-24 22:49:26 +03:00
DYefremov
4cf19e5413 minor code adjustment
* Preventing mixins [str -> Enum] bug in Python 3.11 [100458]
2023-04-22 18:33:18 +03:00
audi06_19
cd4a814838 Turkish translation update (#176) 2023-04-22 17:31:42 +03:00
DYefremov
d7c49f50f2 bump version 2023-04-19 14:07:53 +03:00
DYefremov
b61b8e16fa small fix for Web import options init 2023-04-19 09:56:17 +03:00
DYefremov
56d2a3e991 fix dir copy for FTP tab 2023-04-17 23:16:00 +03:00
DYefremov
b65ea9c0d3 vlc module update 2023-04-17 13:53:46 +03:00
DYefremov
a62ee8f378 extension API improvement 2023-04-15 16:49:20 +03:00
DYefremov
0db8ee6d47 prevent focus lack for main views 2023-04-14 10:48:37 +03:00
DYefremov
ed41b01f63 it *.mo file update 2023-04-13 10:27:24 +03:00
mapi68
c2eaecb8b8 Italian translation update (#174) 2023-04-13 10:20:26 +03:00
DYefremov
f5313f2c40 minor translations update 2023-04-12 23:40:50 +03:00
DYefremov
c6a0b80fdd bouquets gen improvement 2023-04-12 23:30:07 +03:00
DYefremov
7813aeb059 reverted lowercase for some prefixes (#170) 2023-04-12 09:34:50 +03:00
DYefremov
3a0e5c09a1 it *.mo file update 2023-04-09 10:42:48 +03:00
mapi68
ab6a44dc3f Italian translation update (#173) 2023-04-09 10:38:27 +03:00
audi06_19
caefb4587d Update: Turkish translation update (#172) 2023-04-09 10:37:42 +03:00
DYefremov
93ff78d7ce version update 2023-04-08 14:32:00 +03:00
DYefremov
1d583ecd99 fix root bouquet rename 2023-04-08 14:24:12 +03:00
DYefremov
d4914ac451 Russian, Belarusian and German translations update 2023-04-08 00:21:16 +03:00
DYefremov
6207b6a10d Spanish and Dutch translations update 2023-04-07 20:51:13 +03:00
DYefremov
1eb8fe621d save satellites selection for web import 2023-04-07 19:01:16 +03:00
DYefremov
79b41b1661 minor refactoring of settings save 2023-04-07 18:37:40 +03:00
DYefremov
7a36ba8148 minor fix 2023-04-07 13:22:39 +03:00
DYefremov
08e970fc96 save source for web import 2023-04-07 08:42:04 +03:00
DYefremov
9681fcbc79 saving settings for web import 2023-04-07 00:27:55 +03:00
DYefremov
7b002b208f added additional yt link checking (#170) 2023-04-06 14:55:30 +03:00
DYefremov
95a1732f01 satellites merge support for web import (#165) 2023-04-06 00:24:45 +03:00
DYefremov
57e4fdff7f changed case for yt links prefixes (#170) 2023-04-04 22:37:06 +03:00
DYefremov
89c456993f allowed to rename root bouquet 2023-04-03 09:12:18 +03:00
DYefremov
2f3fc31023 minor fix 2023-04-02 12:49:00 +03:00
DYefremov
5c18e49cf7 added 'No' prefix for yt links (#170) 2023-04-02 12:42:59 +03:00
DYefremov
64530bcb85 prefix support for yt playlist import (#170) 2023-03-30 18:57:01 +03:00
DYefremov
4d472609b4 url prefix elems for yt dialog 2023-03-30 00:13:25 +03:00
DYefremov
640b995ab8 prefix support for yt links (#170) 2023-03-27 00:01:08 +03:00
DYefremov
42980a988f minor ui changes for IPTV dialogs 2023-03-26 18:18:32 +03:00
DYefremov
64c5f28957 enabled quality change for yt links 2023-03-26 16:22:12 +03:00
DYefremov
a32bf230cf fix yt link double check on edit (#170) 2023-03-25 16:04:11 +03:00
audi06_19
b8cac728a8 Turkish translation update (#166) 2023-03-12 16:28:55 +03:00
DYefremov
079f07cfd2 mpv module update 2023-03-09 08:47:57 +03:00
DYefremov
9a5884cc9a it *.mo file update 2023-03-06 16:49:55 +03:00
mapi68
115237a10f Italian translation update (#163) 2023-03-06 16:46:29 +03:00
DYefremov
994bd0ee1c Russian, Belarusian and German translations update 2023-03-05 14:01:02 +03:00
DYefremov
27e5b373a3 version update 2023-03-04 20:43:15 +03:00
DYefremov
43c05b1739 fix namespace for web import 2023-03-04 20:00:13 +03:00
DYefremov
bd96c286e9 option skip c-band for web import dialog 2023-03-04 18:15:16 +03:00
DYefremov
1bded41eab close options button for import dialog 2023-03-04 16:00:41 +03:00
DYefremov
5f0f51679c sat list init on update dialog start 2023-03-04 12:28:10 +03:00
DYefremov
380bb3150b updated it *.mo file 2023-03-04 11:15:23 +03:00
mapi68
d1a7a486a2 Italian translation update (#162) 2023-03-04 11:07:52 +03:00
DYefremov
1be167bec3 sorting by pos on bouquets generation 2023-03-04 00:27:37 +03:00
DYefremov
dd3e88589c Russian, Belarusian and German translations update 2023-03-03 17:38:29 +03:00
DYefremov
c5a2df6d7d prevent duplicate for web import 2023-03-03 12:04:04 +03:00
DYefremov
c9fc3803c7 fix FEC value for web import 2023-03-03 10:49:06 +03:00
DYefremov
6afd518cfc fix adding duplicates to the main list 2023-03-03 10:36:46 +03:00
DYefremov
02a51c9b56 bouquets generation for kos web source 2023-03-02 17:50:31 +03:00
DYefremov
c96cfa0e1b category and lang for kingofsat 2023-03-01 23:02:11 +03:00
DYefremov
08bc4ff4c4 bouquets only option for import dialog 2023-03-01 13:36:20 +03:00
DYefremov
ae2b78e990 README update 2023-02-28 00:23:00 +03:00
DYefremov
88be9fe49c updated it *.mo file 2023-02-27 23:48:01 +03:00
mapi68
9dae9b7219 Italian translation update (#161) 2023-02-27 23:42:58 +03:00
DYefremov
f296a6c90b paths normalization in settings 2023-02-27 23:40:41 +03:00
DYefremov
0486776d83 minor fix 2023-02-27 10:31:20 +03:00
DYefremov
3e6146d825 .gitignore update 2023-02-23 16:31:03 +03:00
audi06_19
9b97341e70 add .gitignore and Turkish translation update (#160)
* add .gitignore

* Update: Turkish translation update
2023-02-23 14:36:34 +03:00
DYefremov
41714136e6 README update 2023-02-23 14:31:56 +03:00
DYefremov
f781cbb9f6 bump version 2023-02-23 11:05:50 +03:00
DYefremov
177be7679b added version for extensions 2023-02-22 13:43:24 +03:00
DYefremov
a65914a48c duplicate removal support for the fav list (#159) 2023-02-22 11:55:34 +03:00
DYefremov
e3ffc2e24b playback fix for empty list 2023-02-22 11:46:11 +03:00
DYefremov
79415c69c5 minor refactoring 2023-02-21 15:39:22 +03:00
DYefremov
54c4e02cee bump version 2023-02-18 21:15:50 +03:00
DYefremov
f580c5d83c added header bar option on macOS 2023-02-18 18:35:09 +03:00
DYefremov
e0cbdb2f8d header bar option refactoring 2023-02-18 11:30:06 +03:00
DYefremov
839855c076 reworking of the program options tab 2023-02-18 09:37:05 +03:00
audi06_19
87db39590b Update: Turkish translation update (#158) 2023-02-18 08:28:41 +03:00
DYefremov
ebe58903e5 corrected service type setting for IPTV 2023-02-17 21:51:35 +03:00
DYefremov
ff8d4e5321 fix ref assignment from XML source (#154) 2023-02-17 20:17:17 +03:00
DYefremov
907415a2c9 added path to extensions 2023-02-17 13:15:12 +03:00
DYefremov
f9afffbdb3 *.spec file correction 2023-02-17 11:25:32 +03:00
DYefremov
326856b1e3 added config for extensions 2023-02-17 00:37:43 +03:00
DYefremov
865a326fe9 minor translations update 2023-02-16 20:05:27 +03:00
DYefremov
dd2661c6c9 picons location corrections (#157) 2023-02-16 19:36:15 +03:00
DYefremov
623e5a17f5 small fix to set bouquet lock 2023-02-16 16:29:08 +03:00
DYefremov
18c1fa736b display channel id as tooltip for EPG dialog (#154) 2023-02-16 14:42:20 +03:00
DYefremov
0e6142c751 parental lock and hiding for bouquets 2023-02-15 22:41:37 +03:00
DYefremov
c274c265c6 fix set lock for bouquet 2023-02-15 21:59:30 +03:00
DYefremov
777c09c9b8 storing parental lock for bouquets 2023-02-15 14:17:25 +03:00
DYefremov
12a68a4dbb displaying parental lock for bouquets 2023-02-15 13:04:54 +03:00
DYefremov
eab869d4d5 hidden marker for bouquets 2023-02-15 12:34:06 +03:00
DYefremov
aec76eec45 filtering by sat position for XML source (#154) 2023-02-14 17:36:43 +03:00
DYefremov
bdab316ba7 small resizing fix 2023-02-14 10:55:11 +03:00
DYefremov
771ecb696f updated it *.mo file 2023-02-12 14:43:15 +03:00
mapi68
8993fbed5d Italian translation update (#156) 2023-02-12 14:36:32 +03:00
DYefremov
f3cad81a7d build-deb file update 2023-02-11 13:06:13 +03:00
DYefremov
c1cf343f69 logging support for extensions 2023-02-11 12:32:33 +03:00
DYefremov
f998f66a35 version update 2023-02-11 10:09:46 +03:00
DYefremov
9eb4cdc574 option to enable extensions 2023-02-11 10:04:04 +03:00
DYefremov
20120e0db4 README for extensions 2023-02-10 17:42:54 +03:00
DYefremov
3446bb225c moved extensions module 2023-02-10 14:04:42 +03:00
DYefremov
191975bd14 extension as singleton 2023-02-10 11:59:14 +03:00
DYefremov
f4be52a202 basic extensions API 2023-02-09 23:51:14 +03:00
DYefremov
ba2272cf13 updated tr *.mo file 2023-02-06 21:49:56 +03:00
audi06_19
7bd3fcd9a6 Turkish translation update (#155) 2023-02-06 21:46:10 +03:00
DYefremov
e54719ca2c bump version 2023-02-04 12:37:16 +03:00
DYefremov
0c1c44c866 columns resizing for EPG tab 2023-02-04 11:03:12 +03:00
DYefremov
06b82251ef duration format correction for win 2023-02-02 00:09:51 +03:00
DYefremov
fb929ec723 playback pause on mouse click 2023-02-01 13:24:51 +03:00
DYefremov
fd1c1bfd6e layout init correction 2023-01-31 12:45:26 +03:00
DYefremov
c48a08b239 header bar for yt dialog 2023-01-30 17:36:22 +03:00
DYefremov
b647b0a338 delayed cache init 2023-01-30 14:41:16 +03:00
DYefremov
9b608eeb74 added custom header bar widget 2023-01-29 12:59:57 +03:00
DYefremov
07e55b3f1e header bar activation for macOS 2023-01-29 00:33:59 +03:00
DYefremov
7e639f5637 header bar for update dialog 2023-01-29 00:26:28 +03:00
DYefremov
5570d47cae header bar activation for macOS 2023-01-28 21:45:01 +03:00
DYefremov
5c49c0d123 refs assignment from filtered list only (#154) 2023-01-28 17:55:19 +03:00
DYefremov
ee6dd511b5 clearing EPG on profile change 2023-01-28 14:43:13 +03:00
DYefremov
1f847233b3 copyright update 2023-01-28 14:03:04 +03:00
DYefremov
835e1af8e4 README update 2023-01-28 14:00:22 +03:00
DYefremov
50e0d8b66a time display correction for XMLTV 2023-01-28 13:53:15 +03:00
DYefremov
fb0789664a version update 2023-01-27 22:41:49 +03:00
DYefremov
5dd39492f2 updated it *.mo file 2023-01-27 22:30:55 +03:00
mapi68
33be9f21a2 Italian translation update (#153) 2023-01-27 22:16:45 +03:00
DYefremov
7acc9ae74f Russian, Belarusian and German translations update 2023-01-27 17:04:18 +03:00
DYefremov
d492022232 some ui corrections 2023-01-27 13:44:41 +03:00
DYefremov
b034995130 added unlimited buffer option 2023-01-26 22:06:29 +03:00
DYefremov
f037b3554d disabled fixed height for picon views 2023-01-26 20:30:20 +03:00
DYefremov
7f1f27da57 minor adjustment 2023-01-26 00:53:28 +03:00
DYefremov
d7f3afecb0 increased EPG tab columns width (#150) 2023-01-26 00:17:38 +03:00
DYefremov
f309005c52 additional time columns for EPG tab (#150) 2023-01-25 17:54:09 +03:00
DYefremov
25661816e7 'replace existing' for bouquets tab 2023-01-25 01:00:16 +03:00
DYefremov
a2652cef4b fix update EPG from alt service 2023-01-25 00:29:23 +03:00
DYefremov
adbc9ad322 option 'replace existing' for import dialog (#127) 2023-01-24 23:02:28 +03:00
DYefremov
2dc8611294 bump version 2023-01-21 16:27:08 +03:00
DYefremov
72bfd21056 fix input for settings dialog 2023-01-21 15:49:18 +03:00
DYefremov
65ef018f81 modifiers fix for yt dialog 2023-01-21 15:47:47 +03:00
DYefremov
1236c5ebc9 filtering by satellite position for the EPG dialog (#148) 2023-01-21 15:06:27 +03:00
DYefremov
f0011ebcf2 fixed copy/paste refs from some satellites (#146) 2023-01-20 12:17:59 +03:00
DYefremov
392e94e7ba bump version 2023-01-19 22:05:40 +03:00
DYefremov
c6de18271d setting EPG path active by default 2023-01-19 21:57:52 +03:00
DYefremov
71a65242c1 ui correction for EPG tab 2023-01-19 21:04:53 +03:00
DYefremov
4efc956870 minor ui correction for remote control 2023-01-19 16:02:11 +03:00
DYefremov
a605fdd545 minor ui corrections 2023-01-18 18:52:54 +03:00
DYefremov
285c1cae69 minor style changes 2023-01-14 23:02:24 +03:00
DYefremov
2e937a42a3 updated tr *.mo file 2023-01-14 00:30:46 +03:00
audi06_19
e208cf4656 Turkish translation update (#147) 2023-01-14 00:26:01 +03:00
DYefremov
3db82e3e18 don't hide filter popups after items selection (#145) 2023-01-13 16:11:57 +03:00
DYefremov
38e9a85694 remote control improvement 2023-01-12 21:46:04 +03:00
DYefremov
438e9c10d4 corrected reference assignment for IPTV services (#146) 2023-01-08 01:59:46 +03:00
DYefremov
920fa01159 bump version 2023-01-06 12:00:46 +03:00
DYefremov
d2787364cd fixed *.xml files deletion skip when saving data 2023-01-06 11:24:10 +03:00
DYefremov
839c0fae23 compressed picons path correction 2023-01-06 11:01:39 +03:00
74 changed files with 7591 additions and 5008 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.pyc
*.pyo
*__pycache__
.idea

View File

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

View File

@@ -26,12 +26,13 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
[<img src="https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png)
* Ability to view EPG and manage timers (via HTTP API).
* Simple FTP client (experimental).
[<img src="https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png)
[<img src="https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png)
**To increase program functionality you can use [extensions](https://github.com/DYefremov/demoneditor-extensions).**
#### Keyboard shortcuts
* **Ctrl + X** - only in bouquet list.
* **Ctrl + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + C** - only in services list.
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
beginning or inserts (creates) a new bouquet.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
@@ -109,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.
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.

View 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
@@ -35,6 +35,7 @@ import xml.etree.ElementTree as ETree
from enum import Enum
from ftplib import FTP, CRLF, Error, all_errors
from http.client import RemoteDisconnected
from pathlib import Path
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, quote
@@ -89,11 +90,11 @@ class UtfFTP(FTP):
while 1:
line = fp.readline(self.maxline + 1)
if len(line) > self.maxline:
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
msg = f"UtfFTP [retrlines] error: got more than {self.maxline} bytes"
log(msg)
raise Error(msg)
if self.debugging > 2:
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
log(f"UtfFTP [retrlines] *retr* {repr(line)}")
if not line:
break
if line[-2:] == CRLF:
@@ -112,9 +113,8 @@ class UtfFTP(FTP):
def download_file(self, name, save_path, callback=None):
with open(save_path + name, "wb") as f:
msg = "Downloading file: {}. Status: {}"
resp = self.download_binary(name, f)
msg = msg.format(name, resp)
msg = f"Downloading file: {name}. Status: {resp}"
callback(msg) if callback else log(msg.rstrip())
return resp
@@ -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:
@@ -470,7 +474,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
z_name = "picons.zip"
zip_file = f"{p_src}{z_name}"
p_dst = os.path.abspath(os.path.join(p_dst, os.pardir))
p_dst = Path(p_dst).parent.as_posix()
if files_filter and z_name in files_filter:
files_filter.remove(z_name)
@@ -648,8 +652,21 @@ 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"
TWO = "3"
THREE = "4"
FOUR = "5"
FIVE = "6"
SIX = "7"
SEVEN = "8"
EIGHT = "9"
NINE = "10"
ZERO = "11"
UP = "103"
LEFT = "105"
RIGHT = "106"
@@ -658,6 +675,7 @@ class HttpAPI:
EXIT = "174"
OK = "352"
INFO = "358"
EPG = "365"
TV = "377"
RADIO = "385"
AUDIO = "392"
@@ -668,8 +686,12 @@ class HttpAPI:
BLUE = "401"
CH_UP = "402"
CH_DOWN = "403"
NEXT = "407"
BACK = "412"
def __str__(self):
return self.value
class Power(str, Enum):
""" Args for HttpRequestType [POWER] class. """
TOGGLE_STANDBY = "0"
@@ -679,6 +701,9 @@ class HttpAPI:
WAKEUP = "4"
STANDBY = "5"
def __str__(self):
return self.value
PARAM_REQUESTS = {Request.REMOTE,
Request.POWER,
Request.VOL,

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 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
@@ -68,10 +68,9 @@ def write_bouquet(path, bq, s_type):
write_bouquet(path, bq)
@run_task
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
def write_bouquets(path, bouquets, s_type, force_bq_names=False, blacklist=None):
if s_type is SettingsType.ENIGMA_2:
BouquetsWriter(path, bouquets, force_bq_names).write()
BouquetsWriter(path, bouquets, force_bq_names, blacklist).write()
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)

View 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
@@ -47,16 +47,19 @@ 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:7:{}: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 "userbouquet.{}.{}" 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'
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
def __init__(self, path, bouquets, force_bq_names=False):
def __init__(self, path, bouquets, force_bq_names=False, blacklist=None):
self._path = path
self._bouquets = bouquets
self._force_bq_names = force_bq_names
self._black_list = set() if blacklist is None else blacklist
self._marker_index = 1
self._space_index = 0
self._alt_names = set()
@@ -96,7 +99,13 @@ class BouquetsWriter:
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
else:
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
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)
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))
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(line)
@@ -156,15 +165,19 @@ class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832" # Hidden marker.
SPACE = "832"
ALT = "134" # Alternatives.
UDP = "256"
HIDDEN = "519" # Skip, hide.
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
def __str__(self):
return self.value
class BouquetsReader:
""" Class for reading and parsing bouquets. """
@@ -197,6 +210,8 @@ class BouquetsReader:
for line in file.readlines():
if "#SERVICE" in line:
name = 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 b_name in b_names:
@@ -211,11 +226,14 @@ class BouquetsReader:
rb_name = f"{rb_name} {real_b_names[rb_name]}"
else:
real_b_names[rb_name] = 0
# Locked, hidden.
s_data[:2] = "10"
locked = ":".join(s_data).rstrip()
hidden = s_type is ServiceType.HIDDEN
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, b_name))
else:
s_data = line.split(":")
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
if len(s_data) == 12 and s_type is ServiceType.MARKER:
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
else:

View File

@@ -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
@@ -220,8 +221,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}")

View 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
@@ -38,7 +38,7 @@ from app.ui.uicommons import IPTV_ICON
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION {}\n"
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION {}\n"
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
@@ -153,12 +153,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)

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 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
@@ -31,7 +31,6 @@ import os
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
from app.ui.uicommons import LOCKED_ICON, HIDE_ICON
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType
_FILE = "bouquets.xml"
@@ -72,8 +71,8 @@ def parse_bouquets(file, name, bq_type):
bouquets[2].append(Bouquet(name=bq_name,
type=bq_type,
services=services,
locked=LOCKED_ICON if locked == "1" else None,
hidden=HIDE_ICON if hidden == "1" else None,
locked=locked == "1",
hidden=hidden == "1",
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
if BqType(bq_type) is BqType.BOUQUET:

View 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

View 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
@@ -48,6 +48,8 @@ IS_DARWIN = sys.platform == "darwin"
IS_WIN = sys.platform == "win32"
IS_LINUX = sys.platform == "linux"
USE_HEADER_BAR = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
class Defaults(Enum):
""" Default program settings """
@@ -67,7 +69,9 @@ class Defaults(Enum):
"/media/hdd/picon/",
"/media/usb/picon/",
"/media/mmc/picon/",
"/media/cf/picon/")
"/media/cf/picon/",
"/hdd/picon/",
"/usb/picon/")
# Neutrino.
NEUTRINO_BOX_SERVICES_PATH = "/var/tuxbox/config/zapit/"
NEUTRINO_BOX_SATELLITE_PATH = "/var/tuxbox/config/"
@@ -81,6 +85,8 @@ class Defaults(Enum):
BACKUP_BEFORE_DOWNLOADING = True
BACKUP_BEFORE_SAVE = True
V5_SUPPORT = False
UNLIMITED_COPY_BUFFER = False
EXTENSIONS_SUPPORT = False
FORCE_BQ_NAMES = False
HTTP_API_SUPPORT = True
ENABLE_YT_DL = False
@@ -423,7 +429,7 @@ class Settings:
@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):
@@ -431,7 +437,7 @@ class Settings:
@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):
@@ -439,7 +445,7 @@ class Settings:
@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):
@@ -475,7 +481,7 @@ class Settings:
@recordings_path.setter
def recordings_path(self, value):
self._settings["recordings_path"] = value
self._settings["recordings_path"] = Settings.normalize_path(value)
# ******** Streaming ********* #
@@ -606,6 +612,22 @@ class Settings:
def v5_support(self, value):
self._settings["v5_support"] = value
@property
def unlimited_copy_buffer(self):
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER.value)
@unlimited_copy_buffer.setter
def unlimited_copy_buffer(self, value):
self._settings["unlimited_copy_buffer"] = value
@property
def extensions_support(self):
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT.value)
@extensions_support.setter
def extensions_support(self, value):
self._settings["extensions_support"] = value
@property
def force_bq_names(self):
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
@@ -682,6 +704,14 @@ class Settings:
# *********** Appearance *********** #
@property
def use_header_bar(self):
return self._settings.get("use_header_bar", USE_HEADER_BAR)
@use_header_bar.setter
def use_header_bar(self, value):
self._settings["use_header_bar"] = value
@property
def list_font(self):
return self._settings.get("list_font", "")
@@ -873,13 +903,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)
@@ -911,10 +942,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__":

View 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
@@ -29,15 +29,16 @@
""" Module for working with epg.dat file. """
import abc
import os
import re
import shutil
import struct
import sys
import xml.etree.ElementTree as ET
from collections import namedtuple
from datetime import datetime, timezone
from tempfile import NamedTemporaryFile
from urllib.parse import urlparse
from xml.dom.minidom import parse, Node, Document
import xml.etree.ElementTree as ET
import requests
@@ -54,8 +55,8 @@ except ModuleNotFoundError:
else:
DETECT_ENCODING = True
EpgEvent = namedtuple("EpgEvent", ["service_name", "title", "time", "desc", "event_data"])
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", "N/A", None) # For Python3 < 3.7
EpgEvent = namedtuple("EpgEvent", ["service_name", "title", "start", "end", "length", "desc", "event_data"])
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "N/A", None) # For Python3 < 3.7
class Reader(metaclass=abc.ABCMeta):
@@ -298,14 +299,15 @@ class XmlTvReader(Reader):
offset = datetime.now() - dt
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
ev = list(filter(lambda s: s.start < utc, srv.events))
ev = max(filter(lambda s: s.start < utc, srv.events), key=lambda x: x.start, default=None)
if ev:
ev = ev[-1]
start = datetime.fromtimestamp(ev.start) + offset
end_time = datetime.fromtimestamp(ev.duration) + offset
tm = f"{start.strftime('%H:%M')} - {end_time.strftime('%H:%M')}"
start = start.timestamp()
end_time = end_time.timestamp()
for n in srv.names:
events[n] = EpgEvent(n, ev.title, tm, ev.desc, ev)
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), ev.desc, ev)
return events
@@ -373,32 +375,39 @@ class ChannelsParser:
refs = []
dom = parse(path)
description = "".join(n.data + "\n" for n in dom.childNodes if n.nodeType == Node.COMMENT_NODE)
pos_pat = re.compile(r"^\d+\.\d+[EW]$")
for elem in dom.getElementsByTagName("channels"):
c_count = 0
comment_count = 0
current_data = ""
data = ""
ch_id = None
pos = None
ch_type = BqServiceType.DEFAULT
if elem.hasChildNodes():
for n in elem.childNodes:
if n.nodeType == Node.ELEMENT_NODE:
ch_id = n.getAttribute("id")
if n.nodeType == Node.COMMENT_NODE:
c_count += 1
comment_count += 1
txt = n.data.strip()
if re.match(pos_pat, txt):
pos = txt
if comment_count:
comment_count -= 1
else:
ref_data = current_data.split(":")
refs.append(BouquetService(name=txt,
type=BqServiceType.DEFAULT,
data="{}:{}:{}:{}".format(*ref_data[3:7]).upper(),
num="{}:{}:{}".format(*ref_data[3:6]).upper()))
refs.append(BouquetService(name=txt, type=ch_type, data=data.upper(), num=(pos, ch_id)))
if n.hasChildNodes():
for s_node in n.childNodes:
if s_node.nodeType == Node.TEXT_NODE:
comment_count -= 1
current_data = s_node.data
data = s_node.data
return refs, description
@staticmethod

View 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
@@ -41,6 +41,8 @@ class Player(Gtk.DrawingArea):
def __init__(self, mode, widget, **kwargs):
super().__init__(**kwargs)
self._mode = mode
self._is_playing = False
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
@@ -63,6 +65,7 @@ class Player(Gtk.DrawingArea):
parent = widget.get_parent()
parent.connect("play", self.on_play)
parent.connect("stop", self.on_stop)
parent.connect("pause", self.on_pause)
self.show()
def get_play_mode(self):
@@ -107,6 +110,9 @@ class Player(Gtk.DrawingArea):
def on_stop(self, widget, state):
self.stop()
def on_pause(self, widget, state):
self.pause()
def on_release(self, widget, state):
self.release()
@@ -194,9 +200,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...")
@@ -237,19 +240,21 @@ 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):
pass
self._player.pause = not self._player.pause
def set_time(self, time):
pass
@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
@@ -286,8 +291,6 @@ 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())
@@ -325,21 +328,27 @@ 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):
self._player.set_state(self.STATE.PAUSED)
state = self._player.get_state(self.STATE.NULL).state
if state == self.STATE.PLAYING:
self._player.set_state(self.STATE.PAUSED)
elif state == self.STATE.PAUSED:
self._player.set_state(self.STATE.PLAYING)
def set_time(self, time):
pass
@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,9 +421,6 @@ 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,
@@ -441,7 +447,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()

View File

@@ -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:

View 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
@@ -221,7 +221,8 @@ class PiconsCzDownloader:
"piconblack80": "b50",
"piconblack3d": "b50",
"piconwin11": "win11220",
"piconSNPtransparent": "t50"
"piconSNPtransparent": "t50",
"piconSNPblack": "b50",
}
def get_name_map(self):

View 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
@@ -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
@@ -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.net/{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
@@ -730,11 +736,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 +750,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

File diff suppressed because it is too large Load Diff

View 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
@@ -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("+", " ")

View File

@@ -75,13 +75,13 @@
<section>
<item>
<attribute name="label" translatable="yes">Settings</attribute>
<attribute name="action">app.on_settings</attribute>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
<attribute name="action">app.quit</attribute>
</item>
</section>
</submenu>
@@ -156,6 +156,11 @@
<attribute name="label" translatable="yes">Alternate layout</attribute>
<attribute name="action">app.set_alternate_layout</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Alternate window title</attribute>
<attribute name="action">app.set_alternate_title</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
</submenu>
<submenu id="tools_menu">
@@ -176,6 +181,8 @@
<attribute name="action">app.on_logs_show</attribute>
</item>
</section>
<section id="extension_section">
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>
@@ -191,7 +198,7 @@
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</submenu>
@@ -200,19 +207,19 @@
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Settings</attribute>
<attribute name="action">app.on_settings</attribute>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
<attribute name="action">app.quit</attribute>
</item>
</section>
</menu>
@@ -370,6 +377,11 @@
<attribute name="label" translatable="yes">Alternate layout</attribute>
<attribute name="action">app.set_alternate_layout</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Alternate window title</attribute>
<attribute name="action">app.set_alternate_title</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
</submenu>
<submenu>
@@ -390,6 +402,8 @@
<attribute name="action">app.on_logs_show</attribute>
</item>
</section>
<section id="mac_extension_section">
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>

View 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
@@ -39,7 +39,7 @@ 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 .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, HeaderBar
class RestoreType(Enum):
@@ -77,8 +77,8 @@ class BackupDialog:
self._message_label = builder.get_object("message_label")
self._file_count_label = builder.get_object("file_count_label")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
if self._settings.use_header_bar:
header_bar = HeaderBar()
self._dialog_window.set_titlebar(header_bar)
button_box = builder.get_object("main_button_box")
@@ -245,8 +245,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 satellites.xml).
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
# 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)):
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.
@@ -263,8 +263,8 @@ def restore_data(src, dst):
def clear_data_path(path):
""" Clearing data at the specified path excluding satellites.xml file """
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
""" Clearing data at the specified path excluding *.xml file. """
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
os.remove(os.path.join(path, file))

File diff suppressed because it is too large Load Diff

View 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
@@ -70,6 +70,7 @@ class ControlTool(Gtk.Box):
self._agc_level_bar = builder.get_object("agc_level_bar")
self._volume_button = builder.get_object("volume_button")
self._header_box = builder.get_object("control_header_box")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
# Network.
self._network_button = builder.get_object("control_network_button")
self._network_model = builder.get_object("network_model")
@@ -83,15 +84,27 @@ class ControlTool(Gtk.Box):
def init_actions(self, app):
# Remote controller actions.
app.set_action("on_one", lambda a, v: self.on_remote_action(HttpAPI.Remote.ONE))
app.set_action("on_two", lambda a, v: self.on_remote_action(HttpAPI.Remote.TWO))
app.set_action("on_three", lambda a, v: self.on_remote_action(HttpAPI.Remote.THREE))
app.set_action("on_four", lambda a, v: self.on_remote_action(HttpAPI.Remote.FOUR))
app.set_action("on_five", lambda a, v: self.on_remote_action(HttpAPI.Remote.FIVE))
app.set_action("on_six", lambda a, v: self.on_remote_action(HttpAPI.Remote.SIX))
app.set_action("on_seven", lambda a, v: self.on_remote_action(HttpAPI.Remote.SEVEN))
app.set_action("on_eight", lambda a, v: self.on_remote_action(HttpAPI.Remote.EIGHT))
app.set_action("on_nine", lambda a, v: self.on_remote_action(HttpAPI.Remote.NINE))
app.set_action("on_zero", lambda a, v: self.on_remote_action(HttpAPI.Remote.ZERO))
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
app.set_action("on_next", lambda a, v: self.on_remote_action(HttpAPI.Remote.NEXT))
app.set_action("on_back", lambda a, v: self.on_remote_action(HttpAPI.Remote.BACK))
app.set_action("on_info", lambda a, v: self.on_remote_action(HttpAPI.Remote.INFO))
app.set_action("on_ok", lambda a, v: self.on_remote_action(HttpAPI.Remote.OK))
app.set_action("on_menu", lambda a, v: self.on_remote_action(HttpAPI.Remote.MENU))
app.set_action("on_exit", lambda a, v: self.on_remote_action(HttpAPI.Remote.EXIT))
app.set_action("on_epg", lambda a, v: self.on_remote_action(HttpAPI.Remote.EPG))
app.set_action("on_ch_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_UP))
app.set_action("on_ch_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_DOWN))
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
@@ -124,6 +137,8 @@ class ControlTool(Gtk.Box):
self._remote_box.reorder_child(children[-1], 0)
pack_type = Gtk.PackType.END if alt_layout else Gtk.PackType.START
self._header_box.set_child_packing(self._network_button, False, False, 0, pack_type)
pack_type = Gtk.PackType.START if alt_layout else Gtk.PackType.END
self._header_box.set_child_packing(self._screenshot_button_box, False, False, 0, pack_type)
# ***************** Remote controller ********************* #

View File

@@ -3,7 +3,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
@@ -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-2022 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 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.2.1 Beta</property>
<property name="copyright">2018-2022 Dmitriy Yefremov
<property name="version">3.6.2 Beta</property>
<property name="copyright">2018-2023 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>

View 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
@@ -34,8 +34,8 @@ from functools import lru_cache
from pathlib import Path
from app.commons import run_idle
from app.settings import SEP, IS_WIN
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
from app.settings import SEP, IS_WIN, USE_HEADER_BAR
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
class Dialog(Enum):
@@ -157,7 +157,7 @@ def get_file_chooser_dialog(transient, text, settings, action_type, file_filter,
def get_input_dialog(transient, text):
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=IS_GNOME_SESSION)
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=USE_HEADER_BAR)
entry = builder.get_object("input_entry")
entry.set_text(text if text else "")
response = dialog.run()
@@ -223,9 +223,9 @@ def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"
if use_str:
if objects:
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION), objects)
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR), objects)
else:
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION))
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR))
else:
if objects:
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)

File diff suppressed because it is too large Load Diff

View 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
@@ -43,13 +43,13 @@ from gi.repository import GLib
from app.commons import run_idle, run_task, run_with_delay
from app.connections import download_data, DownloadType, HttpAPI
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP, EpgSource
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.tasks import BGTaskWidget
from app.ui.timers import TimerTool
from ..main_helper import on_popup_menu, update_entry_data, scroll_to
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page
from ..main_helper import on_popup_menu, update_entry_data, scroll_to, update_toggle_model, update_filter_sat_positions
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, Page, HeaderBar
class RefsSource(Enum):
@@ -73,13 +73,14 @@ class EpgCache(dict):
self.init()
@run_idle
@run_with_delay(5)
def init(self):
if self._src is EpgSource.XML:
url = self._settings.epg_xml_source
gz_file = f"{self._settings.profile_data_path}epg{os.sep}epg.gz"
self._reader = XmlTvReader(gz_file, url)
@run_with_delay(2)
def process_data():
t = BGTaskWidget(self._app, "Processing XMLTV data...", self._reader.parse, )
self._app.emit("add-background-task", t)
@@ -196,13 +197,15 @@ class EpgSettingsPopover(Gtk.Popover):
class EpgTool(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, app, **kwargs):
super().__init__(**kwargs)
self._current_bq = None
self._app = app
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("filter-toggled", self.on_filter_toggled)
handlers = {"on_epg_press": self.on_epg_press,
"on_timer_add": self.on_timer_add,
@@ -217,12 +220,24 @@ class EpgTool(Gtk.Box):
self._model = builder.get_object("epg_model")
self._filter_model = builder.get_object("epg_filter_model")
self._filter_model.set_visible_func(self.epg_filter_function)
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._event_count_label = builder.get_object("event_count_label")
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
# Custom sort function.
self._view.get_model().set_sort_func(2, self.time_sort_func, 2)
# Custom data functions.
renderer = builder.get_object("epg_start_renderer")
column = builder.get_object("epg_start_column")
column.set_cell_data_func(renderer, self.start_data_func)
renderer = builder.get_object("epg_end_renderer")
column = builder.get_object("epg_end_column")
column.set_cell_data_func(renderer, self.end_data_func)
renderer = builder.get_object("epg_length_renderer")
column = builder.get_object("epg_length_column")
column.set_cell_data_func(renderer, self.duration_data_func)
# Time formats.
self._time_fmt = "%a %x - %H:%M"
self._duration_fmt = f"%{'' if IS_WIN else '-'}Hh %Mm"
self.show()
@@ -279,29 +294,40 @@ class EpgTool(Gtk.Box):
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):
def update_epg_data(self, epg=None):
self._event_count_label.set_text("0")
self._model.clear()
list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", [])
if e.get("e2eventid", "").isdigit())))
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):
t_str = f"{'%a, ' if show_day else ''}%x, %H:%M"
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"))
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
ev_time = f"{start_time.strftime(t_str)} - {end_time.strftime('%H:%M')}"
return EpgEvent(s_name, title, start, start + duration, duration, desc, event)
return EpgEvent(s_name, title, ev_time, 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()
@@ -312,14 +338,17 @@ class EpgTool(Gtk.Box):
def epg_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return next((s for s in model.get(itr, 0, 1, 2, 3) if txt in s.upper()), False)
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 time_sort_func(self, model, iter1, iter2, column):
""" Custom sort function for time column. """
event1 = model.get_value(iter1, 4)
event2 = model.get_value(iter2, 4)
return int(event1.get("e2eventstart", "0")) - int(event2.get("e2eventstart", "0"))
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)
@@ -373,6 +402,7 @@ class EpgDialog:
"on_save_to_xml": self.on_save_to_xml,
"on_auto_configuration": self.on_auto_configuration,
"on_filter_toggled": self.on_filter_toggled,
"on_filter_satellite_toggled": self.on_filter_satellite_toggled,
"on_filter_changed": self.on_filter_changed,
"on_info_bar_close": self.on_info_bar_close,
"on_popup_menu": on_popup_menu,
@@ -392,7 +422,8 @@ class EpgDialog:
"on_update_on_start_switch": self.on_update_on_start_switch,
"on_field_icon_press": self.on_field_icon_press,
"on_key_press": self.on_key_press,
"on_bq_cursor_changed": self.on_bq_cursor_changed}
"on_bq_cursor_changed": self.on_bq_cursor_changed,
"on_source_view_query_tooltip": self.on_source_view_query_tooltip}
self._app = app
self._ex_services = self._app.current_services
@@ -405,6 +436,7 @@ class EpgDialog:
self._update_epg_data_on_start = False
self._refs_source = RefsSource.SERVICES
self._download_xml_is_active = False
self._sat_positions = None
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}dialog.glade", handlers)
@@ -419,12 +451,14 @@ class EpgDialog:
self._assign_ref_popup_item = builder.get_object("bouquet_assign_ref_popup_item")
self._left_action_box = builder.get_object("left_action_box")
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
self._src_load_spinner = builder.get_object("src_load_spinner")
# 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._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")
# Info
self._source_count_label = builder.get_object("source_count_label")
self._source_info_label = builder.get_object("source_info_label")
@@ -443,9 +477,8 @@ class EpgDialog:
self._update_on_start_switch = builder.get_object("update_on_start_switch")
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True, title="EPG",
subtitle=get_message("List configuration"))
if self._settings.use_header_bar:
header_bar = HeaderBar(title="EPG", subtitle=get_message("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")
@@ -528,6 +561,8 @@ class EpgDialog:
return
yield True
self._src_load_spinner.start()
if self._refs_source is RefsSource.SERVICES:
yield from self.init_lamedb_source(refs)
elif self._refs_source is RefsSource.XML:
@@ -538,6 +573,8 @@ class EpgDialog:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message("Unknown names source!", Gtk.MessageType.ERROR)
self._src_load_spinner.stop()
yield True
def init_bouquet_data(self):
@@ -555,7 +592,7 @@ class EpgDialog:
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(filtered):
self._services_model.append((srv.service, srv.pos, srv.fav_id, srv.picon_id))
self._services_model.append((srv.service, srv.pos, srv.fav_id, srv.picon_id, srv.picon_id))
if index % factor == 0:
yield True
@@ -627,12 +664,20 @@ class EpgDialog:
except Exception as e:
raise ValueError(f"{get_message('XML parsing error:')} {e}")
else:
if refs:
s_refs = filter(lambda x: x.num in refs, s_refs)
refs = refs or {}
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(s_refs):
self._services_model.append((srv.name, " ", srv.data, ""))
ref_data = srv.data.split(":")
ref = ":".join(ref_data[3:6])
if ref in refs:
continue
data = ":".join(ref_data[3:7])
pos, ch_id = srv.num
pos = pos or " "
self._services_model.append((srv.name, pos, data, "_".join(ref_data).rstrip("_"), ch_id))
if index % factor == 0:
yield True
@@ -660,6 +705,25 @@ class EpgDialog:
if path:
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
def on_source_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
result = view.get_dest_row_at_pos(x, y)
if not result:
return False
path, pos = result
ch_id = view.get_model()[path][-1]
if not ch_id:
return False
if self._refs_source is RefsSource.XML:
text = f"ID = {ch_id}"
else:
text = f"{get_message('Service reference')}: {ch_id.rstrip('.png')}"
tooltip.set_text(text)
view.set_tooltip_row(tooltip, path)
return True
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
@@ -692,7 +756,7 @@ class EpgDialog:
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
source = {}
for row in self._services_model:
for row in self._source_view.get_model():
name = re.sub("\\W+", "", str(row[0])).upper()
name = name.translate(tr) if use_cyrillic else name
source[name] = row
@@ -737,11 +801,11 @@ class EpgDialog:
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = data[-2].split(":")
fav_id_data[3:7] = data[-3].split(":")
if data[-1]:
row[Column.FAV_POS] = data[-1]
p_data = data[-1].split("_")
if data[-2]:
row[Column.FAV_POS] = data[-2]
p_data = data[-2].split("_")
if p_data:
fav_id_data[2] = p_data[2]
@@ -755,16 +819,28 @@ class EpgDialog:
def on_filter_toggled(self, button):
self._filter_bar.set_visible(button.get_active())
if not button.get_active():
self._filter_entry.set_text("")
if button.get_active():
self._sat_positions = {r[1] for r in self._services_model}
update_filter_sat_positions(self._sat_pos_filter_model, self._sat_positions)
else:
self._sat_positions = None
self._filter_entry.set_text("") if self._filter_entry.get_text() else self.on_filter_changed()
@run_with_delay(1)
def on_filter_changed(self, entry):
def on_filter_satellite_toggled(self, toggle, path):
update_toggle_model(self._sat_pos_filter_model, path, toggle)
self._sat_positions.clear()
self._sat_positions.update({r[0] for r in self._sat_pos_filter_model if r[1]})
self.on_filter_changed()
@run_with_delay(2)
def on_filter_changed(self, entry=None):
self._services_filter_model.refilter()
def services_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return model is None or model == "None" or txt in model.get_value(itr, 0).upper()
pos = model.get_value(itr, 1)
pos = self._sat_positions is None or pos in self._sat_positions
return model is None or model == "None" or (txt in model.get_value(itr, 0).upper() and pos)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@@ -806,7 +882,6 @@ class EpgDialog:
def update_source_info(self, info):
lines = info.split("\n")
self._source_info_label.set_text(lines[0] if lines else "")
self._source_view.set_tooltip_text(info)
@run_idle
def update_source_count_info(self):

View File

@@ -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,12 +26,13 @@ THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.16"/>
<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 satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="epg_model">
<columns>
@@ -39,8 +40,12 @@ Author: Dmitriy Yefremov
<column type="gchararray"/>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name time -->
<column type="gchararray"/>
<!-- column-name start -->
<column type="gint"/>
<!-- column-name end -->
<column type="gint"/>
<!-- column-name length -->
<column type="gint"/>
<!-- column-name description -->
<column type="gchararray"/>
<!-- column-name data -->
@@ -48,49 +53,62 @@ Author: Dmitriy Yefremov
</columns>
</object>
<object class="GtkTreeModelFilter" id="epg_filter_model">
<property name="child_model">epg_model</property>
<property name="child-model">epg_model</property>
</object>
<object class="GtkTreeModelSort" id="epg_sort_model">
<property name="model">epg_filter_model</property>
</object>
<object class="GtkFrame" id="epg_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.49</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="epg_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="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">
<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="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="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="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>
<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="can_focus">False</property>
<property name="name">header-entry</property>
<property name="can-focus">False</property>
<property name="editable">False</property>
<property name="width_chars">10</property>
<property name="width-chars">10</property>
</object>
</child>
</object>
@@ -103,16 +121,16 @@ Author: Dmitriy Yefremov
<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>
<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="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-replace-symbolic</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find-replace-symbolic</property>
</object>
</child>
</object>
@@ -125,17 +143,17 @@ Author: Dmitriy Yefremov
<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>
<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>
<property name="can-focus">False</property>
<property name="icon-name">alarm-symbolic</property>
</object>
</child>
</object>
@@ -145,20 +163,6 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child type="center">
<object class="GtkToggleButton" id="multi_epg_button">
<property name="label" translatable="yes">Multi EPG</property>
<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>
</object>
<packing>
<property name="expand">False</property>
@@ -169,18 +173,18 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="epg_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="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="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="epg_filter_button" bind-property="active"/>
<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>
@@ -191,15 +195,15 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="fav_search_box">
<property name="can_focus">False</property>
<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>
<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>
@@ -211,13 +215,13 @@ Author: Dmitriy Yefremov
<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>
<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>
<property name="can-focus">False</property>
<property name="arrow-type">down</property>
</object>
</child>
</object>
@@ -231,13 +235,13 @@ Author: Dmitriy Yefremov
<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>
<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>
<property name="can-focus">False</property>
<property name="arrow-type">up</property>
</object>
</child>
</object>
@@ -267,18 +271,17 @@ Author: Dmitriy Yefremov
<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>
<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="can-focus">True</property>
<property name="model">epg_sort_model</property>
<property name="rules_hint">True</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">3</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">
@@ -288,15 +291,14 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_service_column">
<property name="visible">False</property>
<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="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>
<property name="visible" bind-source="multi_epg_button" bind-property="active"/>
<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>
@@ -311,11 +313,10 @@ Author: Dmitriy Yefremov
<object class="GtkTreeViewColumn" id="epg_title_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed_width">170</property>
<property name="min_width">50</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>
<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>
@@ -327,18 +328,18 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_time_column">
<object class="GtkTreeViewColumn" id="epg_start_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed_width">210</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Time</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort_column_id">2</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_time_renderer">
<object class="GtkCellRendererText" id="epg_start_renderer">
<property name="xpad">5</property>
<property name="xalign">0.49000000953674316</property>
<property name="xalign">0.49</property>
<property name="width-chars">27</property>
</object>
<attributes>
<attribute name="text">2</attribute>
@@ -346,21 +347,60 @@ Author: Dmitriy Yefremov
</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="fixed_width">100</property>
<property name="min_width">50</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">3</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">3</attribute>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
@@ -376,17 +416,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="status_box">
<property name="height_request">26</property>
<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="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>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -397,9 +437,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="event_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -420,7 +460,7 @@ Author: Dmitriy Yefremov
<child type="label">
<object class="GtkLabel" id="epg_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">EPG</property>
</object>
</child>

View 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
@@ -42,10 +42,10 @@ 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
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.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION, Page
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
@@ -529,7 +529,7 @@ class FtpClientBox(Gtk.HBox):
@run_idle
def show_edit_dialog(self, f_path, data):
dialog = TextEditDialog(f_path, IS_GNOME_SESSION)
dialog = TextEditDialog(f_path, USE_HEADER_BAR)
dialog.text = data
ok = Gtk.ResponseType.OK
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
@@ -578,7 +578,7 @@ class FtpClientBox(Gtk.HBox):
log(f"Init attributes error [{attrs}]. Invalid length!")
return
dialog = AttributesDialog(attrs, IS_GNOME_SESSION)
dialog = AttributesDialog(attrs, USE_HEADER_BAR)
ok = Gtk.ResponseType.OK
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
log(self._ftp.sendcmd(f"SITE CHMOD {dialog.permissions} {file}"))

View File

@@ -3,7 +3,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
@@ -27,11 +27,12 @@ Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.20"/>
<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-2022 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="bq_list_store">
<columns>
@@ -54,6 +55,113 @@ Author: Dmitriy Yefremov
<property name="icon-name">document-revert-symbolic-rtl</property>
<property name="icon_size">1</property>
</object>
<object class="GtkPopover" id="options_popover">
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="options_popover_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">5</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="replace_existing_settings_box">
<property name="visible">True</property>
<property name="sensitive" bind-source="bouquets_only_switch" bind-property="active" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enables overwriting existing main list services.</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="replace_existing_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Replace existing</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="replace_existing_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="bouquets_settings_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enables skipping services import from lamedb.</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="bouquets_only_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Bouquets data only</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="bouquets_only_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<signal name="state-set" handler="on_bouquets_only_switch" swapped="no"/>
</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>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="text" translatable="yes">Close</property>
<property name="centered">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkImage" id="remove_selection_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
@@ -217,7 +325,6 @@ Author: Dmitriy Yefremov
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Bouquets and services</property>
<property name="valign">center</property>
<property name="image">import_image</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_import" swapped="no"/>
@@ -229,20 +336,81 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkToggleButton" id="details_button">
<property name="label" translatable="yes">Details</property>
<object class="GtkBox" id="extra_header_box">
<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="image">details_image</property>
<property name="always-show-image">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkToggleButton" id="details_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">Details</property>
<property name="margin-start">5</property>
<property name="image">details_image</property>
<property name="always-show-image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="options_menu_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="direction">none</property>
<property name="popover">options_popover</property>
<child>
<object class="GtkBox" id="options_nutton_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage" id="options_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Options</property>
<property name="icon-name">applications-system-symbolic</property>
<property name="icon_size">1</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="options_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Options</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">3</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>
<property name="position">2</property>
</packing>
</child>
</object>

View 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
@@ -37,7 +37,7 @@ from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neu
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 .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, IS_GNOME_SESSION, Page
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, Page, HeaderBar
def import_bouquet(app, model, path, appender, file_path=None):
@@ -121,6 +121,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,6 +145,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")
@@ -158,19 +163,20 @@ class ImportDialog:
self._sat_model = builder.get_object("sat_list_store")
self._sat_count_label = builder.get_object("sat_count_label")
if IS_GNOME_SESSION:
if self._settings.use_header_bar:
actions_box = builder.get_object("actions_box")
builder.get_object("toolbar_box").set_visible(False)
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
header_bar = HeaderBar()
stack_switcher = builder.get_object("stack_switcher")
actions_box.remove(stack_switcher)
header_bar.set_custom_title(stack_switcher)
button = builder.get_object("import_button")
actions_box.remove(button)
header_bar.pack_start(button)
button = builder.get_object("details_button")
actions_box.remove(button)
header_bar.pack_end(button)
extra_box = builder.get_object("extra_header_box")
actions_box.remove(extra_box)
header_bar.pack_end(extra_box)
self._dialog_window.set_titlebar(header_bar)
window_size = self._settings.get("import_dialog_window_size")
@@ -258,17 +264,26 @@ class ImportDialog:
with suppress(ValueError):
bq.remove(b)
services = 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)})
self._dialog_window.destroy()
def import_satellites_data(self):
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
return
replace_existing = False
replace_existing = self._replace_existing_switch.get_active()
services = []
current_services = self._app.current_services
to_replace = {}
for row in self._sat_model:
if row[-1]:
@@ -277,10 +292,14 @@ class ImportDialog:
for s in filter(lambda srv: srv.fav_id not in skip, self._sat_services.get(sat[0], ())):
if replace_existing and s.fav_id in self._ids:
current_services[s.fav_id] = s
to_replace[s.fav_id] = s
elif s.fav_id not in self._ids:
services.append(s)
self._append((), services)
if to_replace:
self._app.emit("services_update", to_replace)
self._dialog_window.destroy()
@run_idle
@@ -398,6 +417,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 """

File diff suppressed because it is too large Load Diff

View 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
@@ -45,12 +45,13 @@ 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.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
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}:0:0:0"
_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 +82,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 +95,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"))
@@ -116,8 +119,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,
@@ -134,6 +139,8 @@ class IptvDialog:
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)
@@ -160,6 +167,13 @@ class IptvDialog:
self.show_info_message(get_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(get_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):
return
@@ -194,15 +208,25 @@ 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(data[2])
self._srv_type_entry.set_text(str(int(data[2], 16)))
self._sid_entry.set_text(str(int(data[3], 16)))
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,10 +236,9 @@ 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._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._srv_type_entry.get_text()),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
@@ -242,15 +265,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 +297,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"{get_message('Getting link error:')} {e}", Gtk.MessageType.ERROR)
return
except YouTubeException as e:
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
@@ -279,7 +312,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"{get_message('Getting link error:')} No link received for id: {video_id}"
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
entry.set_sensitive(True)
@@ -291,22 +324,33 @@ 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(),
self._srv_type_entry.get_text(),
int(self._srv_type_entry.get_text()),
int(self._sid_entry.get_text()),
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)
@@ -601,7 +645,7 @@ class IptvListConfigurationDialog(IptvListDialog):
st_type = get_stream_type(self._stream_type_combobox)
s_id = "0" if id_default else self._list_srv_id_entry.get_text()
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
srv_type = int("1" if type_default else self._list_srv_type_entry.get_text())
sid = "0" if sid_auto else self._list_sid_entry.get_text()
tid = "0" if tid_default else f"{int(self._list_tid_entry.get_text()):X}"
nid = "0" if nid_default else f"{int(self._list_nid_entry.get_text()):X}"
@@ -616,12 +660,13 @@ class IptvListConfigurationDialog(IptvListDialog):
if all_default:
data[1], data[2], data[3], data[4], data[5], data[6] = "010000"
else:
data[0], data[1], data[2], data[4], data[5], data[6] = st_type, s_id, srv_type, tid, nid, namespace
data[0], data[1], data[4], data[5], data[6] = st_type, s_id, tid, nid, namespace
data[2] = f"{srv_type:X}"
data[3] = f"{index:X}" if sid_auto else sid
if sid_auto:
params[0] = index
picon_id = PICON_FORMAT.format(st_type, int(s_id), int(srv_type), *params)
picon_id = PICON_FORMAT.format(st_type, int(s_id), srv_type, *params)
data = ":".join(data)
new_fav_id = f"{data}{sep}{desc}"
row[Column.FAV_ID] = new_fav_id
@@ -859,7 +904,6 @@ class YtListImportDialog:
"on_key_press": self.on_key_press,
"on_close": self.on_close}
# self._main_window, self._settings, self.append_imported_services
self.appender = app.append_imported_services
self._settings = app.app_settings
self._s_type = self._settings.setting_type
@@ -887,26 +931,54 @@ 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"))
self._dialog.set_titlebar(header_bar)
actions_box = builder.get_object("yt_actions_box")
import_box = builder.get_object("yt_import_box")
actions_box.remove(import_box)
header_bar.pack_end(import_box)
actions_box.remove(self._receive_button)
header_bar.pack_start(self._receive_button)
actions_box.set_visible(False)
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:
@@ -935,7 +1007,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
@@ -978,22 +1049,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(get_message("Done!"), Gtk.MessageType.INFO)
@run_idle
def update_active_elements(self, sensitive):

View File

@@ -898,7 +898,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_bottom">2</property>
<property name="action_name">app.on_settings</property>
<property name="action_name">app.preferences</property>
<property name="text" translatable="yes">Settings</property>
</object>
<packing>
@@ -949,7 +949,7 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.on_about_app</property>
<property name="action_name">app.about</property>
<property name="text" translatable="yes">About</property>
</object>
<packing>
@@ -963,8 +963,8 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.quit</property>
<property name="text" translatable="yes">Exit</property>
<signal name="clicked" handler="on_close_app" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
@@ -1003,6 +1003,11 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-find-and-replace</property>
</object>
<object class="GtkImage" id="remove_duplicates_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all</property>
</object>
<object class="GtkImage" id="mark_not_in_bq_image">
<property name="visible">True</property>
@@ -1607,6 +1612,7 @@ Author: Dmitriy Yefremov
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hhomogeneous">False</property>
<property name="transition_type">crossfade</property>
<signal name="notify::visible-child-name" handler="on_visible_page" swapped="no"/>
<child>
@@ -1651,7 +1657,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="app_ver_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">3.2.1 Beta</property>
<property name="label">3.6.2 Beta</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
@@ -1777,12 +1783,12 @@ Author: Dmitriy Yefremov
<child type="center">
<object class="GtkButtonBox" id="services_button_box">
<property name="visible">True</property>
<property name="name">header-stack-switcher</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkRadioButton" id="dvb_button">
<property name="label" translatable="yes">DVB</property>
<property name="name">stack-switch-button</property>
<property name="width_request">80</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -1800,7 +1806,6 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkRadioButton" id="iptv_button">
<property name="label" translatable="yes">IPTV</property>
<property name="name">stack-switch-button</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
@@ -1846,7 +1851,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="enigma_lock_hide_box">
<property name="sensitive">False</property>
<property name="sensitive" bind-source="bouquet_lock_hide_box" bind-property="sensitive" bind-flags="invert-boolean">False</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
@@ -3427,7 +3432,6 @@ Author: Dmitriy Yefremov
<property name="model">fav_list_store</property>
<property name="enable_search">False</property>
<property name="search_column">2</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">9</property>
@@ -3934,6 +3938,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="bouquet_lock_hide_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
@@ -4173,7 +4178,7 @@ Author: Dmitriy Yefremov
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">False</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
@@ -4751,6 +4756,17 @@ Author: Dmitriy Yefremov
<signal name="activate" handler="on_mark_duplicates" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_remove_dup_popup_item">
<property name="label" translatable="yes">Remove duplicates</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="image">remove_duplicates_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_remove_duplicates" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_3">
<property name="visible">True</property>

View 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
@@ -48,7 +48,7 @@ from app.eparser.enigma.bouquets import BqServiceType
from app.eparser.iptv import export_to_m3u, StreamType
from app.eparser.neutrino.bouquets import BqType
from app.settings import (SettingsType, Settings, SettingsException, SettingsReadException,
IS_DARWIN, PlayStreamsMode, IS_LINUX)
IS_DARWIN, PlayStreamsMode, IS_LINUX, USE_HEADER_BAR)
from app.tools.media import Recorder
from app.ui.control import ControlTool
from app.ui.epg.epg import EpgCache, EpgSettingsPopover, EpgDialog, EpgTool
@@ -69,7 +69,7 @@ from .search import SearchProvider
from .service_details_dialog import ServiceDetailsDialog, Action
from .settings_dialog import SettingsDialog
from .uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column,
FavClickMode, MOD_MASK, APP_FONT, Page, IS_GNOME_SESSION)
FavClickMode, MOD_MASK, APP_FONT, Page, HeaderBar)
from .xml.dialogs import ServicesUpdateDialog
from .xml.edit import SatellitesTool
@@ -98,7 +98,8 @@ class Application(Gtk.Application):
_FAV_ELEMENTS = ("fav_cut_popup_item", "fav_paste_popup_item", "fav_locate_popup_item", "fav_iptv_popup_item",
"fav_insert_marker_popup_item", "fav_insert_space_popup_item", "fav_edit_sub_menu_popup_item",
"fav_edit_popup_item", "fav_picon_popup_item", "fav_copy_popup_item", "fav_add_alt_popup_item",
"fav_epg_configuration_popup_item", "fav_mark_dup_popup_item", "fav_reference_popup_item")
"fav_epg_configuration_popup_item", "fav_mark_dup_popup_item", "fav_remove_dup_popup_item",
"fav_reference_popup_item")
_BOUQUET_ELEMENTS = ("bouquets_new_popup_item", "bouquets_edit_popup_item", "bouquets_cut_popup_item",
"bouquets_copy_popup_item", "bouquets_paste_popup_item", "new_header_button",
@@ -111,8 +112,6 @@ class Application(Gtk.Application):
_FAV_IPTV_ELEMENTS = ("fav_iptv_popup_item", "import_m3u_header_button", "export_to_m3u_menu_button",
"iptv_menu_button")
_LOCK_HIDE_ELEMENTS = ("enigma_lock_hide_box", "bouquet_lock_hide_box")
def __init__(self, **kwargs):
super().__init__(flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, **kwargs)
# Adding command line options
@@ -176,6 +175,7 @@ class Application(Gtk.Application):
"on_fav_press": self.on_fav_press,
"on_locate_in_services": self.on_locate_in_services,
"on_mark_duplicates": self.on_mark_duplicates,
"on_remove_duplicates": self.on_remove_duplicates,
"on_services_mark_not_in_bouquets": self.on_services_mark_not_in_bouquets,
"on_services_clear_marked": self.on_services_clear_marked,
"on_services_clear_new_marked": self.on_services_clear_new_marked,
@@ -308,6 +308,8 @@ class Application(Gtk.Application):
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("epg-dat-downloaded", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("services-update", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("iptv-service-edited", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("iptv-service-added", self, GObject.SIGNAL_RUN_LAST,
@@ -418,7 +420,6 @@ class Application(Gtk.Application):
self._filter_only_free_button = builder.get_object("filter_only_free_button")
self._filter_not_in_bq_button = builder.get_object("filter_not_in_bq_button")
self._services_load_spinner.bind_property("active", self._filter_services_button, "sensitive", 4)
self._services_load_spinner.bind_property("active", self._filter_box, "sensitive", 4)
self._filter_iptv_services_button = builder.get_object("filter_iptv_services_button")
# Search.
services_search_provider = SearchProvider(self._services_view,
@@ -458,11 +459,12 @@ class Application(Gtk.Application):
self._record_image = builder.get_object("record_button_image")
# Dynamically active elements depending on the selected view.
d_elements = (self._SERVICE_ELEMENTS, self._BOUQUET_ELEMENTS, self._COMMONS_ELEMENTS, self._FAV_ELEMENTS,
self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS, self._LOCK_HIDE_ELEMENTS)
self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS)
self._tool_elements = {k: builder.get_object(k) for k in set(chain.from_iterable(d_elements))}
# Lock, Hide.
self.bind_property("is-enigma", self._tool_elements.get(self._LOCK_HIDE_ELEMENTS[0]), "visible")
self.bind_property("is-enigma", self._tool_elements.get(self._LOCK_HIDE_ELEMENTS[1]), "visible", 4)
self._bouquet_lock_hide_box = builder.get_object("bouquet_lock_hide_box")
self._bouquets_view.bind_property("is-focus", self._bouquet_lock_hide_box, "sensitive")
self.bind_property("is-enigma", builder.get_object("enigma_lock_hide_box"), "visible")
# Clear "New" menu item
self.bind_property("is-enigma", builder.get_object("services_clear_new_flag_item"), "visible")
# Sub-bouquets menu item.
@@ -489,6 +491,7 @@ class Application(Gtk.Application):
self._logs_box = builder.get_object("logs_box")
self._logs_box.pack_start(LogsClient(self), True, True, 0)
self._bottom_paned = builder.get_object("bottom_paned")
self.connect("services-update", self.on_services_update)
# Send/Receive.
self.connect("data-receive", self.on_download)
self.connect("data-send", self.on_upload)
@@ -504,9 +507,11 @@ class Application(Gtk.Application):
# Header bar.
profile_box = builder.get_object("profile_combo_box")
toolbar_box = builder.get_object("toolbar_main_box")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
header_bar.pack_start(builder.get_object("file_header_button"))
if self._settings.use_header_bar:
header_bar = HeaderBar()
if not IS_DARWIN:
header_bar.pack_start(builder.get_object("file_header_button"))
header_bar.pack_start(profile_box)
header_bar.pack_start(toolbar_box)
header_bar.set_custom_title(builder.get_object("stack_switcher"))
@@ -568,12 +573,16 @@ class Application(Gtk.Application):
self._epg_menu_button = builder.get_object("epg_menu_button")
self._epg_menu_button.connect("realize", lambda b: b.set_popover(EpgSettingsPopover(self)))
self.bind_property("is_enigma", self._epg_menu_button, "sensitive")
self._epg_start_time_fmt = "%a, %H:%M"
self._epg_end_time_fmt = "%H:%M"
# Hiding for Neutrino.
self.bind_property("is_enigma", builder.get_object("services_button_box"), "visible")
# Setting the last size of the window if it was saved.
main_window_size = self._settings.get("window_size")
if main_window_size:
self._main_window.resize(*main_window_size)
# Layout.
self.init_layout()
# Style.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -582,48 +591,19 @@ class Application(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
# App menu.
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui", tag="attribute")
if not IS_GNOME_SESSION:
if IS_DARWIN:
self.set_app_menu(builder.get_object("mac_app_menu"))
self.set_menubar(builder.get_object("mac_menu_bar"))
else:
self.set_menubar(builder.get_object("menu_bar"))
else:
tools_menu = builder.get_object("tools_menu")
tools_button = Gtk.MenuButton(visible=True, menu_model=tools_menu, direction=Gtk.ArrowType.NONE)
tools_button.set_tooltip_text(get_message("Tools"))
tools_button.set_image(Gtk.Image.new_from_icon_name("applications-utilities-symbolic", Gtk.IconSize.BUTTON))
self.init_app_menu()
self.init_actions()
self.set_accels()
view_menu = builder.get_object("view_menu")
view_button = Gtk.MenuButton(visible=True, menu_model=view_menu, direction=Gtk.ArrowType.NONE)
view_button.set_tooltip_text(get_message("View"))
box = Gtk.ButtonBox(visible=True, layout_style="expand")
box.add(tools_button)
box.add(view_button)
self._main_window.get_titlebar().pack_end(box)
# IPTV menu.
self._iptv_menu_button.set_menu_model(builder.get_object("iptv_menu"))
iptv_elem = self._tool_elements.get("fav_iptv_popup_item")
for h in (self.on_iptv, self.on_import_yt_list, self.on_import_m3u, self.on_export_iptv_to_m3u,
self.on_epg_list_configuration, self.on_iptv_list_configuration, self.on_remove_all_unavailable):
iptv_elem.bind_property("sensitive", self.set_action(h.__name__, h, False), "enabled")
self.init_drag_and_drop()
self.init_appearance()
self.filter_set_default()
def do_activate(self):
self._main_window.set_application(self)
self._main_window.set_wmclass("DemonEditor", "DemonEditor")
self._main_window.present()
self.init_actions()
self.set_accels()
self.init_layout()
self.init_drag_and_drop()
self.init_appearance()
self.filter_set_default()
self.init_profiles()
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@@ -667,15 +647,98 @@ class Application(Gtk.Application):
self.activate()
return 0
def init_app_menu(self):
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui", tag="attribute")
if not USE_HEADER_BAR:
if IS_DARWIN:
if not self.get_app_menu():
self.set_app_menu(builder.get_object("mac_app_menu"))
self.set_menubar(builder.get_object("mac_menu_bar"))
else:
self.set_menubar(builder.get_object("menu_bar"))
else:
tools_menu = builder.get_object("tools_menu")
tools_button = Gtk.MenuButton(visible=True, menu_model=tools_menu, direction=Gtk.ArrowType.NONE)
tools_button.set_tooltip_text(get_message("Tools"))
tools_button.set_image(Gtk.Image.new_from_icon_name("applications-utilities-symbolic", Gtk.IconSize.BUTTON))
view_menu = builder.get_object("view_menu")
view_button = Gtk.MenuButton(visible=True, menu_model=view_menu, direction=Gtk.ArrowType.NONE)
view_button.set_tooltip_text(get_message("View"))
box = Gtk.ButtonBox(visible=True, layout_style="expand")
box.add(tools_button)
box.add(view_button)
self._main_window.get_titlebar().pack_end(box)
# IPTV menu.
self._iptv_menu_button.set_menu_model(builder.get_object("iptv_menu"))
iptv_elem = self._tool_elements.get("fav_iptv_popup_item")
for h in (self.on_iptv, self.on_import_yt_list, self.on_import_m3u, self.on_export_iptv_to_m3u,
self.on_epg_list_configuration, self.on_iptv_list_configuration, self.on_remove_all_unavailable):
iptv_elem.bind_property("sensitive", self.set_action(h.__name__, h, False), "enabled")
if self._settings.extensions_support:
self.init_extensions(builder)
def init_extensions(self, builder):
import pkgutil
# Extensions (Plugins) section.
ext_section = builder.get_object(f"{'mac_' if IS_DARWIN else ''}extension_section")
ext_path = f"{self._settings.default_data_path}tools{os.sep}extensions"
ext_paths = [f"{os.path.dirname(__file__)}{os.sep}extensions", ext_path, "extensions"]
extensions = {}
switchable = []
default = []
def ac(a, v):
c = extensions[a.get_name()]
e = c(self)
e.exec()
def sw(a, v):
c = extensions[a.get_name()]
a.set_state(v)
e = c(self)
e.exec() if v else e.stop()
for importer, name, is_package in pkgutil.iter_modules(ext_paths):
if is_package:
m = importer.find_module(name).load_module()
cls_name = name.capitalize()
if hasattr(m, cls_name):
cls = getattr(m, cls_name)
if cls.EMBEDDED:
cls(self)
continue
action_name = f"on_{name}_extension"
item = Gio.MenuItem.new(cls.LABEL, f"app.{action_name}")
extensions[action_name] = cls
if cls.SWITCHABLE:
switchable.append(item)
self.set_state_action(action_name, sw, False)
else:
default.append(item)
self.set_action(action_name, ac)
switchable.sort(key=lambda i: i.get_attribute_value("label"), reverse=True)
default.sort(key=lambda i: i.get_attribute_value("label"), reverse=True)
[ext_section.append_item(item) for item in switchable]
[ext_section.append_item(item) for item in default]
def init_actions(self):
# Main actions.
self.set_action("preferences", self.on_settings)
self.set_action("about", self.on_about_app)
self.set_action("quit", self.on_close_app)
# Import.
self.set_action("on_import_bouquet", self.on_import_bouquet)
self.set_action("on_import_bouquets", self.on_import_bouquets)
self.set_action("on_new_configuration", self.on_new_configuration)
self.set_action("on_import_from_web", self.on_import_from_web)
self.set_action("on_settings", self.on_settings)
# Tools.
self.set_action("on_backup_tool_show", self.on_backup_tool_show)
self.set_action("on_about_app", self.on_about_app)
self.set_action("on_close_app", self.on_close_app)
self.set_state_action("on_telnet_show", self.on_telnet_show, False)
self.set_state_action("on_logs_show", self.on_logs_show, False)
# Filter.
@@ -732,9 +795,12 @@ class Application(Gtk.Application):
# Alternate layout.
sa = self.set_state_action("set_alternate_layout", self.set_use_alt_layout, self._settings.alternate_layout)
sa.connect("change-state", self.on_layout_change)
# Header bar for macOS.
sa = self.set_state_action("set_alternate_title", self.set_use_alt_title, self._settings.use_header_bar)
sa.set_enabled(IS_DARWIN)
# Menu bar and playback.
self.set_action("on_playback_close", self._player_box.on_close)
if not IS_GNOME_SESSION:
if not USE_HEADER_BAR:
# We are working with the "hidden-when" submenu attribute. See 'app_menu_.ui' file.
hide_bar_action = Gio.SimpleAction.new("hide_menu_bar", None)
self._player_box.bind_property("visible", hide_bar_action, "enabled", 4)
@@ -768,7 +834,7 @@ class Application(Gtk.Application):
self.set_accels_for_action("app.open_data", ["<primary>o"])
self.set_accels_for_action("app.on_hide", ["<primary>h"])
self.set_accels_for_action("app.on_locked", ["<primary>l"])
self.set_accels_for_action("app.on_close_app", ["<primary>q"])
self.set_accels_for_action("app.quit", ["<primary>q"])
self.set_accels_for_action("app.on_edit", ["<primary>e"])
self.set_accels_for_action("app.on_telnet_show", ["<primary>t"])
self.set_accels_for_action("app.on_logs_show", ["<shift><primary>l"])
@@ -870,12 +936,12 @@ class Application(Gtk.Application):
def init_layout(self):
""" Initializes an alternate layout, if enabled. """
if self._settings.alternate_layout:
self._main_paned.pack2(self._player_box, True, False)
self._main_paned.pack2(self._player_box, True, True)
self.reverse_main_elements(True)
else:
self._main_paned.remove(self._data_paned)
self._main_paned.pack1(self._player_box, True, False)
self._main_paned.pack2(self._data_paned, True, False)
self._main_paned.pack1(self._player_box, True, True)
self._main_paned.pack2(self._data_paned, True, True)
def init_bq_position(self):
self._fav_paned.remove(self._fav_frame)
@@ -892,7 +958,7 @@ class Application(Gtk.Application):
""" Initializes starting positions of main paned widgets. """
width = paned.get_allocated_width()
main_position = self._settings.get("data_paned_position", width * 0.5)
fav_position = self._settings.get("fav_paned_position", width * 0.27)
fav_position = self._settings.get("fav_paned_position", width * 0.25)
paned.set_position(main_position)
self._fav_paned.set_position(fav_position)
@@ -1070,6 +1136,17 @@ class Application(Gtk.Application):
action.set_state(value)
self._settings.alternate_layout = bool(value)
def set_use_alt_title(self, action, value):
action.set_state(value)
value = bool(value)
self._settings.use_header_bar = bool(value)
msg = get_message("Restart the program to apply all changes.")
if value:
warn = "It can cause some problems."
msg = f"{get_message('EXPERIMENTAL!')} {warn} {msg}"
self.show_info_message(msg, Gtk.MessageType.WARNING)
@run_idle
def on_layout_change(self, action, value):
is_alt = bool(value)
@@ -1145,10 +1222,16 @@ class Application(Gtk.Application):
event = self._epg_cache.get_current_event(srv_name)
if event:
if event.start:
start = datetime.fromtimestamp(event.start).strftime(self._epg_start_time_fmt)
end = datetime.fromtimestamp(event.end).strftime(self._epg_end_time_fmt)
sep = "-"
else:
start, end, sep = "", "", ""
# https://docs.gtk.org/Pango/pango_markup.html
renderer.set_property("markup", (f'{escape(srv_name)}\n\n'
f'<span size="small" weight="bold">{escape(event.title)}</span>\n'
f'<span size="small" style="italic">{event.time}</span>'))
f'<span size="small" style="italic">{start} {sep} {end}</span>'))
return False
return True
@@ -1174,6 +1257,9 @@ class Application(Gtk.Application):
self.on_copy(view, target=ViewTarget.BOUQUET)
def on_copy(self, view, target):
if not self._settings.unlimited_copy_buffer:
self._bouquets_buffer.clear() if target is ViewTarget.BOUQUET else self._rows_buffer.clear()
model, paths = view.get_selection().get_selected_rows()
if target is ViewTarget.FAV:
@@ -1202,6 +1288,9 @@ class Application(Gtk.Application):
self.on_cut(view, ViewTarget.BOUQUET)
def on_cut(self, view, target=None):
if not self._settings.unlimited_copy_buffer:
self._bouquets_buffer.clear() if target is ViewTarget.BOUQUET else self._rows_buffer.clear()
if target is ViewTarget.FAV:
for row in tuple(self.on_delete(view)):
self._rows_buffer.append(row)
@@ -1269,6 +1358,20 @@ class Application(Gtk.Application):
self._bouquets_buffer.clear()
self.update_bouquets_type()
def on_services_update(self, app, services):
""" Updates services in the main model. """
for r in self._fav_model:
fav_id = r[Column.FAV_ID]
if fav_id in services:
service = services[fav_id]
r[Column.FAV_SERVICE] = service.service
for r in self._services_model:
fav_id = r[Column.SRV_FAV_ID]
if fav_id in services:
service = services[fav_id]
r[Column.SRV_SERVICE] = service.service
# ***************** Deletion ********************* #
def on_delete(self, view):
@@ -1282,6 +1385,10 @@ class Application(Gtk.Application):
selection = view.get_selection()
model, paths = selection.get_selected_rows()
if not paths:
self.show_error_message("No selected item!")
return
model_name = get_base_model(model).get_name()
itrs = [model.get_iter(path) for path in paths]
rows = [model[in_itr][:] for in_itr in itrs]
@@ -1560,7 +1667,7 @@ class Application(Gtk.Application):
for s_row, row in zip(sorted(map(
lambda r: r[:], rows),
key=lambda r: r[c_num] or nv if c_num != Column.FAV_POS else self.get_pos_num(r[c_num]),
key=lambda r: r[c_num] or nv if c_num != Column.FAV_POS else get_pos_num(r[c_num]),
reverse=rev), rows):
self._fav_model.set(row.iter, columns, s_row)
bq[index] = s_row[Column.FAV_ID]
@@ -1576,18 +1683,7 @@ class Application(Gtk.Application):
def position_sort_func(self, model, iter1, iter2, column):
""" Custom sort function for position column. """
return self.get_pos_num(model.get_value(iter1, column)) - self.get_pos_num(model.get_value(iter2, column))
def get_pos_num(self, pos):
""" Returns num [float] representation of satellite position. """
if not pos:
return -183.0
if len(pos) > 1:
m = -1 if pos[-1] == "W" else 1
return float(pos[:-1]) * m
return -181.0 if pos == "T" else -182.0
return get_pos_num(model.get_value(iter1, column)) - get_pos_num(model.get_value(iter2, column))
# ********************* Hints ************************* #
@@ -2276,7 +2372,13 @@ class Application(Gtk.Application):
self.append_bouquet(bq, row.iter)
def append_bouquet(self, bq, parent):
name, bq_type, locked, hidden = bq.name, bq.type, bq.locked, bq.hidden
name, bq_type, locked, hidden = bq.name, bq.type, bq.locked, HIDE_ICON if bq.hidden else None
# Parental control state.
if self._s_type is SettingsType.ENIGMA_2:
locked = LOCKED_ICON if bq.locked in self._blacklist else None
else:
locked = LOCKED_ICON if bq.locked else None
bouquet = self._bouquets_model.append(parent, [name, locked, hidden, bq_type])
bq_id = f"{name}:{bq_type}"
services = []
@@ -2337,7 +2439,10 @@ class Application(Gtk.Application):
break
def append_services(self, services):
to_add = []
for srv in services:
if srv.fav_id not in self._services:
to_add.append(srv)
# Adding channels to dict with fav_id as keys.
self._services[srv.fav_id] = srv
self.update_services_counts(len(self._services.values()))
@@ -2345,7 +2450,7 @@ class Application(Gtk.Application):
self._services_load_spinner.start()
factor = self.DEL_FACTOR / 4
for index, srv in enumerate(services):
for index, srv in enumerate(to_add):
background = self.get_new_background(srv.flags_cas)
s = srv + (None, background)
self._services_model.append(s)
@@ -2469,7 +2574,7 @@ class Application(Gtk.Application):
# Getting bouquets
self._bouquets_view.get_model().foreach(parse_bouquets)
write_bouquets(path, bouquets, profile, self._settings.force_bq_names)
write_bouquets(path, bouquets, profile, self._settings.force_bq_names, self._blacklist)
yield True
# Getting services
services_model = get_base_model(self._services_view.get_model())
@@ -2718,6 +2823,8 @@ class Application(Gtk.Application):
if changed:
self.open_data()
if self._settings.display_epg:
self.change_action_state("display_epg", GLib.Variant.new_boolean(self._settings.display_epg))
self.emit("profile-changed", None)
def set_profile(self, active):
@@ -2822,6 +2929,11 @@ class Application(Gtk.Application):
self.update_bouquet_list()
def on_view_focus(self, view, focus_event=None):
# Preventing focus lack for some cases.
if not focus_event and not view.is_focus():
view.grab_focus()
return True
model_name, model = get_model_data(view)
not_empty = len(model) > 0 if model else False
is_service = model_name == self.SERVICE_MODEL
@@ -2833,9 +2945,6 @@ class Application(Gtk.Application):
self._tool_elements[elem].set_sensitive(not_empty)
if elem == "bouquets_paste_popup_item":
self._tool_elements[elem].set_sensitive(not_empty and self._bouquets_buffer)
if self._s_type is SettingsType.NEUTRINO_MP:
for elem in self._LOCK_HIDE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty)
else:
for elem in self._FAV_ELEMENTS:
if elem in ("paste_tool_button", "fav_paste_popup_item"):
@@ -2848,8 +2957,6 @@ class Application(Gtk.Application):
self._tool_elements[elem].set_sensitive(not_empty and is_service)
for elem in self._BOUQUET_ELEMENTS:
self._tool_elements[elem].set_sensitive(False)
for elem in self._LOCK_HIDE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty and self._s_type is SettingsType.ENIGMA_2)
for elem in self._FAV_IPTV_ELEMENTS:
is_iptv = self._bq_selected and not is_service
@@ -2870,14 +2977,21 @@ class Application(Gtk.Application):
self.set_service_flags(Flag.LOCK)
def set_service_flags(self, flag):
if self._s_type is SettingsType.ENIGMA_2:
set_flags(flag, self._services_view, self._fav_view, self._services, self._blacklist)
elif self._s_type is SettingsType.NEUTRINO_MP and self._bq_selected:
if self._bouquets_view.is_focus() and self._bq_selected:
model, paths = self._bouquets_view.get_selection().get_selected_rows()
itr = model.get_iter(paths[0])
value = model.get_value(itr, 1 if flag is Flag.LOCK else 2)
value = None if value else LOCKED_ICON if flag is Flag.LOCK else HIDE_ICON
model.set_value(itr, 1 if flag is Flag.LOCK else 2, value)
for p in paths:
itr = model.get_iter(p)
if not model.iter_has_child(itr):
value = model.get_value(itr, 1 if flag is Flag.LOCK else 2)
value = None if value else LOCKED_ICON if flag is Flag.LOCK else HIDE_ICON
model.set_value(itr, 1 if flag is Flag.LOCK else 2, value)
if self._s_type is SettingsType.ENIGMA_2:
msg = get_message("After uploading the changes you may need to completely reboot the receiver!")
self.show_info_message(f"{get_message('EXPERIMENTAL!')} {msg}", Gtk.MessageType.WARNING)
else:
if self._s_type is SettingsType.ENIGMA_2:
set_flags(flag, self._services_view, self._fav_view, self._services, self._blacklist)
def on_model_changed(self, model, path=None, itr=None):
model_name = model.get_name()
@@ -3024,9 +3138,11 @@ class Application(Gtk.Application):
return
ref = self._clipboard.wait_for_text()
if ref and re.match(r"\d+_\d+_\d+_\w+_\w+_\w+_\d+_0_0_0", ref):
if ref and re.match(r"\d+_\d+_\w+_\w+_\w+_\w+_\w+_0_0_0", ref):
[self.assign_reference(model, p, ref) for p in iptv_paths]
self._clipboard.clear()
else:
log(f"Error parsing reference [{ref}].")
self.emit("clipboard-changed", self._clipboard.wait_is_text_available())
@@ -3035,7 +3151,7 @@ class Application(Gtk.Application):
row = model[path]
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = ref_data[3:7]
fav_id_data[2:7] = ref_data[2:7]
new_fav_id = ":".join(fav_id_data)
new_data_id = ":".join(fav_id_data[:11]).strip()
old_srv = self._services.pop(fav_id, None)
@@ -3162,26 +3278,26 @@ class Application(Gtk.Application):
if self._s_type is not SettingsType.ENIGMA_2:
self.show_error_message("Not allowed in this context!")
return
ServicesUpdateDialog(self._main_window, self._settings, self.on_import_data_from_web).show()
ServicesUpdateDialog(self).show()
@run_idle
def on_import_data_from_web(self, services):
def on_import_data_from_web(self, services, bouquets=None):
msg = "Combine with the current data?"
def clb():
self.show_info_message("Done!")
if len(self._services_model) > 0 and show_dialog(DialogType.QUESTION, self._main_window,
msg) == Gtk.ResponseType.OK:
gen = self.append_imported_data([], services)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
gen = self.append_imported_data(bouquets or [], services, clb)
else:
gen = self.import_data_from_web(services)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
gen = self.import_data_from_web(services, bouquets, clb)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def import_data_from_web(self, services):
def import_data_from_web(self, services, bouquets, callback=None):
self._wait_dialog.show()
if self._app_info_box.get_visible():
yield from self.create_new_configuration(self._s_type)
yield from self.append_services(services)
self.update_sat_positions()
yield True
yield from self.create_new_configuration(self._s_type)
yield from self.append_imported_data(bouquets or [], services, callback)
self._wait_dialog.hide()
# ***************** Export ******************** #
@@ -3306,7 +3422,7 @@ class Application(Gtk.Application):
def on_playback_full_screen(self, box, state):
self._data_paned.set_visible(state)
self._main_window.unfullscreen() if state else self._main_window.fullscreen()
if not IS_GNOME_SESSION:
if not USE_HEADER_BAR:
self._main_window.set_show_menubar(state)
def on_playback_show(self, box):
@@ -3499,7 +3615,7 @@ class Application(Gtk.Application):
elif self._s_type is SettingsType.NEUTRINO_MP:
# It may require some correction for cable and terrestrial channels!
try:
pos, freq = int(self.get_pos_num(srv.pos)) * 10, int(srv.freq)
pos, freq = int(get_pos_num(srv.pos)) * 10, int(srv.freq)
tid, nid, sid = int(ref[: -8], 16), int(ref[-8: -4], 16), int(srv.ssid, 16)
except ValueError:
log(f"Error getting reference for: {srv}")
@@ -3701,16 +3817,7 @@ class Application(Gtk.Application):
elif self._s_type is SettingsType.NEUTRINO_MP:
list(map(lambda s: self._sat_positions.add(s.pos), filter(lambda s: s.pos, self._services.values())))
self.update_filter_sat_positions()
def update_filter_sat_positions(self):
""" Updates the values for the satellite positions button model. """
first = self._filter_sat_pos_model[self._filter_sat_pos_model.get_iter_first()][:]
self._filter_sat_pos_model.clear()
self._filter_sat_pos_model.append((first[0], True))
self._sat_positions.discard(first[0])
list(map(lambda pos: self._filter_sat_pos_model.append((pos, True)),
sorted(self._sat_positions, key=self.get_pos_num, reverse=True)))
update_filter_sat_positions(self._filter_sat_pos_model, self._sat_positions)
@run_with_delay(2)
def on_filter_changed(self, item=None):
@@ -3720,7 +3827,6 @@ class Application(Gtk.Application):
@run_with_delay(2)
def on_iptv_filter_changed(self, item=None):
self._iptv_filter_box.set_sensitive(False)
self.update_iptv_filter_cache()
self.update_iptv_filter_state()
@@ -3732,7 +3838,6 @@ class Application(Gtk.Application):
@run_idle
def update_iptv_filter_state(self):
self._iptv_services_model_filter.refilter()
GLib.idle_add(self._iptv_filter_box.set_sensitive, True)
def update_filter_cache(self):
self._filter_cache.clear()
@@ -3750,7 +3855,7 @@ class Application(Gtk.Application):
r[Column.SRV_PACKAGE],
r[Column.SRV_TYPE],
r[Column.SRV_SSID],
r[Column.SRV_POS])).upper()))
r[Column.SRV_FREQ])).upper()))
def update_iptv_filter_cache(self):
self._iptv_filter_cache.clear()
@@ -3795,16 +3900,7 @@ class Application(Gtk.Application):
self.on_filter_changed()
def update_filter_toggle_model(self, model, toggle, path, values_set):
active = not toggle.get_active()
if path == "0":
model.foreach(lambda m, p, i: m.set_value(i, 1, active))
else:
model.set_value(model.get_iter(path), 1, active)
if active:
model.set_value(model.get_iter_first(), 1, len({r[0] for r in model if r[1]}) == len(model) - 1)
else:
model.set_value(model.get_iter_first(), 1, False)
update_toggle_model(model, path, toggle)
values_set.clear()
values_set.update({r[0] for r in model if r[1]})
self.on_iptv_filter_changed() if self._iptv_button.get_active() else self.on_filter_changed()
@@ -3824,7 +3920,7 @@ class Application(Gtk.Application):
return self.show_error_message("Data loading in progress!")
model, paths = view.get_selection().get_selected_rows()
if is_only_one_item_selected(paths, self._main_window):
if is_only_one_item_selected(paths, self):
model_name = get_base_model(model).get_name()
if model_name == self.FAV_MODEL:
srv_type = model.get_value(model.get_iter(paths), Column.FAV_TYPE)
@@ -3849,7 +3945,7 @@ class Application(Gtk.Application):
def on_bouquets_edit(self, view):
""" Renaming bouquets. """
if not self._bq_selected:
if not self._bq_selected and self._s_type is SettingsType.NEUTRINO_MP:
self.show_error_message("This item is not allowed to edit!")
return
@@ -3857,7 +3953,7 @@ class Application(Gtk.Application):
if paths:
itr = model.get_iter(paths[0])
bq_name, bq_type = model.get(itr, 0, 3)
bq_name, bq_type = model.get(itr, Column.BQ_NAME, Column.BQ_TYPE)
response = show_dialog(DialogType.INPUT, self._main_window, bq_name)
if response == Gtk.ResponseType.CANCEL:
return
@@ -3867,14 +3963,17 @@ class Application(Gtk.Application):
self.show_error_message(get_message("A bouquet with that name exists!"))
return
model.set_value(itr, 0, response)
model.set_value(itr, Column.BQ_NAME, response)
if not model.iter_parent(itr):
return
old_bq_name = f"{bq_name}:{bq_type}"
self._bouquets[bq] = self._bouquets.pop(old_bq_name)
self._bq_file[bq] = self._bq_file.pop(old_bq_name, None)
self._current_bq_name = response
self._bq_name_label.set_text(self._current_bq_name)
self._bq_selected = bq
# services with extra names for the bouquet
# Services with extra names for the bouquet.
ext_bq = self._extra_bouquets.get(old_bq_name, None)
if ext_bq:
self._extra_bouquets[bq] = ext_bq
@@ -3961,6 +4060,26 @@ class Application(Gtk.Application):
if r[Column.FAV_SERVICE] in dup:
r[Column.FAV_BACKGROUND] = self._NEW_COLOR
def on_remove_duplicates(self, item):
exist = set()
to_remove = []
for r in self._fav_model:
fav_id = r[Column.FAV_ID]
if fav_id in exist:
to_remove.append(r.iter)
else:
exist.add(fav_id)
count = len(to_remove)
if count:
if show_dialog(DialogType.QUESTION, self._main_window) != Gtk.ResponseType.OK:
return
gen = self.remove_favs(to_remove, self._fav_model)
GLib.idle_add(lambda: next(gen, False))
self.show_info_message(f"{get_message('Done!')} {get_message('Removed')}: {count}")
else:
self.show_info_message(f"{get_message('Done!')} {get_message('Found')}: {count}")
def on_services_mark_not_in_bouquets(self, item):
if self.is_data_loading():
self.show_error_message("Data loading in progress!")
@@ -4140,8 +4259,7 @@ class Application(Gtk.Application):
self.show_error_message("No bouquets config is loaded. Load or create a new config!")
return
gen_bouquets(self._services_view, self._bouquets_view, self._main_window, g_type, self._s_type,
self.append_bouquet)
gen_bouquets(self, g_type)
# ***************** Alternatives ********************* #
@@ -4296,11 +4414,15 @@ class Application(Gtk.Application):
return True
def on_alt_selection(self, model, path, column):
if self._control_tool and self._control_tool.update_epg:
if self._page is Page.EPG:
row = model[path][:]
srv = self._services.get(row[Column.ALT_FAV_ID], None)
if srv and srv.transponder or row[Column.ALT_TYPE] == BqServiceType.IPTV.name:
self._control_tool.on_service_changed(srv.picon_id.rstrip(".png").replace("_", ":"))
ref = self.get_service_ref_data(srv)
if not ref:
return
self.emit("fav-changed", ref)
# ***************** Profile label ********************* #
@@ -4325,7 +4447,7 @@ class Application(Gtk.Application):
self.show_info_message(message, Gtk.MessageType.ERROR)
@run_idle
def show_info_message(self, text, message_type):
def show_info_message(self, text, message_type=Gtk.MessageType.INFO):
self._info_bar.set_visible(False)
self._info_label.set_text(get_message(text))
self._info_bar.set_message_type(message_type)

View 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
@@ -32,18 +32,19 @@ __all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "
"scroll_to", "get_base_model", "get_base_paths", "copy_reference", "assign_picons", "remove_picon",
"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")
"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")
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
@@ -545,13 +546,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
@@ -564,6 +565,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. """
@@ -573,44 +587,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"{get_message('Selected type:')} '{cond}'\n\n{get_message('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):
@@ -626,12 +685,28 @@ def get_bouquets_names(model):
return bouquets_names
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)
@@ -697,6 +772,44 @@ def get_model_data(view):
return model_name, model
def update_toggle_model(model, path, toggle):
""" Updates the toggle state for the model. """
active = not toggle.get_active()
if path == "0":
model.foreach(lambda m, p, i: m.set_value(i, 1, active))
else:
model.set_value(model.get_iter(path), 1, active)
if active:
model.set_value(model.get_iter_first(), 1, len({r[0] for r in model if r[1]}) == len(model) - 1)
else:
model.set_value(model.get_iter_first(), 1, False)
def update_popup_filter_model(model, elements: set):
first = model[model.get_iter_first()][:]
model.clear()
model.append((first[0], True))
elements.discard(first[0])
def update_filter_sat_positions(model, sat_positions):
""" Updates the values for the satellite positions button model. """
update_popup_filter_model(model, sat_positions)
list(map(lambda pos: model.append((pos, True)), sorted(sat_positions, key=get_pos_num, reverse=True)))
def get_pos_num(pos):
""" Returns num [float] representation of satellite position. """
if not pos:
return -183.0
if len(pos) > 1:
m = -1 if pos[-1] == "W" else 1
return float(pos[:-1]) * m
return -181.0 if pos == "T" else -182.0
def append_text_to_tview(char, view):
""" Appending text and scrolling to a given line in the text view. """
buf = view.get_buffer()
@@ -720,7 +833,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())

View File

@@ -3,7 +3,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
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkMenu" id="add_menu">
<property name="visible">True</property>
@@ -379,6 +379,7 @@ Author: Dmitriy Yefremov
<child type="center">
<object class="GtkButtonBox" id="header_button_box">
<property name="visible">True</property>
<property name="name">header-stack-switcher</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="layout_style">expand</property>
@@ -640,7 +641,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
<property name="model">picons_src_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">0</property>
@@ -665,7 +665,6 @@ Author: Dmitriy Yefremov
<property name="alignment">0.49000000953674316</property>
<child>
<object class="GtkCellRendererPixbuf" id="picons_src_renderer">
<property name="height">50</property>
<property name="ypad">5</property>
</object>
<attributes>
@@ -778,7 +777,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
<property name="model">picons_dst_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">0</property>
@@ -804,7 +802,6 @@ Author: Dmitriy Yefremov
<property name="alignment">0.49000000953674316</property>
<child>
<object class="GtkCellRendererPixbuf" id="picons_dest_renderer">
<property name="height">50</property>
<property name="ypad">5</property>
</object>
<attributes>

View 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
@@ -33,7 +33,7 @@ from enum import Enum
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
@@ -43,7 +43,7 @@ from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_t
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message, 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,8 +52,8 @@ 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-receive", self.on_download)
@@ -291,7 +291,7 @@ 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 update_picons_from_file(self, view, uri):
""" Adds picons in the view on dragging from file system. """
@@ -303,18 +303,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 +380,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 +418,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)
@@ -602,10 +596,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 +610,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
@@ -699,20 +693,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!")
@@ -949,7 +938,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

View 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
@@ -34,17 +34,17 @@ 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
from app.settings import PlayStreamsMode, IS_DARWIN, SettingsType, USE_HEADER_BAR
from app.tools.media import Player
from app.ui.dialogs import get_builder, get_message
from app.ui.main_helper import get_iptv_url
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, IS_GNOME_SESSION, Page
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, Page
class PlayerBox(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, app, **kwargs):
super().__init__(**kwargs)
# Signals.
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
@@ -54,6 +54,8 @@ class PlayerBox(Gtk.Box):
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("pause", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
self._app = app
self._app.connect("fav-clicked", self.on_fav_clicked)
@@ -106,6 +108,9 @@ class PlayerBox(Gtk.Box):
if mode is not FavClickMode.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()
@@ -189,7 +194,7 @@ class PlayerBox(Gtk.Box):
video_menu = builder.get_object("video_menu")
subtitle_menu = builder.get_object("subtitle_menu")
if not IS_GNOME_SESSION:
if not USE_HEADER_BAR:
menu_bar = self._app.get_menubar()
menu_bar.insert_section(1, None, audio_menu)
menu_bar.insert_section(2, None, video_menu)
@@ -284,7 +289,9 @@ class PlayerBox(Gtk.Box):
def on_press(self, area, event):
if event.button == Gdk.BUTTON_PRIMARY:
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
if event.type == Gdk.EventType.BUTTON_PRESS:
self.emit("pause", None)
elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_full_screen()
def on_key_press(self, widget, event):

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2
Copyright (C) 2018-2022 Dmitriy Yefremov
Copyright (C) 2018-2023 Dmitriy Yefremov
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
@@ -12,12 +12,12 @@ Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.20"/>
<requires lib="gtk+" version="3.22"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type all_permissive -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAdjustment" id="font_size_adjustment">
<property name="lower">8</property>
@@ -79,7 +79,7 @@ Author: Dmitriy Yefremov
<property name="image">set_default_image</property>
<property name="use-stock">False</property>
<signal name="activate" handler="on_profile_set_default" swapped="no"/>
<accelerator key="d" signal="activate"/>
<accelerator key="d" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
@@ -291,7 +291,7 @@ Author: Dmitriy Yefremov
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Save</property>
<property name="valign">center</property>
<accelerator key="s" signal="clicked"/>
<accelerator key="s" signal="clicked" modifiers="Primary"/>
</object>
<packing>
<property name="expand">False</property>
@@ -508,7 +508,7 @@ Author: Dmitriy Yefremov
<property name="icon-name">emblem-default</property>
</object>
</child>
<accelerator key="d" signal="clicked"/>
<accelerator key="d" signal="clicked" modifiers="Primary"/>
</object>
<packing>
<property name="expand">False</property>
@@ -1671,47 +1671,137 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkFrame" id="load_on_startup_frame">
<object class="GtkFrame" id="program_options_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="load_on_startup_box">
<object class="GtkBox" id="program_options_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="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="load_on_startup_label">
<object class="GtkBox" id="load_on_startup_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Load the last open configuration at program startup</property>
<child>
<object class="GtkLabel" id="load_on_startup_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Load the last open configuration at program startup</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="load_on_startup_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="load_on_startup_switch">
<object class="GtkBox" id="enable_extensions_box">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">EXPERIMENTAL!</property>
<child>
<object class="GtkLabel" id="enable_extensions_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Enable extensions support</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="enable_extensions_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="enable_http_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="enable_http_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Enable HTTP API</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="support_http_api_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">end</property>
<signal name="state-set" handler="on_http_mode_switch" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
@@ -1882,59 +1972,6 @@ Author: Dmitriy Yefremov
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="http_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.019999999552965164</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="enable_http_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>
<child>
<object class="GtkLabel" id="enable_http_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Enable HTTP API</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="support_http_api_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">end</property>
<signal name="state-set" handler="on_http_mode_switch" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="name">program</property>
@@ -2737,10 +2774,7 @@ Author: Dmitriy Yefremov
<object class="GtkGrid" id="bq_naming_grid">
<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="border-width">5</property>
<property name="row-spacing">5</property>
<property name="column-spacing">5</property>
<child>
@@ -2950,16 +2984,13 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="experimental_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="border-width">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="v5_support_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive" bind-source="enable_experimental_switch" bind-property="active">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel">
@@ -2989,6 +3020,46 @@ Author: Dmitriy Yefremov
</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="unlimited_buffer_box">
<property name="visible">True</property>
<property name="sensitive" bind-source="enable_experimental_switch" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enables unlimited copy buffer for the bouquets tab.</property>
<child>
<object class="GtkLabel" id="unlimited_buffer_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Enable unlimited copy buffer</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="unlimited_buffer_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
@@ -2998,7 +3069,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="yt_dl_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive" bind-source="enable_experimental_switch" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
@@ -3064,7 +3135,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="yt_dl_update_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive" bind-source="enable_yt_dl_switch" bind-property="active">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="auto_update_yt_dl_label">
@@ -3104,13 +3175,13 @@ 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>
<child>
<object class="GtkBox" id="enable_direct_playback_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive" bind-source="enable_experimental_switch" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enables direct sending and playback of media links on the receiver</property>
<child>
@@ -3144,7 +3215,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
</object>
@@ -3729,7 +3800,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="epg_dat_label">
<property name="visible">True</property>
<property name="sensitive" bind-source="enigma_radio_button" bind-property="active">False</property>
<property name="sensitive" bind-source="enigma_radio_button" bind-property="active">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">EPG *.dat file:</property>
<property name="xalign">0.019999999552965164</property>
@@ -3742,7 +3813,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkComboBoxText" id="epg_dat_box">
<property name="visible">True</property>
<property name="sensitive" bind-source="enigma_radio_button" bind-property="active">False</property>
<property name="sensitive" bind-source="enigma_radio_button" bind-property="active">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="active">0</property>

View 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
@@ -35,7 +35,7 @@ from app.connections import test_telnet, test_ftp, TestException, test_http, Htt
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, IS_GNOME_SESSION
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, HeaderBar
class SettingsDialog:
@@ -180,16 +180,13 @@ class SettingsDialog:
self._compress_picons_switch = builder.get_object("compress_picons_switch")
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
self._support_ver5_switch = builder.get_object("support_ver5_switch")
self._unlimited_buffer_switch = builder.get_object("unlimited_buffer_switch")
self._enable_extensions_switch = builder.get_object("enable_extensions_switch")
self._support_http_api_switch = builder.get_object("support_http_api_switch")
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
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")
# EXPERIMENTAL.
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
self._enable_exp_switch.bind_property("active", builder.get_object("yt_dl_box"), "sensitive")
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
# 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")
@@ -211,13 +208,14 @@ class SettingsDialog:
[self.init_element_style(el, screen, style_provider) for el in self._digit_elems]
self.init_element_style(self._host_field, screen, style_provider)
if IS_GNOME_SESSION:
if self._settings.use_header_bar:
switcher = builder.get_object("main_stack_switcher")
switcher.set_margin_top(0)
switcher.set_margin_bottom(0)
builder.get_object("main_box").remove(switcher)
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
header_bar = HeaderBar()
header_bar.set_custom_title(switcher)
self._dialog.set_titlebar(header_bar)
self.init_ui_elements()
@@ -347,6 +345,8 @@ class SettingsDialog:
if self._s_type is SettingsType.ENIGMA_2:
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
self._support_ver5_switch.set_active(self._settings.v5_support)
self._unlimited_buffer_switch.set_active(self._settings.unlimited_copy_buffer)
self._enable_extensions_switch.set_active(self._settings.extensions_support)
self._use_http_switch.set_active(self._settings.use_http)
self._remove_unused_bq_switch.set_active(self._settings.remove_unused_bouquets)
self._keep_power_mode_switch.set_active(self._settings.keep_power_mode)
@@ -430,6 +430,8 @@ class SettingsDialog:
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
self._ext_settings.unlimited_copy_buffer = self._unlimited_buffer_switch.get_active()
self._ext_settings.extensions_support = self._enable_extensions_switch.get_active()
self._ext_settings.use_http = self._use_http_switch.get_active()
self._ext_settings.remove_unused_bouquets = self._remove_unused_bq_switch.get_active()
self._ext_settings.keep_power_mode = self._keep_power_mode_switch.get_active()
@@ -516,6 +518,7 @@ class SettingsDialog:
def on_experimental_switch(self, switch, state):
if not state:
self._support_ver5_switch.set_active(state)
self._unlimited_buffer_switch.set_active(state)
self._enable_send_to_switch.set_active(state)
self._enable_yt_dl_switch.set_active(state)
@@ -645,11 +648,13 @@ class SettingsDialog:
if response is Gtk.ResponseType.CANCEL:
return
if response in self._settings.picons_paths:
sep = "/"
path = response if response.endswith(sep) else response + sep
if path in self._settings.picons_paths:
self.show_info_message("This path already exists!", Gtk.MessageType.ERROR)
return
path = response if response.endswith(SEP) else response + SEP
model = self._picons_paths_box.get_model()
model.append((path, path))
self._picons_paths_box.set_active_id(path)

View File

@@ -18,11 +18,24 @@
padding: 0;
}
#stack-switch-button {
#header-button {
padding-top: 0;
padding-bottom: 0;
}
#header-entry {
min-height: 0;
}
#header-stack-switcher > button {
padding-top: 0;
padding-bottom: 0;
}
buttonbox {
padding: 0;
}
paned > separator {
background-repeat: no-repeat;
background-position: center;

View File

@@ -31,9 +31,10 @@ from datetime import datetime, timedelta
from enum import Enum
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 .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, MOD_MASK
from ..commons import run_idle, log
from ..connections import HttpAPI
from ..eparser.ecommons import BqServiceType
@@ -56,7 +57,7 @@ class TimerTool(Gtk.Box):
class TimerDialog(Gtk.Dialog):
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
super().__init__(use_header_bar=IS_GNOME_SESSION, *args, **kwargs)
super().__init__(use_header_bar=USE_HEADER_BAR, *args, **kwargs)
self._action = action or TimerTool.TimerAction.ADD
self._timer_data = timer_data or {}

View 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
@@ -52,7 +52,6 @@ TEXT_DOMAIN = "demon-editor"
NOTIFY_IS_INIT = False
APP_FONT = None
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
try:
settings = Settings.get_instance()
@@ -162,6 +161,18 @@ def show_notification(message, timeout=10000, urgency=1):
notify.show()
class HeaderBar(Gtk.HeaderBar):
""" Custom header bar widget. """
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_visible(True)
self.set_show_close_button(True)
if IS_DARWIN:
self.set_decoration_layout("close,minimize,maximize")
class Page(Enum):
""" Main stack widget page. """
INFO = "info"
@@ -269,6 +280,14 @@ class Column(IntEnum):
IPTV_FAV_ID = 5
IPTV_PICON_ID = 6
IPTV_TOOLTIP = 7
# EPG view
EPG_SERVICE = 0
EPG_TITLE = 1
EPG_START = 2
EPG_END = 3
EPG_LENGTH = 4
EPG_DESC = 5
EPG_DATA = 6
def __index__(self):
""" Overridden to get the index in slices directly """

View 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
@@ -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,12 +40,15 @@ 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)
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 USE_HEADER_BAR, 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 ..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, IS_GNOME_SESSION
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HeaderBar
_DIALOGS_UI_PATH = f"{UI_RESOURCES_PATH}xml{os.sep}dialogs.glade"
@@ -60,7 +65,7 @@ class DVBDialog(Gtk.Dialog):
skip_taskbar_hint=True,
skip_pager_hint=True,
destroy_with_parent=True,
use_header_bar=IS_GNOME_SESSION,
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)
@@ -407,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")
@@ -432,9 +436,10 @@ class UpdateDialog:
update_button = builder.get_object("sat_update_button")
self._sat_view.bind_property("sensitive", update_button, "sensitive")
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
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,8 +461,36 @@ 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")
window_size = self._settings.get(self._size_name)
if self._settings.use_header_bar:
header_bar = HeaderBar()
builder.get_object("sat_update_header").set_visible(False)
header_box = builder.get_object("satellites_update_header_box")
header_box.remove(self._source_box)
header_bar.pack_start(self._source_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)
# 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)
@@ -483,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()
@@ -512,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):
@@ -611,10 +647,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. """
@@ -624,6 +683,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(get_message("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):
@@ -639,6 +708,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"
@@ -658,10 +728,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]
@@ -674,11 +773,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)
@@ -687,16 +814,16 @@ 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))
@@ -714,6 +841,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(get_message("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(get_message("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):
@@ -723,6 +868,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 get_message("KingOfSat only!"))
@run_task
def receive_services(self):
self.is_download = True
@@ -791,6 +944,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:
@@ -801,10 +955,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
@@ -817,10 +1008,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)

View 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
@@ -37,9 +37,9 @@ 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 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, get_message, 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,8 +56,8 @@ 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-save", self.on_save)
@@ -162,8 +162,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))

View File

@@ -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
@@ -27,11 +27,11 @@ Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="gtk+" version="3.22"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="cable_model">
<columns>
@@ -65,19 +65,19 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkImage" id="popup_menu_add_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="stock">gtk-add</property>
</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="add_sat_popup_menu_item">
<property name="label" translatable="yes">Add</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="image">popup_menu_add_image</property>
<property name="use_stock">False</property>
<property name="use-stock">False</property>
<signal name="activate" handler="on_add" swapped="no"/>
<accelerator key="Insert" signal="activate"/>
</object>
@@ -85,16 +85,16 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkSeparatorMenuItem" id="popup_sat_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="edit_sat_popup_menu_item">
<property name="label">gtk-edit</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<property name="can-focus">False</property>
<property name="use-underline">True</property>
<property name="use-stock">True</property>
<signal name="activate" handler="on_edit" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
@@ -102,16 +102,16 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkSeparatorMenuItem" id="popup_sat_menu_separator_2">
<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_sat_popup_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_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
@@ -119,19 +119,19 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkImage" id="popup_menu_add_image_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="stock">gtk-add</property>
</object>
<object class="GtkMenu" id="transponder_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImageMenuItem" id="add_tr_popup_menu_item">
<property name="label" translatable="yes">Add</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="image">popup_menu_add_image_2</property>
<property name="use_stock">False</property>
<property name="use-stock">False</property>
<signal name="activate" handler="on_transponder_add" swapped="no"/>
<accelerator key="Insert" signal="activate"/>
</object>
@@ -139,16 +139,16 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkSeparatorMenuItem" id="popup_tr_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="edit_tr_popup_menu_item">
<property name="label">gtk-edit</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<property name="can-focus">False</property>
<property name="use-underline">True</property>
<property name="use-stock">True</property>
<signal name="activate" handler="on_transponder_edit" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
@@ -156,16 +156,16 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkSeparatorMenuItem" id="popup_tr_menu_separator_2">
<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_tr_popup_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_transponder_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
@@ -255,45 +255,58 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkPaned" id="main_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="editor_sat_frame">
<property name="width_request">360</property>
<property name="width-request">360</property>
<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">in</property>
<child>
<object class="GtkBox" id="editor_sat_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="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="editor_header_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="margin_bottom">5</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="margin-bottom">5</property>
<property name="spacing">5</property>
<child type="center">
<object class="GtkStackSwitcher" id="stack_switcher">
<property name="name">header-stack-switcher</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stack">sat_stack</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_header_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add</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_add" swapped="no"/>
<child>
<object class="GtkImage" id="add_satellite_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-new-symbolic</property>
<property name="can-focus">False</property>
<property name="icon-name">document-new-symbolic</property>
</object>
</child>
</object>
@@ -306,15 +319,15 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkButton" id="update_header_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Update</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Update</property>
<signal name="clicked" handler="on_update" swapped="no"/>
<child>
<object class="GtkImage" id="update_header_button_img">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-synchronizing-symbolic</property>
<property name="can-focus">False</property>
<property name="icon-name">emblem-synchronizing-symbolic</property>
</object>
</child>
</object>
@@ -324,18 +337,6 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child type="center">
<object class="GtkStackSwitcher" id="stack_switcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stack">sat_stack</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>
@@ -346,26 +347,26 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkStack" id="sat_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<signal name="notify::visible-child-name" handler="on_visible_page" swapped="no"/>
<child>
<object class="GtkBox" id="satellite_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Satellites</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Satellites</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="satellite_view_scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="satellite_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="model">satellite_model</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
<property name="rubber-banding">True</property>
<property name="activate-on-single-click">True</property>
<signal name="button-press-event" handler="on_button_press" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_satellite_selection" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
@@ -392,7 +393,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="sat_pos_column">
<property name="fixed_width">85</property>
<property name="fixed-width">85</property>
<property name="title" translatable="yes">Pos</property>
<property name="alignment">0.49000000953674316</property>
<child>
@@ -416,17 +417,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="sat_status_box">
<property name="height_request">26</property>
<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="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="sat_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -437,9 +438,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="sat_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -464,22 +465,22 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="terrestrial_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Terrestrial</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Terrestrial</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="terrestrial_view_scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="terrestrial_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="model">terrestrial_model</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</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_button_press" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_terrestrial_selection" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
@@ -515,17 +516,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="ter_status_box">
<property name="height_request">26</property>
<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="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="ter_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -536,9 +537,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="ter_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -564,22 +565,22 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="cable_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Cable</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Cable</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="cable_view_scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="cable_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="model">cable_model</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</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_button_press" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cable_selection" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
@@ -615,17 +616,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="cable_status_box">
<property name="height_request">26</property>
<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="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="cable_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -636,9 +637,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="cable_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -673,7 +674,7 @@ Author: Dmitriy Yefremov
<child type="label">
<object class="GtkLabel" id="editor_sat_frame_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">DVB</property>
</object>
</child>
@@ -686,39 +687,39 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkFrame" id="editor_transponder_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">in</property>
<child>
<object class="GtkBox" id="transponders_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="can-focus">False</property>
<property name="margin-left">5</property>
<property name="margin-right">5</property>
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="editor_tr_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<property name="margin_top">2</property>
<property name="margin_bottom">5</property>
<property name="can-focus">False</property>
<property name="margin-left">15</property>
<property name="margin-right">15</property>
<property name="margin-top">2</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="transponder_add_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add</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_transponder_add" swapped="no"/>
<child>
<object class="GtkImage" id="add_transponder_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-new-symbolic</property>
<property name="can-focus">False</property>
<property name="icon-name">document-new-symbolic</property>
</object>
</child>
</object>
@@ -741,25 +742,25 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkStack" id="transponders_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="sat_tr_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="GtkScrolledWindow" id="sat_tr_view_scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="sat_tr_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="model">sat_tr_view_model</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="search-column">0</property>
<property name="rubber-banding">True</property>
<property name="enable-grid-lines">both</property>
<signal name="button-press-event" handler="on_tr_button_press" object="transponder_popup_menu" swapped="no"/>
<signal name="key-press-event" handler="on_tr_key_press" swapped="no"/>
<child internal-child="selection">
@@ -770,7 +771,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="freq_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Freq</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -787,7 +788,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="rate_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Rate</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -804,7 +805,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="pol_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Pol</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -821,7 +822,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="fec_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">FEC</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -838,7 +839,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="sys_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">System</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -855,7 +856,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="mod_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Mod</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -934,17 +935,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="sat_tr_status_box">
<property name="height_request">26</property>
<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="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="sat_tr_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -955,9 +956,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="sat_tr_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -982,21 +983,21 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="ter_tr_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="GtkScrolledWindow" id="ter_tr_view_scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="ter_tr_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="model">ter_tr_view_model</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="search-column">0</property>
<property name="rubber-banding">True</property>
<property name="enable-grid-lines">both</property>
<signal name="button-press-event" handler="on_tr_button_press" object="transponder_popup_menu" swapped="no"/>
<signal name="key-press-event" handler="on_tr_key_press" swapped="no"/>
<child internal-child="selection">
@@ -1007,7 +1008,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ter_freq_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Freq</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1024,7 +1025,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ter_system_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">System</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1041,7 +1042,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ter_bandwidth_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Bandwidth</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1058,7 +1059,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ter_constellation_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Constellation</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1075,7 +1076,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ter_rate_hp_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">SR (HP)</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1106,7 +1107,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="ter_guard_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Guard</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1176,17 +1177,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="ter_tr_status_box">
<property name="height_request">26</property>
<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="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="ter_tr_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1197,9 +1198,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="ter_tr_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -1225,21 +1226,21 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="cable_tr_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="GtkScrolledWindow" id="cable_tr_view_scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="cable_tr_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="model">cable_tr_view_model</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="search-column">0</property>
<property name="rubber-banding">True</property>
<property name="enable-grid-lines">both</property>
<signal name="button-press-event" handler="on_tr_button_press" object="transponder_popup_menu" swapped="no"/>
<signal name="key-press-event" handler="on_tr_key_press" swapped="no"/>
<child internal-child="selection">
@@ -1250,7 +1251,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="cable_freq_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Freq</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1267,7 +1268,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="cable_rate_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Rate</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1284,7 +1285,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="cable_fec_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">FEC</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1301,7 +1302,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeViewColumn" id="cable_mod_column">
<property name="resizable">True</property>
<property name="min_width">20</property>
<property name="min-width">20</property>
<property name="title" translatable="yes">Mod</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
@@ -1326,17 +1327,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="cable_tr_status_box">
<property name="height_request">26</property>
<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="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="cable_tr_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1347,9 +1348,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkLabel" id="cable_tr_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -1384,7 +1385,7 @@ Author: Dmitriy Yefremov
<child type="label">
<object class="GtkLabel" id="editor_transponder_frame_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Transponder</property>
</object>
</child>

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
#!/bin/bash
VER="3.2.1_Beta"
VER="3.6.2_Beta"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"
mkdir -p $B_PATH
cp -TRv deb $B_PATH
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
rsync --exclude=__pycache__ -arv ../../extensions $DEB_PATH
cd dist
fakeroot dpkg-deb -Zxz --build DemonEditor

View File

@@ -1,5 +1,5 @@
Package: demon-editor
Version: 3.2.1-Beta
Version: 3.6.2-Beta
Section: utils
Priority: optional
Architecture: all

View File

@@ -24,7 +24,8 @@ ui_files = [('app/ui/*.glade', 'ui'),
('app/ui/epg/*.glade', 'ui/epg'),
('app/ui/xml/*.glade', 'ui/xml'),
('app/ui/lang*', 'share/locale'),
('app/ui/icons*', 'share/icons')
('app/ui/icons*', 'share/icons'),
('extensions/*', 'extensions')
]
a = Analysis([EXE_NAME],
@@ -80,8 +81,8 @@ app = BUNDLE(coll,
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"3.2.1.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2022, Dmitriy Yefremov",
'CFBundleShortVersionString': f"3.6.2.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2023, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false',
'NSHighResolutionCapable': 'true'
})

View File

@@ -21,7 +21,8 @@ ui_files = [('app\\ui\\*.glade', 'ui'),
('app\\ui\\epg\\*.glade', 'ui\\epg'),
('app\\ui\\xml\\*.glade', 'ui\\xml'),
('app\\ui\\lang*', 'share\\locale'),
('app\\ui\\icons*', 'share\\icons')
('app\\ui\\icons*', 'share\\icons'),
('extensions\\*', 'extensions')
]

10
extensions/README.md Normal file
View File

@@ -0,0 +1,10 @@
Extension packages must be located in the following paths:
``` app/ui/extensions```, ``` your data path/tools/extensions ```.
For builds:
``` Program Root\extensions ```
Extensions and examples can be found [here](https://github.com/DYefremov/demoneditor-extensions).
The possibilities of extending the API, as well as the creation and publication of the necessary extensions, can be discussed there.
### Pull requests for extensions are not accepted here!

104
extensions/__init__.py Normal file
View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 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
#
import json
import logging
import os
from pathlib import Path
CONFIG_PATH = f"{Path.home()}{os.sep}.config{os.sep}demon-editor{os.sep}extensions{os.sep}"
class Singleton(type):
_INSTANCE = None
def __call__(cls, *args, **kwargs):
if not cls._INSTANCE:
cls._INSTANCE = type.__call__(cls, *args, **kwargs)
return cls._INSTANCE
class BaseExtension(metaclass=Singleton):
""" Base extension (plugin) class. """
# 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"
def __init__(self, app):
# Current application instance.
# It can be used all public methods, properties or signals.
self.app = app
self._config_path = f"{CONFIG_PATH}{self.__class__.__name__}{os.sep}config"
self.log(f"Extension initialized...")
def exec(self):
""" Triggers an action for the given extension.
E.g. shows a dialog or runs an external script.
"""
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}")
def reset_config(self):
path = Path(self._config_path)
if path.is_file():
path.unlink()
@property
def config(self) -> dict:
if not Path(self._config_path).is_file():
return {}
with open(self._config_path, "r", encoding="utf-8") as config_file:
try:
return json.load(config_file)
except ValueError as e:
self.log(f"Configuration load error: {e}")
return {}
@config.setter
def config(self, value: dict):
Path(self._config_path).parent.mkdir(parents=True, exist_ok=True)
with open(self._config_path, "w", encoding="utf-8") as config_file:
json.dump(value, config_file, indent=" ")
if __name__ == "__main__":
pass

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 Dmitriy Yefremov
# Copyright (C) 2018-2023 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -1416,3 +1416,75 @@ msgstr "Здаліць сцяжок \"New\""
msgid "Group by"
msgstr "Групаваць па"
msgid "Replace existing"
msgstr "Замяніць існуючыя"
msgid "Already exists"
msgstr "Ужо існуе"
msgid "Enable unlimited copy buffer"
msgstr "Уключыць неабмежаваны буфер капіявання"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Улучае неабмежаваны буфер капіявання для ўкладкі букетаў."
msgid "Start time"
msgstr "Пачатак"
msgid "End time"
msgstr "Сканчэнне"
msgid "Enable extensions support"
msgstr "Уключыць падтрымку пашырэнняў"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Пасля загрузкі змен можа запатрабавацца перазапуск рэсівера!"
msgid "Remove duplicates"
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 "Абраны тып:"

View File

@@ -2,8 +2,8 @@
# This file is distributed under the MIT license.
#
# Charly, 2019.
# Dmitriy Yefremov, 2020-2021.
# Thomas Schmidt, 2021
# Dmitriy Yefremov, 2020-2023.
# Thomas Schmidt, 2021.
msgid ""
msgstr ""
"Last-Translator: Dmitriy Yefremov\n"
@@ -1430,3 +1430,75 @@ msgstr "Das \"Neu\"-Flag entfernen"
msgid "Group by"
msgstr "Gruppieren nach"
msgid "Replace existing"
msgstr "Vorhandene ersetzen"
msgid "Already exists"
msgstr "Bereits vorhanden"
msgid "Enable unlimited copy buffer"
msgstr "Unbegrenzte Zwischenablage aktivieren"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Aktiviert unbegrenzte Zwischenablage für die Bouquets-Tab."
msgid "Start time"
msgstr "Anfangszeit"
msgid "End time"
msgstr "Endzeit"
msgid "Enable extensions support"
msgstr "Erweiterungen Unterstützung aktivieren"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Nach dem Hochladen der Änderungen muss den Receiver eventuell komplett neu starten!"
msgid "Remove duplicates"
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:"

View File

@@ -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 ""
@@ -1441,3 +1441,57 @@ 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."

View File

@@ -2,11 +2,11 @@
# This file is distributed under the MIT license.
#
#
# Massimo Pissarello <mapi68@gmail.com>, 2022.
# Massimo Pissarello <mapi68@gmail.com>, 2022, 2023.
msgid ""
msgstr ""
"Project-Id-Version: \n"
"PO-Revision-Date: 2022-12-14 06:37+0100\n"
"PO-Revision-Date: 2023-04-13 02:15+0200\n"
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
"Language-Team: Italian <>\n"
"Language: it\n"
@@ -14,7 +14,7 @@ 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.0\n"
"X-Generator: Lokalize 22.12.3\n"
msgid "translator-credits"
msgstr "Massimo Pissarello"
@@ -1230,6 +1230,9 @@ msgstr "Dimensione"
msgid "Date"
msgstr "Data"
msgid "Attr."
msgstr "Attr"
msgid "Toggle display position"
msgstr "Commuta posizione visualizzazione"
@@ -1472,3 +1475,78 @@ msgstr "Rimuovi il flag \"Nuovo\""
msgid "Group by"
msgstr "Raggruppa per"
msgid "Replace existing"
msgstr "Sostituisci esistente"
msgid "Already exists"
msgstr "Esiste già"
msgid "Enable unlimited copy buffer"
msgstr "Abilita buffer di copia illimitato"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Abilita buffer di copia illimitato per la scheda bouquet."
msgid "Start time"
msgstr "Ora di inizio"
msgid "End time"
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:"

View File

@@ -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.
@@ -1401,3 +1401,57 @@ 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."

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2022 Dmitriy Yefremov
# Copyright (C) 2018-2023 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -1413,3 +1413,75 @@ msgstr "Очистить флаг \"New\""
msgid "Group by"
msgstr "Группировать по"
msgid "Replace existing"
msgstr "Заменить существующие"
msgid "Already exists"
msgstr "Уже существует"
msgid "Enable unlimited copy buffer"
msgstr "Включить неограниченный буфер копирования"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Включает неограниченный буфер копирования для вкладки букетов."
msgid "Start time"
msgstr "Начало"
msgid "End time"
msgstr "Окончание"
msgid "Enable extensions support"
msgstr "Включить поддержку расширений"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "После загрузки изменений может потребоваться перезапуск ресивера!"
msgid "Remove duplicates"
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 "Выбран тип:"

View File

@@ -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: 2022-08-27 23:17+0300\n"
"PO-Revision-Date: 2023-04-22 16:34+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.0.1\n"
"X-Generator: Poedit 3.2.2\n"
msgid "translator-credits"
msgstr "audi06_19 <info@dreamosat-forum.com>"
@@ -1416,3 +1416,106 @@ msgstr "Günlük"
msgid "Assign reference"
msgstr "Referans ata"
msgid "Specify hostname or IP address"
msgstr "Ana bilgisayar adını veya IP adresini belirtin"
msgid "Default selection"
msgstr "Varsayılan seçim"
msgid "Don't change power state"
msgstr "Güç durumunu değiştirme"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Buketleri ve servisleri güncellerken bekleme moduna geçmeyin."
msgid "Region"
msgstr "Bölge"
msgid "Provider"
msgstr "Sağlayıcı"
msgid ""
"Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr ""
"Çok sayıda picon (> 1000) seçilirse arşiv olarak yüklemeyi etkinleştirir.\n"
" Yalnızca harici depolamanız varsa önerilir."
msgid "Clear \"New\" flag"
msgstr "\"Yeni\" bayrağını temizleyin"
msgid "Group by"
msgstr "Göre gruplandır"
msgid "Replace existing"
msgstr "Mevcut olanı değiştir"
msgid "Already exists"
msgstr "Zaten var"
msgid "Enable unlimited copy buffer"
msgstr "Sınırsız kopya arabelleğini etkinleştir"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Buketler sekmesi için sınırsız kopya arabelleğini etkinleştirir."
msgid "Start time"
msgstr "Başlangıç saati"
msgid "End time"
msgstr "Bitiş zamanı"
msgid "Enable extensions support"
msgstr "Uzantı desteğini etkinleştir"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Değişiklikleri yükledikten sonra, alıcıyı tamamen yeniden başlatmanız gerekebilir!"
msgid "Remove duplicates"
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:"