mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-09 10:37:17 +02:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a289b9fb53 | ||
|
|
a735b3db13 | ||
|
|
e57e8f9dd7 | ||
|
|
300e0a38c3 | ||
|
|
5eb6f8d63f | ||
|
|
3ec4817665 | ||
|
|
55bb7b2f45 | ||
|
|
a3cf5edd5e | ||
|
|
4f8f6cfcc9 | ||
|
|
ef97cd365c | ||
|
|
4b27e81f5e | ||
|
|
408753f124 | ||
|
|
c3bd3cd2bb | ||
|
|
a15666204c | ||
|
|
1e5c47c3cc | ||
|
|
bc94bb3505 | ||
|
|
6cfb72b1d7 | ||
|
|
3c5144134c | ||
|
|
4fd2939d2e | ||
|
|
68b5fcfd39 | ||
|
|
9fcc1e5f90 | ||
|
|
92b028dc16 | ||
|
|
665de22549 | ||
|
|
5036ecee37 | ||
|
|
e755e9eb6e | ||
|
|
305323726f | ||
|
|
7b3ae01253 | ||
|
|
08fb55ea54 | ||
|
|
f1ee406818 | ||
|
|
8aa9d0797f | ||
|
|
8d61663d3e | ||
|
|
3a9ca26619 | ||
|
|
228466d523 | ||
|
|
e9b3b3f374 | ||
|
|
05b1619d1e | ||
|
|
9a2a2b49f6 | ||
|
|
a034d0476d | ||
|
|
0cf2f070b0 | ||
|
|
dd70dffa1c | ||
|
|
5902b207e1 | ||
|
|
b23319ba52 | ||
|
|
cb8ec264cc | ||
|
|
9fc07308ab | ||
|
|
5f0f1e4e64 | ||
|
|
3fe78e0292 | ||
|
|
0ae9123d98 | ||
|
|
7da3c0fd94 | ||
|
|
b584126bff | ||
|
|
fb173449c6 | ||
|
|
64de141807 | ||
|
|
cab20f744f | ||
|
|
9d44b8002c | ||
|
|
d4389e8b0a | ||
|
|
57696a2460 | ||
|
|
8383214e15 | ||
|
|
56f1c75a41 | ||
|
|
f7be210b85 | ||
|
|
7e0b694a4c | ||
|
|
9a24cae626 | ||
|
|
6e459f80bd | ||
|
|
c55645e5db | ||
|
|
d6796bc5a5 | ||
|
|
4ac04e5401 | ||
|
|
3c72f0cc3c | ||
|
|
4b940b7135 | ||
|
|
0aa9eaa401 | ||
|
|
de8445d55a | ||
|
|
8153c5e6d6 | ||
|
|
51fd013e0f | ||
|
|
872f3f0f81 | ||
|
|
95aa8aaed6 | ||
|
|
b18207b376 | ||
|
|
8f840ed3f5 | ||
|
|
6841090cc0 | ||
|
|
4e700ca1e8 | ||
|
|
3312576416 | ||
|
|
b047153591 | ||
|
|
b15cae8d79 | ||
|
|
c5e8e19941 | ||
|
|
50d8e3365e | ||
|
|
42fd490a33 | ||
|
|
ef84a45ebb | ||
|
|
d924643436 | ||
|
|
a7bde2e25f | ||
|
|
f64d9d31ab | ||
|
|
ec4ebfa24a | ||
|
|
03ba1af356 | ||
|
|
d231edd0bb | ||
|
|
97fef050b7 | ||
|
|
61fe46da8a | ||
|
|
dc7dc9a087 | ||
|
|
e27c34df4e | ||
|
|
a3b6d37fd3 | ||
|
|
d01af855f9 | ||
|
|
5353af94ac | ||
|
|
2656d0b3a9 | ||
|
|
a99c1e00b9 | ||
|
|
73a1f2ccef | ||
|
|
a4892efe61 | ||
|
|
5ded562e12 | ||
|
|
d2508218f7 | ||
|
|
1e10df6f03 | ||
|
|
06887d9440 | ||
|
|
3e9776e8e0 | ||
|
|
6b2c4dc79f | ||
|
|
726abc4965 | ||
|
|
6450cf1b17 | ||
|
|
2aaa196acf | ||
|
|
4d35364a30 | ||
|
|
f1c5a57abd | ||
|
|
dada57141f | ||
|
|
d45d1d42c2 | ||
|
|
9ee757aa19 | ||
|
|
17de98991d | ||
|
|
2862f22243 | ||
|
|
9c2ab5c3e6 | ||
|
|
4d6fbfce6c | ||
|
|
5c0d1d27f5 |
32
README.md
32
README.md
@@ -1,33 +1,33 @@
|
||||
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
|
||||
[](LICENSE) 
|
||||
### Enigma2 channel and satellite list editor for GNU/Linux.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103152885-4a7bd880-479d-11eb-96e8-4ad0f5dc3e2e.png" width="560"/>](https://user-images.githubusercontent.com/7511379/103152885-4a7bd880-479d-11eb-96e8-4ad0f5dc3e2e.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118884719-8277e980-b8ff-11eb-8621-c8c4afd6181b.png" width="560"/>](https://user-images.githubusercontent.com/7511379/118884719-8277e980-b8ff-11eb-8621-c8c4afd6181b.png)
|
||||
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
|
||||
|
||||
## Main features of the program
|
||||
* Editing bouquets, channels, satellites.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/100156250-a3fc9900-2eb9-11eb-8729-7bcb6ddcdd4a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/100156250-a3fc9900-2eb9-11eb-8729-7bcb6ddcdd4a.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118884747-8ad02480-b8ff-11eb-9104-8cf8fb6e785d.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118884747-8ad02480-b8ff-11eb-9104-8cf8fb6e785d.png)
|
||||
* Import function.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103150878-8a38c500-4789-11eb-9e03-0c8ee832ff99.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150878-8a38c500-4789-11eb-9e03-0c8ee832ff99.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118526825-4dc23180-b749-11eb-8197-e9bbccbc3bdf.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118526825-4dc23180-b749-11eb-8197-e9bbccbc3bdf.png)
|
||||
* Backup function.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103150886-a0df1c00-4789-11eb-88fa-91996b72d1f9.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150886-a0df1c00-4789-11eb-88fa-91996b72d1f9.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118528402-f58c2f00-b74a-11eb-9b84-edf220526e6e.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118528402-f58c2f00-b74a-11eb-9b84-edf220526e6e.png)
|
||||
* Support of picons.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103150891-accade00-4789-11eb-8804-e1807df89c99.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150891-accade00-4789-11eb-8804-e1807df89c99.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118526864-5c104d80-b749-11eb-8497-6e8c78542ab1.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118526864-5c104d80-b749-11eb-8497-6e8c78542ab1.png)
|
||||
* Importing services, downloading picons and updating satellites from the Web.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/97909451-4c0ac080-1d59-11eb-9626-85bed91f7ccc.png" width="250"/>](https://user-images.githubusercontent.com/7511379/97909451-4c0ac080-1d59-11eb-9626-85bed91f7ccc.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103150872-77be8b80-4789-11eb-8a74-7a49fb3edd98.png" width="292"/>](https://user-images.githubusercontent.com/7511379/103150872-77be8b80-4789-11eb-8a74-7a49fb3edd98.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118530243-1a81a180-b74d-11eb-8e01-aea904d954af.png" width="250"/>](https://user-images.githubusercontent.com/7511379/118530243-1a81a180-b74d-11eb-8e01-aea904d954af.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118526706-31be9000-b749-11eb-9956-c4bf2e13f968.png" width="292"/>](https://user-images.githubusercontent.com/7511379/118526706-31be9000-b749-11eb-9956-c4bf2e13f968.png)
|
||||
* Extended support of IPTV.
|
||||
* Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
* Export of bouquets with IPTV services in m3u.
|
||||
* Assignment of EPG from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
* Preview (playback) of IPTV or other streams directly from the bouquet list.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103151911-89a52c00-4793-11eb-9941-8430f4e87eef.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103151911-89a52c00-4793-11eb-9941-8430f4e87eef.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118884891-b3f0b500-b8ff-11eb-8717-3588d6e089de.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118884891-b3f0b500-b8ff-11eb-8717-3588d6e089de.png)
|
||||
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103150898-c79d5280-4789-11eb-9d16-e7f89225738b.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150898-c79d5280-4789-11eb-9d16-e7f89225738b.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118886284-66754780-b901-11eb-9068-29b5a607ccaf.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118886284-66754780-b901-11eb-9068-29b5a607ccaf.png)
|
||||
* Simple FTP client (experimental).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/103152009-7e9ecb80-4794-11eb-85f1-c97e189a3195.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103152009-7e9ecb80-4794-11eb-85f1-c97e189a3195.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png)
|
||||
|
||||
#### Keyboard shortcuts
|
||||
* **Ctrl + X** - only in bouquet list.
|
||||
@@ -57,7 +57,7 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
|
||||
|
||||
## Minimum requirements
|
||||
*Python >= 3.5.2, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
|
||||
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
|
||||
|
||||
***Optional:** python3-pil, python3-chardet.*
|
||||
## Installation and Launch
|
||||
@@ -70,17 +70,15 @@ Extra folders can be deleted, excluding the *app* folder and root files like *De
|
||||
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
|
||||
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
|
||||
* ### macOS (experimental)
|
||||
**This program can also be run on macOS.**
|
||||
To work in this OS, you must use a [separate branch](https://github.com/DYefremov/DemonEditor/tree/experimental-mac).
|
||||
* ### macOS
|
||||
**This program can be run on macOS.** To work in this OS, you must use a [separate branch](https://github.com/DYefremov/DemonEditor/tree/experimental-mac). A ready-made package can be downloaded from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
**The functionality and performance of this version may be different from the Linux version!**
|
||||
|
||||
|
||||
## Important
|
||||
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
|
||||
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2.
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support!
|
||||
For version **3** is only read mode available. When saving, version **4** format is used instead.
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
|
||||
|
||||
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
|
||||
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from threading import Thread, Timer
|
||||
|
||||
@@ -77,5 +78,18 @@ def run_with_delay(timeout=5):
|
||||
return run_with
|
||||
|
||||
|
||||
class DefaultDict(defaultdict):
|
||||
""" Extended to support functions with params as default factory. """
|
||||
|
||||
def __missing__(self, key):
|
||||
if self.default_factory:
|
||||
value = self[key] = self.default_factory(key)
|
||||
return value
|
||||
return super().__missing__(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -9,7 +9,7 @@ from ftplib import FTP, CRLF, Error, error_perm
|
||||
from http.client import RemoteDisconnected
|
||||
from telnetlib import Telnet
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode, quote
|
||||
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
|
||||
install_opener, Request)
|
||||
|
||||
@@ -17,7 +17,7 @@ from app.commons import log, run_task
|
||||
from app.settings import SettingsType
|
||||
|
||||
BQ_FILES_LIST = ("tv", "radio", # enigma 2
|
||||
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
|
||||
@@ -328,7 +328,7 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
save_path = settings.data_local_path
|
||||
save_path = settings.profile_data_path
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# bouquets
|
||||
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
|
||||
@@ -342,7 +342,7 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
picons_path = settings.picons_local_path
|
||||
picons_path = settings.profile_picons_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
|
||||
# epg.dat
|
||||
@@ -362,15 +362,16 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
|
||||
callback=log, done_callback=None, use_http=False, files_filter=None):
|
||||
s_type = settings.setting_type
|
||||
data_path = settings.data_local_path
|
||||
host = settings.host
|
||||
base_url = "http{}://{}:{}".format("s" if settings.http_use_ssl else "", host, settings.http_port)
|
||||
url = "{}/web/".format(base_url)
|
||||
data_path = settings.profile_data_path
|
||||
host, port, use_ssl = settings.host, settings.http_port, settings.http_use_ssl
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/"
|
||||
tn, ht = None, None # telnet, http
|
||||
|
||||
try:
|
||||
if s_type is SettingsType.ENIGMA_2 and use_http:
|
||||
ht = http(settings.user, settings.password, base_url, callback, settings.http_use_ssl)
|
||||
if use_http:
|
||||
ht = http(settings.user, settings.password, base_url, callback, use_ssl, s_type)
|
||||
next(ht)
|
||||
message = ""
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
@@ -382,22 +383,26 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
elif download_type is DownloadType.PICONS:
|
||||
message = "Picons will be updated!"
|
||||
|
||||
params = urlencode({"text": message, "type": 2, "timeout": 5})
|
||||
ht.send((url + "message?{}".format(params), "Sending info message... "))
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": message, "type": 2, "timeout": 5})
|
||||
else:
|
||||
params = urlencode({"nmsg": message, "timeout": 5}, quote_via=quote)
|
||||
|
||||
if download_type is DownloadType.ALL:
|
||||
ht.send((f"{url}message?{params}", "Sending info message... "))
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
|
||||
time.sleep(5)
|
||||
ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
|
||||
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
|
||||
time.sleep(2)
|
||||
else:
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# telnet
|
||||
# Telnet
|
||||
tn = telnet(host=host,
|
||||
user=settings.user,
|
||||
password=settings.password,
|
||||
timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# terminate enigma or neutrino
|
||||
# Terminate Enigma2 or Neutrino.
|
||||
callback("Telnet initialization ...\n")
|
||||
tn.send("init 4")
|
||||
callback("Stopping GUI...\n")
|
||||
@@ -428,18 +433,21 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
ftp.upload_picons(settings.picons_local_path, settings.picons_path, callback, files_filter)
|
||||
ftp.upload_picons(settings.profile_picons_path, settings.picons_path, callback, files_filter)
|
||||
|
||||
if tn and not use_http:
|
||||
# resume enigma or restart neutrino
|
||||
# Resume Enigma2 or restart Neutrino.
|
||||
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
|
||||
callback("Starting...\n" if s_type is SettingsType.ENIGMA_2 else "Rebooting...\n")
|
||||
elif ht and use_http:
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
|
||||
elif download_type is DownloadType.ALL:
|
||||
ht.send((url + "servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
|
||||
ht.send((url + "powerstate?newstate=4", "Wakeup from Standby."))
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ht.send((f"{url}servicelistreload?mode=2", "Reloading Userbouquets."))
|
||||
elif download_type is DownloadType.ALL:
|
||||
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
|
||||
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
|
||||
else:
|
||||
ht.send((f"{url}reloadchannels", "Reloading channels..."))
|
||||
|
||||
if done_callback is not None:
|
||||
done_callback()
|
||||
@@ -465,14 +473,17 @@ def picons_filter_function(files_filter=None):
|
||||
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
|
||||
|
||||
|
||||
def http(user, password, url, callback, use_ssl=False):
|
||||
init_auth(user, password, url, use_ssl)
|
||||
data = get_post_data(url, password, url)
|
||||
def http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGMA_2):
|
||||
HttpAPI.init_auth(user, password, url, use_ssl)
|
||||
data = HttpAPI.get_post_data(url, password, url) if s_type is SettingsType.ENIGMA_2 else None
|
||||
|
||||
while True:
|
||||
url, message = yield
|
||||
resp = get_response(HttpAPI.Request.TEST, url, data).get("e2statetext", None)
|
||||
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
resp = resp.get("e2statetext", None)
|
||||
|
||||
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}\n")
|
||||
|
||||
|
||||
def telnet(host, port=23, user="", password="", timeout=5):
|
||||
@@ -505,11 +516,12 @@ def telnet(host, port=23, user="", password="", timeout=5):
|
||||
class HttpAPI:
|
||||
__MAX_WORKERS = 4
|
||||
|
||||
class Request(Enum):
|
||||
class Request(str, Enum):
|
||||
ZAP = "zap?sRef="
|
||||
INFO = "about"
|
||||
SIGNAL = "signal"
|
||||
STREAM = "stream.m3u?ref="
|
||||
STREAM_TS = "ts.m3u?file="
|
||||
STREAM_CURRENT = "streamcurrent.m3u"
|
||||
CURRENT = "getcurrent"
|
||||
TEST = None
|
||||
@@ -531,8 +543,16 @@ class HttpAPI:
|
||||
# Timer
|
||||
TIMER = ""
|
||||
TIMER_LIST = "timerlist"
|
||||
# Recordings
|
||||
RECORDINGS = "movielist?dirname="
|
||||
REC_DIRS = "getlocations"
|
||||
REC_CURRENT = "getcurrlocation"
|
||||
# Screenshot
|
||||
GRUB = "grab?format=jpg&"
|
||||
# Neutrino requests.
|
||||
N_INFO = "info"
|
||||
N_ZAP = "zapto"
|
||||
N_STREAM = "build_playlist?id="
|
||||
|
||||
class Remote(str, Enum):
|
||||
""" Args for HttpRequestType [REMOTE] class. """
|
||||
@@ -543,10 +563,12 @@ class HttpAPI:
|
||||
MENU = "139"
|
||||
EXIT = "174"
|
||||
OK = "352"
|
||||
INFO = "358"
|
||||
RED = "398"
|
||||
GREEN = "399"
|
||||
YELLOW = "400"
|
||||
BLUE = "401"
|
||||
BACK = "412"
|
||||
|
||||
class Power(str, Enum):
|
||||
""" Args for HttpRequestType [POWER] class. """
|
||||
@@ -557,6 +579,19 @@ class HttpAPI:
|
||||
WAKEUP = "4"
|
||||
STANDBY = "5"
|
||||
|
||||
PARAM_REQUESTS = {Request.REMOTE,
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
Request.EPG,
|
||||
Request.TIMER,
|
||||
Request.RECORDINGS,
|
||||
Request.N_ZAP}
|
||||
|
||||
STREAM_REQUESTS = {Request.STREAM,
|
||||
Request.STREAM_CURRENT,
|
||||
Request.STREAM_TS,
|
||||
Request.N_STREAM}
|
||||
|
||||
def __init__(self, settings):
|
||||
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
|
||||
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
|
||||
@@ -568,46 +603,45 @@ class HttpAPI:
|
||||
self._base_url = None
|
||||
self._data = None
|
||||
self._is_owif = True
|
||||
self._s_type = SettingsType.ENIGMA_2
|
||||
self.init()
|
||||
|
||||
def send(self, req_type, ref, callback=print, ref_prefix=""):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
url = self._base_url + req_type.value
|
||||
url = self._base_url + req_type
|
||||
data = self._data
|
||||
|
||||
if req_type is self.Request.ZAP or req_type is self.Request.STREAM:
|
||||
url += urllib.parse.quote(ref)
|
||||
if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
|
||||
url += quote(ref)
|
||||
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
|
||||
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
|
||||
url = f"{url}{ref_prefix}{quote(ref).replace('%3A', '%253A')}"
|
||||
elif req_type is self.Request.GRUB:
|
||||
data = None # Must be disabled for token-based security.
|
||||
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
|
||||
elif req_type in (self.Request.REMOTE,
|
||||
self.Request.POWER,
|
||||
self.Request.VOL,
|
||||
self.Request.EPG,
|
||||
self.Request.TIMER):
|
||||
url = f"{self._main_url}/{req_type}{ref}"
|
||||
elif req_type in self.PARAM_REQUESTS:
|
||||
url += ref
|
||||
|
||||
def done_callback(f):
|
||||
callback(f.result())
|
||||
|
||||
future = self._executor.submit(get_response, req_type, url, data)
|
||||
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type)
|
||||
future.add_done_callback(done_callback)
|
||||
|
||||
@run_task
|
||||
def init(self):
|
||||
user, password = self._settings.user, self._settings.password
|
||||
use_ssl = self._settings.http_use_ssl
|
||||
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
|
||||
self._base_url = "{}/web/".format(self._main_url)
|
||||
init_auth(user, password, self._main_url, use_ssl)
|
||||
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
|
||||
s_id = get_session_id(user, password, url)
|
||||
if s_id != "0":
|
||||
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
self._s_type = self._settings.setting_type
|
||||
user, password, use_ssl = self._settings.user, self._settings.password, self._settings.http_use_ssl
|
||||
self._main_url = f"http{'s' if use_ssl else ''}://{self._settings.host}:{self._settings.http_port}"
|
||||
self._base_url = f"{self._main_url}/{'web' if self._s_type is SettingsType.ENIGMA_2 else 'control'}/"
|
||||
self.init_auth(user, password, self._main_url, use_ssl)
|
||||
|
||||
self._data = None
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
s_id = self.get_session_id(user, password, f"{self._main_url}/web/{self.Request.TOKEN}")
|
||||
if s_id != "0":
|
||||
self._data = urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
|
||||
self.send(self.Request.INFO, None, self.init_callback)
|
||||
|
||||
@@ -628,69 +662,91 @@ class HttpAPI:
|
||||
self._shutdown = True
|
||||
self._executor.shutdown()
|
||||
|
||||
@staticmethod
|
||||
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2):
|
||||
try:
|
||||
with urlopen(Request(url, data=data), timeout=10) as f:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return HttpAPI.get_e2_response_data(req_type, f)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return HttpAPI.get_neutrino_response_data(req_type, f)
|
||||
else:
|
||||
return f.read().decode("utf-8")
|
||||
except HTTPError as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
return {"error_code": e.code}
|
||||
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
except ETree.ParseError as e:
|
||||
log("Parsing response error: {}".format(e))
|
||||
|
||||
def get_response(req_type, url, data=None):
|
||||
try:
|
||||
with urlopen(Request(url, data=data), timeout=10) as f:
|
||||
if req_type is HttpAPI.Request.STREAM or req_type is HttpAPI.Request.STREAM_CURRENT:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
elif req_type is HttpAPI.Request.GRUB:
|
||||
return {"img_data": f.read()}
|
||||
elif req_type is HttpAPI.Request.CURRENT:
|
||||
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
|
||||
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
|
||||
elif req_type is HttpAPI.Request.PLAYER_LIST:
|
||||
return [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
|
||||
elif req_type is HttpAPI.Request.EPG:
|
||||
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
|
||||
elif req_type is HttpAPI.Request.TIMER_LIST:
|
||||
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
|
||||
else:
|
||||
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
|
||||
except HTTPError as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
return {"error_code": e.code}
|
||||
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
except ETree.ParseError as e:
|
||||
log("Parsing response error: {}".format(e))
|
||||
return {"error_code": -1}
|
||||
|
||||
return {"error_code": -1}
|
||||
@staticmethod
|
||||
def get_e2_response_data(req_type, f):
|
||||
if req_type in HttpAPI.STREAM_REQUESTS:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
elif req_type is HttpAPI.Request.GRUB:
|
||||
return {"img_data": f.read()}
|
||||
elif req_type is HttpAPI.Request.CURRENT:
|
||||
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
|
||||
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
|
||||
elif req_type is HttpAPI.Request.PLAYER_LIST:
|
||||
return [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
|
||||
elif req_type is HttpAPI.Request.EPG:
|
||||
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
|
||||
elif req_type is HttpAPI.Request.TIMER_LIST:
|
||||
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
|
||||
elif req_type is HttpAPI.Request.REC_DIRS:
|
||||
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
|
||||
elif req_type is HttpAPI.Request.RECORDINGS:
|
||||
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
|
||||
else:
|
||||
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
|
||||
|
||||
@staticmethod
|
||||
def get_neutrino_response_data(req_type, f):
|
||||
if req_type is HttpAPI.Request.N_INFO:
|
||||
return {"info": f.read().decode("utf-8").strip()}
|
||||
elif req_type is HttpAPI.Request.N_STREAM:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
return {"data": f.read().decode("utf-8")}
|
||||
|
||||
def init_auth(user, password, url, use_ssl=False):
|
||||
""" Init authentication """
|
||||
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
|
||||
pass_mgr.add_password(None, url, user, password)
|
||||
auth_handler = HTTPBasicAuthHandler(pass_mgr)
|
||||
@staticmethod
|
||||
def init_auth(user, password, url, use_ssl=False):
|
||||
""" Init authentication """
|
||||
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
|
||||
pass_mgr.add_password(None, url, user, password)
|
||||
auth_handler = HTTPBasicAuthHandler(pass_mgr)
|
||||
|
||||
if use_ssl:
|
||||
import ssl
|
||||
from urllib.request import HTTPSHandler
|
||||
if use_ssl:
|
||||
import ssl
|
||||
from urllib.request import HTTPSHandler
|
||||
|
||||
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
|
||||
else:
|
||||
opener = build_opener(auth_handler)
|
||||
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
|
||||
else:
|
||||
opener = build_opener(auth_handler)
|
||||
|
||||
install_opener(opener)
|
||||
install_opener(opener)
|
||||
|
||||
@staticmethod
|
||||
def get_session_id(user, password, url):
|
||||
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
|
||||
return HttpAPI.get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
|
||||
|
||||
def get_session_id(user, password, url):
|
||||
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
|
||||
return get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
|
||||
|
||||
|
||||
def get_post_data(base_url, password, user):
|
||||
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN.value))
|
||||
data = None
|
||||
if s_id != "0":
|
||||
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
return data
|
||||
@staticmethod
|
||||
def get_post_data(base_url, password, user):
|
||||
s_id = HttpAPI.get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN))
|
||||
data = None
|
||||
if s_id != "0":
|
||||
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
return data
|
||||
|
||||
|
||||
# ***************** Connections testing *******************#
|
||||
@@ -703,16 +759,29 @@ def test_ftp(host, port, user, password, timeout=5):
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False):
|
||||
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
|
||||
params = "statusinfo" if skip_message else "message?{}".format(params)
|
||||
base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port)
|
||||
# authentication
|
||||
init_auth(user, password, base_url, use_ssl)
|
||||
data = get_post_data(base_url, password, user)
|
||||
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False, s_type=SettingsType.ENIGMA_2):
|
||||
t_msg = "Connection test!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": t_msg, "type": 2, "timeout": timeout})
|
||||
params = "statusinfo" if skip_message else f"message?{params}"
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
params = urlencode({"nmsg": t_msg, "timeout": 5}, quote_via=quote)
|
||||
params = "info" if skip_message else f"message?{params}"
|
||||
else:
|
||||
raise TestException("This type of settings is not supported!")
|
||||
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/{params}"
|
||||
# Authentication
|
||||
HttpAPI.init_auth(user, password, base_url, use_ssl)
|
||||
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
|
||||
|
||||
try:
|
||||
return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return resp.get("e2statetext", "")
|
||||
return resp
|
||||
except (RemoteDisconnected, URLError, HTTPError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.commons import run_task
|
||||
from app.settings import SettingsType
|
||||
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
|
||||
@@ -32,6 +59,15 @@ def get_bouquets(path, s_type):
|
||||
return get_neutrino_bouquets(path)
|
||||
|
||||
|
||||
def write_bouquet(path, bq, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
writer = BouquetsWriter(path, None)
|
||||
writer.write_bouquet(path + "userbouquet.{}.{}".format(bq.name, bq.type), bq.name, bq.services)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
from .neutrino.bouquets import write_bouquet
|
||||
write_bouquet(path, bq)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
|
||||
@@ -15,6 +15,7 @@ class BqServiceType(Enum):
|
||||
MARKER = "MARKER" # 64
|
||||
SPACE = "SPACE" # 832 [hidden marker]
|
||||
ALT = "ALT" # Service with alternatives
|
||||
BOUQUET = "BOUQUET" # Sub bouquet.
|
||||
|
||||
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden", "file"])
|
||||
@@ -35,14 +36,20 @@ class TrType(Enum):
|
||||
Satellite = "s"
|
||||
Terrestrial = "t"
|
||||
Cable = "c"
|
||||
ATSC = "a"
|
||||
|
||||
|
||||
class BqType(Enum):
|
||||
""" Bouquet type"""
|
||||
""" Bouquet type. """
|
||||
BOUQUET = "bouquet"
|
||||
TV = "tv"
|
||||
RADIO = "radio"
|
||||
WEBTV = "webtv"
|
||||
MARKER = "marker"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.TV
|
||||
|
||||
|
||||
class Flag(Enum):
|
||||
@@ -94,12 +101,20 @@ class Inversion(Enum):
|
||||
On = "1"
|
||||
Auto = "2"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Auto
|
||||
|
||||
|
||||
class Pilot(Enum):
|
||||
Off = "0"
|
||||
On = "1"
|
||||
Auto = "2"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Auto
|
||||
|
||||
|
||||
class SystemCable(Enum):
|
||||
""" System of cable service """
|
||||
@@ -147,6 +162,10 @@ T_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
|
||||
# Cable
|
||||
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
|
||||
|
||||
# ATSC
|
||||
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
|
||||
"7": "16VSB"}
|
||||
|
||||
# CAS
|
||||
CAS = {"C:26": "BISS", "C:0B": "Conax", "C:06": "Irdeto", "C:18": "Nagravision", "C:05": "Viaccess", "C:01": "SECA",
|
||||
"C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard"}
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" This module used for parsing blacklist file
|
||||
|
||||
Parent Lock/Unlock
|
||||
@@ -9,14 +37,14 @@ __FILE_NAME = "blacklist"
|
||||
|
||||
def get_blacklist(path):
|
||||
with suppress(FileNotFoundError):
|
||||
with open(path + __FILE_NAME, "r") as file:
|
||||
with open(path + __FILE_NAME, "r", encoding="utf-8") as file:
|
||||
# filter empty values and "\n"
|
||||
return {*list(filter(None, (x.strip() for x in file.readlines())))}
|
||||
return {}
|
||||
|
||||
|
||||
def write_blacklist(path, channels):
|
||||
with open(path + __FILE_NAME, "w") as file:
|
||||
with open(path + __FILE_NAME, "w", encoding="utf-8") as file:
|
||||
if channels:
|
||||
file.writelines("\n".join(channels))
|
||||
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with Enigma2 bouquets. """
|
||||
import re
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import log
|
||||
@@ -9,10 +38,11 @@ from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouque
|
||||
_TV_FILE = "bouquets.tv"
|
||||
_RADIO_FILE = "bouquets.radio"
|
||||
_DEFAULT_BOUQUET_NAME = "favourites"
|
||||
_MARKER_PREFIX = "[MARKER!] "
|
||||
|
||||
|
||||
class BouquetsWriter:
|
||||
""" Class for creating and writing bouquet files..
|
||||
""" Class for creating and writing bouquet files.
|
||||
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
@@ -40,6 +70,7 @@ class BouquetsWriter:
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
bq_file_names = {b.file for b in bqs.bouquets}
|
||||
count = 1
|
||||
m_count = 0
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.file
|
||||
@@ -53,19 +84,25 @@ class BouquetsWriter:
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
|
||||
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
|
||||
if BqType(bq.type) is BqType.MARKER:
|
||||
m_data = bq.file.split(":") if bq.file else None
|
||||
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
|
||||
line.append(self._MARKER.format(m_count, b_name))
|
||||
m_count += 1
|
||||
else:
|
||||
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bq.type}", bq.name, bq.services)
|
||||
|
||||
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
|
||||
file.writelines(line)
|
||||
|
||||
def write_bouquet(self, path, name, services):
|
||||
""" Writes single bouquet file. """
|
||||
bouquet = ["#NAME {}\n".format(name)]
|
||||
bouquet = [f"#NAME {name}\n"]
|
||||
for srv in services:
|
||||
s_type = srv.service_type
|
||||
if s_type == BqServiceType.IPTV.name:
|
||||
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
|
||||
bouquet.append(f"#SERVICE {srv.fav_id.strip()}\n")
|
||||
elif s_type == BqServiceType.MARKER.name:
|
||||
m_data = srv.fav_id.strip().split(":")
|
||||
m_data[2] = self._marker_index
|
||||
@@ -79,30 +116,43 @@ class BouquetsWriter:
|
||||
if services:
|
||||
p = Path(path)
|
||||
alt_name = srv.data_id
|
||||
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
|
||||
f_name = f"alternatives.{alt_name}{p.suffix}"
|
||||
|
||||
if self._force_bq_names:
|
||||
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
|
||||
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
|
||||
f_name = f"alternatives.{alt_name}{p.suffix}"
|
||||
|
||||
alt_path = "{}/{}".format(p.parent, f_name)
|
||||
bouquet.append(self._ALT.format(f_name))
|
||||
self.write_bouquet(alt_path, srv.service, services)
|
||||
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
|
||||
else:
|
||||
data = to_bouquet_id(srv)
|
||||
if srv.service:
|
||||
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
|
||||
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
|
||||
else:
|
||||
bouquet.append("#SERVICE {}\n".format(data))
|
||||
bouquet.append(f"#SERVICE {data}\n")
|
||||
|
||||
with open(path, "w", encoding="utf-8") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
|
||||
class ServiceType(Enum):
|
||||
SERVICE = "0"
|
||||
BOUQUET = "7" # Sub bouquet.
|
||||
MARKER = "64"
|
||||
SPACE = "832" # Hidden marker.
|
||||
ALT = "134" # Alternatives.
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
|
||||
return cls.SERVICE
|
||||
|
||||
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(".*subbouquet\\.+(.*)\\.([tv|radio]+).*")
|
||||
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
|
||||
|
||||
__slots__ = ["_path"]
|
||||
@@ -116,37 +166,44 @@ class BouquetsReader:
|
||||
|
||||
def parse_bouquets(self, bq_name, bq_type):
|
||||
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
|
||||
lines = file.readlines()
|
||||
bouquets = None
|
||||
nm_sep = "#NAME"
|
||||
line = file.readline()
|
||||
_, _, bqs_name = line.partition("#NAME")
|
||||
if not bqs_name:
|
||||
log(f"No bouquets name found in '{bq_name}'")
|
||||
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
|
||||
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
|
||||
|
||||
b_names = set()
|
||||
real_b_names = Counter()
|
||||
|
||||
for line in lines:
|
||||
if nm_sep in line:
|
||||
_, _, name = line.partition(nm_sep)
|
||||
bouquets = Bouquets(name.strip(), bq_type, [])
|
||||
if bouquets and "#SERVICE" in line:
|
||||
for line in file.readlines():
|
||||
if "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
if name:
|
||||
b_name = name.group(1)
|
||||
if b_name in b_names:
|
||||
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
|
||||
log(f"The list of bouquets contains duplicate [{b_name}] names!")
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
|
||||
if rb_name in real_b_names:
|
||||
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
|
||||
rb_name))
|
||||
log(f"Bouquet file 'userbouquet.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
|
||||
rb_name = f"{rb_name} {real_b_names[rb_name]}"
|
||||
else:
|
||||
real_b_names[rb_name] = 0
|
||||
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
|
||||
else:
|
||||
raise ValueError("No bouquet name found for: {}".format(line))
|
||||
s_data = line.split(":")
|
||||
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
|
||||
b_name = "{}{}".format(_MARKER_PREFIX, s_data[-1].strip())
|
||||
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
|
||||
else:
|
||||
log(f"Unsupported or invalid data format: [{line}].")
|
||||
else:
|
||||
log(f"Unsupported or invalid line format: [{line}].")
|
||||
|
||||
return bouquets
|
||||
|
||||
@@ -166,19 +223,31 @@ class BouquetsReader:
|
||||
|
||||
for num, srv in enumerate(srvs, start=1):
|
||||
srv_data = srv.strip().split(":")
|
||||
s_type = srv_data[1]
|
||||
if s_type == "64":
|
||||
data_len = len(srv_data)
|
||||
if data_len < 10:
|
||||
log("The bouquet [{}] service [{}] has the wrong data format: [{}]".format(bq_name, num, srv))
|
||||
continue
|
||||
|
||||
s_type = ServiceType(srv_data[1])
|
||||
if s_type is ServiceType.MARKER:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
|
||||
elif s_type == "832":
|
||||
elif s_type is ServiceType.SPACE:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
|
||||
elif s_type == "134":
|
||||
elif s_type is ServiceType.ALT:
|
||||
alt = re.match(BouquetsReader._ALT_PAT, srv)
|
||||
if alt:
|
||||
alt_name, alt_type = alt.group(1), alt.group(2)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
|
||||
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
|
||||
elif s_type is ServiceType.BOUQUET:
|
||||
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
|
||||
if sub:
|
||||
sub_name, sub_type = sub.group(1), sub.group(2)
|
||||
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
|
||||
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
|
||||
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
|
||||
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
|
||||
stream_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
|
||||
@@ -186,7 +255,7 @@ class BouquetsReader:
|
||||
else:
|
||||
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
|
||||
name = None
|
||||
if len(srv_data) == 12:
|
||||
if data_len == 12:
|
||||
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
|
||||
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" This module used for parsing and write lamedb file """
|
||||
import re
|
||||
|
||||
@@ -47,7 +75,7 @@ class LameDbReader:
|
||||
tr_type = tr[0:1]
|
||||
if tr_type == "c":
|
||||
tr += ":0:0:0"
|
||||
elif tr_type == "t":
|
||||
elif tr_type == "t" or tr_type == "a":
|
||||
tr += ":0:0"
|
||||
else:
|
||||
tr_data = tr.split(_SEP)
|
||||
@@ -81,7 +109,7 @@ class LameDbReader:
|
||||
lns = file.readlines()
|
||||
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
|
||||
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
for line in lns:
|
||||
@@ -95,8 +123,13 @@ class LameDbReader:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
elif line.startswith("t:"):
|
||||
tr, srv = line.split(",")
|
||||
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
|
||||
data = line.split(",")
|
||||
len_data = len(data)
|
||||
if len_data > 1:
|
||||
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
|
||||
trs[tr] = srv
|
||||
else:
|
||||
log("Error while parsing transponder data [ver. 5] for line: {}".format(line))
|
||||
|
||||
return self.parse_services(srvs, trs)
|
||||
|
||||
@@ -177,6 +210,10 @@ class LameDbReader:
|
||||
system = "DVB-C"
|
||||
pos = "C"
|
||||
fec = FEC_DEFAULT.get(tr[4])
|
||||
elif tr_type is TrType.ATSC:
|
||||
system = "ATSC"
|
||||
pos = "T"
|
||||
fec = FEC_DEFAULT.get("0")
|
||||
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
@@ -274,7 +311,7 @@ class LameDbWriter:
|
||||
def write(self):
|
||||
if self._fmt == 4:
|
||||
# Writing lamedb file ver.4
|
||||
with open(self._path + _FILE_NAME, "w") as file:
|
||||
with open(self._path + _FILE_NAME, "w", encoding="utf-8") as file:
|
||||
file.writelines(LameDbReader.get_services_lines(self._services))
|
||||
elif self._fmt == 5:
|
||||
self.write_to_lamedb5()
|
||||
@@ -299,7 +336,7 @@ class LameDbWriter:
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
|
||||
with open(self._path + "lamedb5", "w") as file:
|
||||
with open(self._path + "lamedb5", "w", encoding="utf-8") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
SP = "_:::_"
|
||||
KSP = "_::_"
|
||||
API_VER = "4"
|
||||
|
||||
|
||||
def get_attributes(data):
|
||||
return {el[0]: el[1] for el in (e.split(KSP) for e in data.split(SP))}
|
||||
|
||||
|
||||
def get_xml_attributes(attr):
|
||||
attrs = attr.attributes
|
||||
return {t: attrs[t].value for t in attrs.keys()}
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
from xml.dom.minidom import parse, Document
|
||||
|
||||
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
|
||||
|
||||
@@ -23,29 +52,29 @@ def parse_bouquets(file, name, bq_type):
|
||||
if not os.path.exists(file):
|
||||
return bouquets
|
||||
|
||||
dom = parse(file)
|
||||
dom = XmlHandler.parse(file)
|
||||
|
||||
for elem in dom.getElementsByTagName("Bouquet"):
|
||||
if elem.hasAttributes():
|
||||
bq_name = elem.attributes["name"].value
|
||||
hidden = elem.attributes.get("hidden")
|
||||
hidden = hidden.value if hidden else hidden
|
||||
locked = elem.attributes.get("locked")
|
||||
locked = locked.value if locked else locked
|
||||
# epg = elem.attributes["epg"].value
|
||||
bq_attrs = get_xml_attributes(elem)
|
||||
bq_name = bq_attrs.get("name", "")
|
||||
hidden = bq_attrs.get("hidden", "0")
|
||||
locked = bq_attrs.get("locked", "0")
|
||||
services = []
|
||||
for srv_elem in elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
ssid = srv_elem.attributes["i"].value
|
||||
on = srv_elem.attributes["on"].value
|
||||
tr_id = srv_elem.attributes["t"].value
|
||||
s_attrs = get_xml_attributes(srv_elem)
|
||||
ssid = s_attrs.get("i", "0")
|
||||
on = s_attrs.get("on", "0")
|
||||
tr_id = s_attrs.get("t", "0")
|
||||
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
|
||||
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
|
||||
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))
|
||||
hidden=HIDE_ICON if hidden == "1" else None,
|
||||
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
|
||||
|
||||
if BqType(bq_type) is BqType.BOUQUET:
|
||||
for bq in bouquets.bouquets:
|
||||
@@ -63,35 +92,27 @@ def parse_webtv(path, name, bq_type):
|
||||
if not os.path.exists(path):
|
||||
return bouquets
|
||||
|
||||
dom = parse(path)
|
||||
dom = XmlHandler.parse(path)
|
||||
services = []
|
||||
for elem in dom.getElementsByTagName("webtv"):
|
||||
if elem.hasAttributes():
|
||||
title = elem.attributes["title"].value
|
||||
url = elem.attributes["url"].value
|
||||
description = elem.attributes.get("description")
|
||||
description = description.value if description else description
|
||||
urlkey = elem.attributes.get("urlkey", None)
|
||||
urlkey = urlkey.value if urlkey else urlkey
|
||||
account = elem.attributes.get("account", None)
|
||||
account = account.value if account else account
|
||||
usrname = elem.attributes.get("usrname", None)
|
||||
usrname = usrname.value if usrname else usrname
|
||||
psw = elem.attributes.get("psw", None)
|
||||
psw = psw.value if psw else psw
|
||||
s_type = elem.attributes.get("type", None)
|
||||
s_type = s_type.value if s_type else s_type
|
||||
iconsrc = elem.attributes.get("iconsrc", None)
|
||||
iconsrc = iconsrc.value if iconsrc else iconsrc
|
||||
iconsrc_b = elem.attributes.get("iconsrc_b", None)
|
||||
iconsrc_b = iconsrc_b.value if iconsrc_b else iconsrc_b
|
||||
group = elem.attributes.get("group", None)
|
||||
group = group.value if group else group
|
||||
web_attrs = get_xml_attributes(elem)
|
||||
title = web_attrs.get("title", "")
|
||||
url = web_attrs.get("url", "")
|
||||
description = web_attrs.get("description", "")
|
||||
urlkey = web_attrs.get("urlkey", None)
|
||||
account = web_attrs.get("account", None)
|
||||
usrname = web_attrs.get("usrname", None)
|
||||
psw = web_attrs.get("psw", None)
|
||||
s_type = web_attrs.get("type", None)
|
||||
iconsrc = web_attrs.get("iconsrc", None)
|
||||
iconsrc_b = web_attrs.get("iconsrc_b", None)
|
||||
group = web_attrs.get("group", None)
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
|
||||
iconsrc_b, group)
|
||||
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
|
||||
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
|
||||
bouquets[2].append(bouquet)
|
||||
|
||||
return bouquets
|
||||
@@ -107,38 +128,47 @@ def write_bouquets(path, bouquets):
|
||||
|
||||
|
||||
def write_bouquet(file, bouquet):
|
||||
doc = Document()
|
||||
doc = NeutrinoDocument()
|
||||
root = doc.createElement("zapit")
|
||||
root.setAttribute("api", API_VER)
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(_COMMENT)
|
||||
doc.appendChild(comment)
|
||||
|
||||
for bq in bouquet.bouquets:
|
||||
attrs = get_attributes(bq.file) if bq.file else {}
|
||||
attrs["name"] = bq.name
|
||||
if bq.hidden:
|
||||
attrs["hidden"] = "1"
|
||||
else:
|
||||
attrs.pop("hidden", None)
|
||||
if bq.locked:
|
||||
attrs["locked"] = "1"
|
||||
else:
|
||||
attrs.pop("locked", None)
|
||||
|
||||
bq_elem = doc.createElement("Bouquet")
|
||||
bq_elem.setAttribute("name", bq.name)
|
||||
bq_elem.setAttribute("hidden", "1" if bq.hidden else "0")
|
||||
bq_elem.setAttribute("locked", "1" if bq.locked else "0")
|
||||
bq_elem.setAttribute("epg", "0")
|
||||
for k, v in attrs.items():
|
||||
bq_elem.setAttribute(k, v)
|
||||
|
||||
root.appendChild(bq_elem)
|
||||
|
||||
for srv in bq.services:
|
||||
f_data = srv.flags_cas.split(":")
|
||||
tr_id, on, ssid = srv.fav_id.split(":")
|
||||
srv_elem = doc.createElement("S")
|
||||
srv_elem.setAttribute("i", ssid)
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
srv_elem.setAttribute("t", tr_id)
|
||||
srv_elem.setAttribute("on", on)
|
||||
srv_elem.setAttribute("s", f_data[1])
|
||||
srv_elem.setAttribute("frq", srv.freq)
|
||||
srv_elem.setAttribute("l", "0") # temporary !!!
|
||||
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
|
||||
bq_elem.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
doc.write_xml(file)
|
||||
|
||||
|
||||
def write_webtv(file, bouquet):
|
||||
doc = Document()
|
||||
doc = NeutrinoDocument()
|
||||
root = doc.createElement("webtvs")
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(_COMMENT)
|
||||
@@ -172,7 +202,7 @@ def write_webtv(file, bouquet):
|
||||
|
||||
root.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
doc.write_xml(file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
114
app/eparser/neutrino/nxml.py
Normal file
114
app/eparser/neutrino/nxml.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Additional module for working with Neutrino xml files. """
|
||||
import re
|
||||
from xml.dom.minidom import parseString, Document, Element, Node
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
from app.commons import log
|
||||
|
||||
|
||||
class XmlHandler:
|
||||
""" Utility class for handling Neutrino xml files. """
|
||||
__slots__ = ()
|
||||
|
||||
ERROR_MESSAGE = "The file [{}] is not formatted correctly or contains invalid characters! Cause: {}"
|
||||
|
||||
@staticmethod
|
||||
def parse(path):
|
||||
""" Parses a file into the DOM by filename. """
|
||||
try:
|
||||
return parseString(open(path, "r", encoding="utf-8", errors="ignore").read())
|
||||
except ExpatError as e:
|
||||
# Some neutrino configuration files may contain text data with invalid character ['&'].
|
||||
# https://www.w3.org/TR/xml/#syntax
|
||||
# Apparently there is an error in Neutrino itself and the document is not initially formed correctly.
|
||||
log(XmlHandler.ERROR_MESSAGE.format(path, e))
|
||||
|
||||
return XmlHandler.preprocess(path)
|
||||
|
||||
@staticmethod
|
||||
def preprocess(path):
|
||||
""" Pre-processing xml [for '&' symbol] for correct parsing. """
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
pat = re.compile("&([^;\\W]*([^;\\w]|$))")
|
||||
log("Processing the file '{}'...".format(path))
|
||||
try:
|
||||
dom = parseString(re.sub(pat, "&", f.read()))
|
||||
except ExpatError as e:
|
||||
msg = XmlHandler.ERROR_MESSAGE.format(path, e)
|
||||
log(msg)
|
||||
raise ValueError(e)
|
||||
else:
|
||||
log("Done!")
|
||||
return dom
|
||||
|
||||
|
||||
class NeutrinoDocument(Document):
|
||||
|
||||
def createElement(self, tag_name):
|
||||
e = NElement(tag_name)
|
||||
e.ownerDocument = self
|
||||
return e
|
||||
|
||||
def write_xml(self, path):
|
||||
self.writexml(open(path, "w", encoding="utf-8"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
|
||||
|
||||
class NElement(Element):
|
||||
|
||||
def writexml(self, writer, indent="", add_indent="", new_line=""):
|
||||
""" Overridden specifically for neutrino for more correct [' -> optional] xml attrs generation. """
|
||||
writer.write(indent + "<" + self.tagName)
|
||||
attrs = self._get_attributes()
|
||||
|
||||
for a_name in attrs.keys():
|
||||
writer.write(" %s=\"" % a_name)
|
||||
self.write_data(writer, attrs[a_name].value)
|
||||
writer.write("\"")
|
||||
if self.childNodes:
|
||||
writer.write(">")
|
||||
if len(self.childNodes) == 1 and self.childNodes[0].nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
|
||||
self.childNodes[0].writexml(writer, '', '', '')
|
||||
else:
|
||||
writer.write(new_line)
|
||||
for node in self.childNodes:
|
||||
node.writexml(writer, indent + add_indent, add_indent, new_line)
|
||||
writer.write(indent)
|
||||
writer.write("</%s>%s" % (self.tagName, new_line))
|
||||
else:
|
||||
writer.write("/>%s" % new_line)
|
||||
|
||||
@staticmethod
|
||||
def write_data(writer, data):
|
||||
""" Writes data chars to writer."""
|
||||
if data:
|
||||
data = data.replace("&", "&").replace("<", "<").replace("\"", """).replace(">", ">")
|
||||
data = data.replace("'", "'")
|
||||
writer.write(data)
|
||||
@@ -1,160 +1,227 @@
|
||||
from xml.dom.minidom import parse, Document
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from app.commons import log
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
|
||||
from app.eparser.ecommons import (Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER, T_SYSTEM, TrType,
|
||||
SystemCable)
|
||||
from app.eparser.neutrino import get_xml_attributes, SP, KSP, get_attributes, API_VER
|
||||
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
|
||||
|
||||
_FILE = "services.xml"
|
||||
_TR_ATTR_NAMES = ("id", "on", "frq", "inv", "sr", "fec", "pol", "mod", "sys") # transponder attributes
|
||||
_SRV_ATTR_NAMES = ("t", "s", "num", "f", "v", "a", "p", "pmt", "tx", "vt") # service attributes
|
||||
|
||||
|
||||
def write_services(path, services):
|
||||
doc = Document()
|
||||
root = doc.createElement("zapit")
|
||||
root.setAttribute("api", "4")
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(" File was created in DemonEditor. Enjoy watching! ")
|
||||
doc.appendChild(comment)
|
||||
|
||||
sats = {}
|
||||
for srv in services:
|
||||
flag = srv[0]
|
||||
if flag in sats:
|
||||
sats.get(flag).append(srv)
|
||||
else:
|
||||
srv_list = [srv]
|
||||
sats[flag] = srv_list
|
||||
|
||||
for sat in sats:
|
||||
tr_atr = sat.split(":")
|
||||
sat_elem = doc.createElement("sat")
|
||||
sat_elem.setAttribute("name", tr_atr[0])
|
||||
sat_elem.setAttribute("position", tr_atr[1])
|
||||
sat_elem.setAttribute("diseqc", tr_atr[2])
|
||||
sat_elem.setAttribute("uncommited", tr_atr[3])
|
||||
root.appendChild(sat_elem)
|
||||
|
||||
transponers = {}
|
||||
for srv in sats.get(sat):
|
||||
flag = srv[-1]
|
||||
if flag in transponers:
|
||||
transponers.get(flag).append(srv)
|
||||
else:
|
||||
srv_list = [srv]
|
||||
transponers[flag] = srv_list
|
||||
|
||||
for tr in transponers:
|
||||
tr_elem = doc.createElement("TS")
|
||||
tr_atr = tr.split(":")
|
||||
for i, value in enumerate(tr_atr):
|
||||
if value == "None":
|
||||
continue
|
||||
tr_elem.setAttribute(_TR_ATTR_NAMES[i], value)
|
||||
sat_elem.appendChild(tr_elem)
|
||||
|
||||
for srv in transponers.get(tr):
|
||||
srv_elem = doc.createElement("S")
|
||||
srv_elem.setAttribute("i", srv.ssid)
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
|
||||
srv_attrs = srv.data_id.split(":")
|
||||
api = srv_attrs.pop(0)
|
||||
|
||||
if api == "3":
|
||||
root.setAttribute("api", "3") # !!!
|
||||
for i, value in enumerate(srv_attrs):
|
||||
if value == "None":
|
||||
continue
|
||||
srv_elem.setAttribute(_SRV_ATTR_NAMES[i], value)
|
||||
|
||||
tr_elem.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(path + _FILE, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
doc.unlink()
|
||||
NeutrinoServiceWriter(path, services).write()
|
||||
|
||||
|
||||
def get_services(path):
|
||||
return parse_services(path)
|
||||
return NeutrinoServicesReader(path).get_services()
|
||||
|
||||
|
||||
def parse_services(path):
|
||||
""" Parsing services from xml"""
|
||||
dom = parse(path + _FILE)
|
||||
services = []
|
||||
class NeutrinoServiceWriter:
|
||||
|
||||
for root in dom.getElementsByTagName("zapit"):
|
||||
api = root.attributes["api"].value
|
||||
def __init__(self, path, services):
|
||||
self._path = path + _FILE
|
||||
self._services = services
|
||||
|
||||
for elem in root.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
sat_name = elem.attributes["name"].value
|
||||
sat_pos = elem.attributes["position"].value
|
||||
diseqc = elem.attributes.get("diseqc")
|
||||
diseqc = diseqc.value if diseqc else diseqc
|
||||
uncommited = elem.attributes.get("uncommited")
|
||||
uncommited = uncommited.value if uncommited else uncommited
|
||||
sat = "{}:{}:{}:{}".format(sat_name, sat_pos, diseqc, uncommited)
|
||||
self._api = API_VER
|
||||
self._doc = NeutrinoDocument()
|
||||
self._root = self._doc.createElement("zapit")
|
||||
self._root.setAttribute("api", self._api)
|
||||
self._doc.appendChild(self._root)
|
||||
self._doc.appendChild(self._doc.createComment(" File was created in DemonEditor. Enjoy watching! "))
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
parse_transponder(api, sat, sat_pos, services, tr_elem)
|
||||
def write(self):
|
||||
srvs = defaultdict(list)
|
||||
for s in self._services:
|
||||
srvs[s.transponder_type].append(s)
|
||||
self.append_services(srvs.get(TrType.Satellite.value), "sat")
|
||||
self.append_services(srvs.get(TrType.Terrestrial.value), "terrestrial")
|
||||
self.append_services(srvs.get(TrType.Cable.value), "cable")
|
||||
|
||||
return services
|
||||
self._doc.write_xml(self._path)
|
||||
self._doc.unlink()
|
||||
|
||||
def append_services(self, services, s_type):
|
||||
if not services:
|
||||
return
|
||||
|
||||
sats = defaultdict(list)
|
||||
for srv in services:
|
||||
sats[srv[0]].append(srv)
|
||||
|
||||
for sat in sats:
|
||||
sat_elem = self._doc.createElement(s_type)
|
||||
attrs = get_attributes(sat)
|
||||
for k, v in attrs.items():
|
||||
sat_elem.setAttribute(k, v)
|
||||
|
||||
self._root.appendChild(sat_elem)
|
||||
|
||||
transponders = defaultdict(list)
|
||||
for srv in sats.get(sat):
|
||||
transponders[srv[-1]].append(srv)
|
||||
|
||||
for tr in transponders:
|
||||
tr_elem = self._doc.createElement("TS")
|
||||
for k, v in get_attributes(tr).items():
|
||||
tr_elem.setAttribute(k, v)
|
||||
sat_elem.appendChild(tr_elem)
|
||||
|
||||
for srv in transponders.get(tr):
|
||||
srv_elem = self._doc.createElement("S")
|
||||
s_attrs = get_attributes(srv.data_id)
|
||||
api = s_attrs.pop("api", self._api)
|
||||
if api != self._api:
|
||||
self._root.setAttribute("api", api)
|
||||
|
||||
for k, v in s_attrs.items():
|
||||
srv_elem.setAttribute(k, v)
|
||||
|
||||
tr_elem.appendChild(srv_elem)
|
||||
|
||||
|
||||
def parse_transponder(api, sat, sat_pos, services, tr_elem):
|
||||
tr_id = tr_elem.attributes["id"].value
|
||||
on = tr_elem.attributes["on"].value
|
||||
freq = tr_elem.attributes["frq"].value
|
||||
rate = tr_elem.attributes["sr"].value
|
||||
inv = tr_elem.attributes["inv"].value
|
||||
fec = tr_elem.attributes["fec"].value
|
||||
pol = tr_elem.attributes["pol"].value
|
||||
mod = tr_elem.attributes.get("mod")
|
||||
mod = mod.value if mod else mod
|
||||
sys = tr_elem.attributes.get("sys")
|
||||
sys = sys.value if sys else sys
|
||||
class NeutrinoServicesReader:
|
||||
|
||||
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
|
||||
tr_id = tr_id.lstrip("0")
|
||||
pol = POLARIZATION.get(pol)
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
sat_pos = int(sat_pos)
|
||||
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse_transponder]: {}".format(e))
|
||||
def __init__(self, path):
|
||||
self._path = path + _FILE
|
||||
self._attrs = None
|
||||
self._tr = None
|
||||
self._api = "4"
|
||||
self._services = []
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
ssid = srv_elem.attributes["i"].value
|
||||
name = srv_elem.attributes["n"].value
|
||||
srv_type = srv_elem.attributes["t"].value
|
||||
sys = srv_elem.attributes["s"].value
|
||||
num = srv_elem.attributes.get("num")
|
||||
num = num.value if num else num
|
||||
f = srv_elem.attributes.get("f")
|
||||
f = f.value if f else f
|
||||
v, a, p, pmt, tx, vt = [None] * 6
|
||||
# For v3 is possible so: '<S i="0001" n="name" t="1" s="0" num="770" f="4"/>' (equals v4 api)
|
||||
if api == "3" and len(srv_elem.attributes) > 6:
|
||||
v = srv_elem.attributes["v"].value
|
||||
a = srv_elem.attributes["a"].value
|
||||
p = srv_elem.attributes["p"].value
|
||||
pmt = srv_elem.attributes["pmt"].value
|
||||
tx = srv_elem.attributes["tx"].value
|
||||
vt = srv_elem.attributes["vt"].value
|
||||
def get_services(self):
|
||||
dom = XmlHandler.parse(self._path)
|
||||
|
||||
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv, st, = PROVIDER.get(int(on, 16)), SERVICE_TYPE.get(str(int(srv_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
for root in dom.getElementsByTagName("zapit"):
|
||||
if root.hasAttributes():
|
||||
api = root.attributes["api"]
|
||||
self._api = api.value if api else self._api
|
||||
|
||||
srv = Service(sat, None, None, name, None, None, prv, st, None, picon_id, ssid, freq, rate, pol,
|
||||
FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
|
||||
services.append(srv)
|
||||
for elem in root.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
sat_attrs = get_xml_attributes(elem)
|
||||
sat_pos = 0
|
||||
try:
|
||||
sat_pos = int(sat_attrs.get("position", "0"))
|
||||
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse sat position]: {}".format(e))
|
||||
sat = SP.join("{}{}{}".format(k, KSP, v) for k, v in sat_attrs.items())
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_sat_transponder(sat, sat_pos, tr_elem)
|
||||
|
||||
# Terrestrial DVB-T[2].
|
||||
for elem in root.getElementsByTagName("terrestrial"):
|
||||
if elem.hasAttributes():
|
||||
terr_attrs = get_xml_attributes(elem)
|
||||
terr = SP.join("{}{}{}".format(k, KSP, v) for k, v in terr_attrs.items())
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_ct_transponder(terr, tr_elem, TrType.Terrestrial)
|
||||
|
||||
# Cable.
|
||||
for elem in root.getElementsByTagName("cable"):
|
||||
if elem.hasAttributes():
|
||||
cable_attrs = get_xml_attributes(elem)
|
||||
cable = SP.join("{}{}{}".format(k, KSP, v) for k, v in cable_attrs.items())
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_ct_transponder(cable, tr_elem, TrType.Cable)
|
||||
|
||||
return self._services
|
||||
|
||||
def parse_sat_transponder(self, sat, sat_pos, tr_elem):
|
||||
tr_attr = get_xml_attributes(tr_elem)
|
||||
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_attr.items())
|
||||
tr_id = tr_attr.get("id", "0").lstrip("0")
|
||||
on = tr_attr.get("on", "0")
|
||||
freq = tr_attr.get("frq", "0")
|
||||
rate = tr_attr.get("sr", "0")
|
||||
fec = tr_attr.get("fec", "0")
|
||||
|
||||
pol = POLARIZATION.get(tr_attr.get("pol", "0"))
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse_transponder]: {}".format(e))
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
at = get_xml_attributes(srv_elem)
|
||||
at["api"] = self._api
|
||||
ssid, name, s_type, sys = at.get("i", "0"), at.get("n", ""), at.get("t", "3"), at.get("s", "0")
|
||||
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in at.items())
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv = PROVIDER.get(int(on, 16), "")
|
||||
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
srv = Service(sat, TrType.Satellite.value, None, name, None, None, prv, st, None, picon_id, ssid, freq,
|
||||
rate, pol, FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
|
||||
self._services.append(srv)
|
||||
|
||||
def parse_ct_transponder(self, terr, tr_elem, tr_type):
|
||||
attrs = get_xml_attributes(tr_elem)
|
||||
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in attrs.items())
|
||||
tr_id, on, freq = attrs.get("id", "0").lstrip("0"), attrs.get("on", "0"), attrs.get("frq", "0")
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
s_at = get_xml_attributes(srv_elem)
|
||||
s_at["api"] = self._api
|
||||
ssid, name, s_type, sys = s_at.get("i", "0"), s_at.get("n", ""), s_at.get("t", "3"), s_at.get("s", "0")
|
||||
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in s_at.items())
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv = PROVIDER.get(int(on, 16), "")
|
||||
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
if tr_type is TrType.Terrestrial:
|
||||
sys = T_SYSTEM.get(sys)
|
||||
pos = "T"
|
||||
elif tr_type is TrType.Cable:
|
||||
sys = SystemCable(sys).name
|
||||
pos = "C"
|
||||
else:
|
||||
log("Parse transponder error: Not supported type [{}]".format(tr_type))
|
||||
break
|
||||
|
||||
srv = Service(terr, tr_type.value, None, name, None, None, prv, st, None, picon_id, ssid,
|
||||
freq, "0", None, None, sys, pos, data_id, fav_id, tr)
|
||||
self._services.append(srv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
311
app/settings.py
311
app/settings.py
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
import locale
|
||||
@@ -9,22 +37,51 @@ from pathlib import Path
|
||||
from pprint import pformat
|
||||
from textwrap import dedent
|
||||
|
||||
SEP = os.sep
|
||||
HOME_PATH = str(Path.home())
|
||||
CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
|
||||
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = HOME_PATH + "/DemonEditor/data/"
|
||||
DATA_PATH = HOME_PATH + "{}DemonEditor{}".format(SEP, SEP)
|
||||
GTK_PATH = os.environ.get("GTK_PATH", None)
|
||||
|
||||
IS_DARWIN = sys.platform == "darwin"
|
||||
IS_WIN = sys.platform == "win32"
|
||||
IS_LINUX = sys.platform == "linux"
|
||||
|
||||
|
||||
class Defaults(Enum):
|
||||
""" Default program settings """
|
||||
USER = "root"
|
||||
PASSWORD = ""
|
||||
HOST = "127.0.0.1"
|
||||
FTP_PORT = "21"
|
||||
HTTP_PORT = "80"
|
||||
TELNET_PORT = "23"
|
||||
HTTP_USE_SSL = False
|
||||
# Enigma2.
|
||||
BOX_SERVICES_PATH = "/etc/enigma2/"
|
||||
BOX_SATELLITE_PATH = "/etc/tuxbox/"
|
||||
BOX_PICON_PATH = "/usr/share/enigma2/picon/"
|
||||
BOX_PICON_PATHS = ("/usr/share/enigma2/picon/",
|
||||
"/media/hdd/picon/",
|
||||
"/media/usb/picon/",
|
||||
"/media/mmc/picon/",
|
||||
"/media/cf/picon/")
|
||||
# Neutrino.
|
||||
NEUTRINO_BOX_SERVICES_PATH = "/var/tuxbox/config/zapit/"
|
||||
NEUTRINO_BOX_SATELLITE_PATH = "/var/tuxbox/config/"
|
||||
NEUTRINO_BOX_PICON_PATH = "/usr/share/tuxbox/neutrino/icons/logo/"
|
||||
NEUTRINO_BOX_PICON_PATHS = ("/usr/share/tuxbox/neutrino/icons/logo/",)
|
||||
# Paths.
|
||||
BACKUP_PATH = "{}backup{}".format(DATA_PATH, SEP)
|
||||
PICON_PATH = "{}picons{}".format(DATA_PATH, SEP)
|
||||
|
||||
DEFAULT_PROFILE = "default"
|
||||
BACKUP_BEFORE_DOWNLOADING = True
|
||||
BACKUP_BEFORE_SAVE = True
|
||||
V5_SUPPORT = False
|
||||
FORCE_BQ_NAMES = False
|
||||
HTTP_API_SUPPORT = False
|
||||
HTTP_API_SUPPORT = True
|
||||
ENABLE_YT_DL = False
|
||||
ENABLE_SEND_TO = False
|
||||
USE_COLORS = True
|
||||
@@ -34,63 +91,11 @@ class Defaults(Enum):
|
||||
LIST_PICON_SIZE = 32
|
||||
FAV_CLICK_MODE = 0
|
||||
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
|
||||
STREAM_LIB = "vlc"
|
||||
STREAM_LIB = "mpv" if IS_WIN else "vlc"
|
||||
PROFILE_FOLDER_DEFAULT = False
|
||||
RECORDS_PATH = DATA_PATH + "records/"
|
||||
RECORDS_PATH = DATA_PATH + "records{}".format(SEP)
|
||||
ACTIVATE_TRANSCODING = False
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV/device"
|
||||
|
||||
|
||||
def get_settings():
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
write_settings(get_default_settings())
|
||||
|
||||
with open(CONFIG_FILE, "r") as config_file:
|
||||
return json.load(config_file)
|
||||
|
||||
|
||||
def get_default_settings(profile_name="default"):
|
||||
def_settings = SettingsType.ENIGMA_2.get_default_settings()
|
||||
set_local_paths(def_settings, profile_name)
|
||||
|
||||
return {
|
||||
"version": 1,
|
||||
"default_profile": Defaults.DEFAULT_PROFILE.value,
|
||||
"profiles": {profile_name: def_settings},
|
||||
"v5_support": Defaults.V5_SUPPORT.value,
|
||||
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
|
||||
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
|
||||
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
|
||||
"use_colors": Defaults.USE_COLORS.value,
|
||||
"new_color": Defaults.NEW_COLOR.value,
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDS_PATH.value
|
||||
}
|
||||
|
||||
|
||||
def get_default_transcoding_presets():
|
||||
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
|
||||
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
|
||||
|
||||
|
||||
def write_settings(config):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_FILE, "w") as config_file:
|
||||
json.dump(config, config_file, indent=" ")
|
||||
|
||||
|
||||
def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False):
|
||||
settings["data_local_path"] = "{}{}/".format(data_path, profile_name)
|
||||
if use_profile_folder:
|
||||
settings["picons_local_path"] = "{}{}/{}/".format(data_path, profile_name, "picons")
|
||||
settings["backup_local_path"] = "{}{}/{}/".format(data_path, profile_name, "backup")
|
||||
else:
|
||||
settings["picons_local_path"] = "{}{}/{}/".format(data_path, "picons", profile_name)
|
||||
settings["backup_local_path"] = "{}{}/{}/".format(data_path, "backup", profile_name)
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV{}device".format(SEP)
|
||||
|
||||
|
||||
class SettingsType(IntEnum):
|
||||
@@ -99,29 +104,35 @@ class SettingsType(IntEnum):
|
||||
NEUTRINO_MP = 1
|
||||
|
||||
def get_default_settings(self):
|
||||
""" Returns default settings for current type """
|
||||
""" Returns default settings for current type. """
|
||||
if self is self.ENIGMA_2:
|
||||
return {"setting_type": self.value,
|
||||
"host": "127.0.0.1", "port": "21", "timeout": 5,
|
||||
"user": "root", "password": "root",
|
||||
"http_port": "80", "http_timeout": 5, "http_use_ssl": False,
|
||||
"telnet_port": "23", "telnet_timeout": 5,
|
||||
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
|
||||
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
|
||||
"picons_path": "/usr/share/enigma2/picon/",
|
||||
"picons_local_path": DATA_PATH + "enigma2/picons/",
|
||||
"backup_local_path": DATA_PATH + "enigma2/backup/"}
|
||||
elif self is self.NEUTRINO_MP:
|
||||
return {"setting_type": self,
|
||||
"host": "127.0.0.1", "port": "21", "timeout": 5,
|
||||
"user": "root", "password": "root",
|
||||
"http_port": "80", "http_timeout": 2, "http_use_ssl": False,
|
||||
"telnet_port": "23", "telnet_timeout": 1,
|
||||
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
|
||||
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
|
||||
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
|
||||
"picons_local_path": DATA_PATH + "neutrino/picons/",
|
||||
"backup_local_path": DATA_PATH + "neutrino/backup/"}
|
||||
srv_path = Defaults.BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.BOX_PICON_PATH.value
|
||||
http_timeout = 5
|
||||
telnet_timeout = 5
|
||||
else:
|
||||
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH.value
|
||||
http_timeout = 2
|
||||
telnet_timeout = 1
|
||||
|
||||
return {"setting_type": self.value,
|
||||
"host": Defaults.HOST.value,
|
||||
"port": Defaults.FTP_PORT.value,
|
||||
"timeout": 5,
|
||||
"user": Defaults.USER.value,
|
||||
"password": Defaults.PASSWORD.value,
|
||||
"http_port": Defaults.HTTP_PORT.value,
|
||||
"http_timeout": http_timeout,
|
||||
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
|
||||
"telnet_port": Defaults.TELNET_PORT.value,
|
||||
"telnet_timeout": telnet_timeout,
|
||||
"services_path": srv_path,
|
||||
"user_bouquet_path": srv_path,
|
||||
"satellites_xml_path": sat_path,
|
||||
"picons_path": picons_path}
|
||||
|
||||
|
||||
class SettingsException(Exception):
|
||||
@@ -141,11 +152,11 @@ class PlayStreamsMode(IntEnum):
|
||||
|
||||
class Settings:
|
||||
__INSTANCE = None
|
||||
__VERSION = 1
|
||||
__VERSION = 2
|
||||
|
||||
def __init__(self, ext_settings=None):
|
||||
try:
|
||||
settings = ext_settings or get_settings()
|
||||
settings = ext_settings or self.get_settings()
|
||||
except PermissionError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
@@ -176,22 +187,18 @@ class Settings:
|
||||
return cls.__INSTANCE
|
||||
|
||||
def save(self):
|
||||
write_settings(self._settings)
|
||||
self.write_settings(self._settings)
|
||||
|
||||
def reset(self, force_write=False):
|
||||
for k, v in self.setting_type.get_default_settings().items():
|
||||
self._cp_settings[k] = v
|
||||
|
||||
def_path = self.default_data_path
|
||||
def_path += "enigma2/" if self.setting_type is SettingsType.ENIGMA_2 else "neutrino/"
|
||||
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
|
||||
|
||||
if force_write:
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
def reset_to_default():
|
||||
write_settings(get_default_settings())
|
||||
Settings.write_settings(Settings.get_default_settings())
|
||||
|
||||
def get_default(self, p_name):
|
||||
""" Returns default value for current settings type """
|
||||
@@ -201,9 +208,9 @@ class Settings:
|
||||
""" Adds extra options """
|
||||
self._settings[name] = value
|
||||
|
||||
def get(self, name):
|
||||
def get(self, name, default=None):
|
||||
""" Returns extra options or None """
|
||||
return self._settings.get(name, None)
|
||||
return self._settings.get(name, default)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
@@ -232,6 +239,10 @@ class Settings:
|
||||
def default_profile(self, value):
|
||||
self._settings["default_profile"] = value
|
||||
|
||||
@property
|
||||
def current_profile_settings(self):
|
||||
return self._cp_settings
|
||||
|
||||
@property
|
||||
def profiles(self):
|
||||
return self._profiles
|
||||
@@ -355,6 +366,20 @@ class Settings:
|
||||
def picons_path(self, value):
|
||||
self._cp_settings["picons_path"] = value
|
||||
|
||||
@property
|
||||
def picons_paths(self):
|
||||
if self.setting_type is SettingsType.NEUTRINO_MP:
|
||||
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS.value)
|
||||
else:
|
||||
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS.value)
|
||||
|
||||
@picons_paths.setter
|
||||
def picons_paths(self, value):
|
||||
if self.setting_type is SettingsType.NEUTRINO_MP:
|
||||
self._settings["neutrino_picon_paths"] = value
|
||||
else:
|
||||
self._settings["picon_paths"] = value
|
||||
|
||||
# ***** Local paths ***** #
|
||||
|
||||
@property
|
||||
@@ -374,28 +399,48 @@ class Settings:
|
||||
self._settings["default_data_path"] = value
|
||||
|
||||
@property
|
||||
def data_local_path(self):
|
||||
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
|
||||
def default_backup_path(self):
|
||||
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH.value)
|
||||
|
||||
@data_local_path.setter
|
||||
def data_local_path(self, value):
|
||||
self._cp_settings["data_local_path"] = value
|
||||
@default_backup_path.setter
|
||||
def default_backup_path(self, value):
|
||||
self._settings["default_backup_path"] = value
|
||||
|
||||
@property
|
||||
def picons_local_path(self):
|
||||
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
|
||||
def default_picon_path(self):
|
||||
return self._settings.get("default_picon_path", Defaults.PICON_PATH.value)
|
||||
|
||||
@picons_local_path.setter
|
||||
def picons_local_path(self, value):
|
||||
self._cp_settings["picons_local_path"] = value
|
||||
@default_picon_path.setter
|
||||
def default_picon_path(self, value):
|
||||
self._settings["default_picon_path"] = value
|
||||
|
||||
@property
|
||||
def backup_local_path(self):
|
||||
return self._cp_settings.get("backup_local_path", self.get_default("backup_local_path"))
|
||||
def profile_data_path(self):
|
||||
return "{}data{}{}{}".format(self.default_data_path, SEP, self._current_profile, SEP)
|
||||
|
||||
@backup_local_path.setter
|
||||
def backup_local_path(self, value):
|
||||
self._cp_settings["backup_local_path"] = value
|
||||
@profile_data_path.setter
|
||||
def profile_data_path(self, value):
|
||||
self._cp_settings["profile_data_path"] = value
|
||||
|
||||
@property
|
||||
def profile_picons_path(self):
|
||||
if self.profile_folder_is_default:
|
||||
return "{}picons{}".format(self.profile_data_path, SEP)
|
||||
return "{}{}{}".format(self.default_picon_path, self._current_profile, SEP)
|
||||
|
||||
@profile_picons_path.setter
|
||||
def profile_picons_path(self, value):
|
||||
self._cp_settings["profile_picons_path"] = value
|
||||
|
||||
@property
|
||||
def profile_backup_path(self):
|
||||
if self.profile_folder_is_default:
|
||||
return "{}backup{}".format(self.profile_data_path, SEP)
|
||||
return "{}{}{}".format(self.default_backup_path, self._current_profile, SEP)
|
||||
|
||||
@profile_backup_path.setter
|
||||
def profile_backup_path(self, value):
|
||||
self._cp_settings["profile_backup_path"] = value
|
||||
|
||||
@property
|
||||
def records_path(self):
|
||||
@@ -425,7 +470,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def transcoding_presets(self):
|
||||
return self._settings.get("transcoding_presets", get_default_transcoding_presets())
|
||||
return self._settings.get("transcoding_presets", self.get_default_transcoding_presets())
|
||||
|
||||
@transcoding_presets.setter
|
||||
def transcoding_presets(self, value):
|
||||
@@ -618,6 +663,13 @@ class Settings:
|
||||
|
||||
@property
|
||||
def dark_mode(self):
|
||||
if IS_DARWIN:
|
||||
import subprocess
|
||||
|
||||
cmd = ["defaults", "read", "-g", "AppleInterfaceStyle"]
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
return "Dark" in str(p[0])
|
||||
|
||||
return self._settings.get("dark_mode", False)
|
||||
|
||||
@dark_mode.setter
|
||||
@@ -659,7 +711,7 @@ class Settings:
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def themes_path(self):
|
||||
return "{}/.themes/".format(HOME_PATH)
|
||||
return "{}{}.themes{}".format(HOME_PATH, SEP, SEP)
|
||||
|
||||
@property
|
||||
def icon_theme(self):
|
||||
@@ -672,7 +724,7 @@ class Settings:
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def icon_themes_path(self):
|
||||
return "{}/.icons/".format(HOME_PATH)
|
||||
return "{}{}.icons{}".format(HOME_PATH, SEP, SEP)
|
||||
|
||||
@property
|
||||
def is_darwin(self):
|
||||
@@ -717,6 +769,49 @@ class Settings:
|
||||
def is_enable_experimental(self, value):
|
||||
self._settings["enable_experimental"] = value
|
||||
|
||||
# **************** 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())
|
||||
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
|
||||
return json.load(config_file)
|
||||
|
||||
@staticmethod
|
||||
def get_default_settings(profile_name="default"):
|
||||
def_settings = SettingsType.ENIGMA_2.get_default_settings()
|
||||
|
||||
return {
|
||||
"version": Settings.__VERSION,
|
||||
"default_profile": Defaults.DEFAULT_PROFILE.value,
|
||||
"profiles": {profile_name: def_settings},
|
||||
"v5_support": Defaults.V5_SUPPORT.value,
|
||||
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
|
||||
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
|
||||
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
|
||||
"use_colors": Defaults.USE_COLORS.value,
|
||||
"new_color": Defaults.NEW_COLOR.value,
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDS_PATH.value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_default_transcoding_presets():
|
||||
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
|
||||
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
|
||||
"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=" ")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,54 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT
|
||||
from gi.repository import Gdk, Gtk, GObject
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
|
||||
|
||||
|
||||
class Player(ABC):
|
||||
class Player(Gtk.DrawingArea):
|
||||
""" Base player class. Also used as a factory. """
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, mode, widget, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("message", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("position", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("played", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("audio-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self.connect("draw", self.on_draw)
|
||||
self.connect("motion-notify-event", self.on_mouse_motion)
|
||||
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
widget.add(self)
|
||||
|
||||
parent = widget.get_parent()
|
||||
parent.connect("play", self.on_play)
|
||||
parent.connect("stop", self.on_stop)
|
||||
self.show()
|
||||
|
||||
def get_play_mode(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def play(self, mrl=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_playing(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def set_audio_track(self, track):
|
||||
pass
|
||||
|
||||
def get_window_handle(self, widget):
|
||||
def get_audio_track(self):
|
||||
pass
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
pass
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
pass
|
||||
|
||||
def get_instance(self, mode, widget):
|
||||
pass
|
||||
|
||||
def on_play(self, widget, url):
|
||||
self.play(url)
|
||||
|
||||
def on_stop(self, widget, state):
|
||||
self.stop()
|
||||
|
||||
def on_release(self, widget, state):
|
||||
self.release()
|
||||
|
||||
def get_window_handle(self):
|
||||
""" Returns the identifier [pointer] for the window.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if sys.platform == "linux":
|
||||
return widget.get_window().get_xid()
|
||||
return self.get_window().get_xid()
|
||||
else:
|
||||
is_darwin = sys.platform == "darwin"
|
||||
try:
|
||||
@@ -61,48 +129,47 @@ class Player(ABC):
|
||||
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None)
|
||||
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
|
||||
get_pointer.restype = ctypes.c_void_p
|
||||
get_pointer.argtypes = [ctypes.c_void_p]
|
||||
|
||||
return get_pointer(gpointer)
|
||||
|
||||
def get_video_widget(self, widget):
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
area = Gtk.DrawingArea(visible=True)
|
||||
area.connect("draw", self.on_drawing_area_draw)
|
||||
area.set_events(Gdk.ModifierType.BUTTON1_MASK)
|
||||
widget.add(area)
|
||||
|
||||
return area
|
||||
|
||||
def on_drawing_area_draw(self, widget, cr):
|
||||
def on_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
def on_mouse_motion(self, widget, event):
|
||||
display = widget.get_display()
|
||||
window = widget.get_window()
|
||||
cursor = Gdk.Cursor.new_from_name(display, "default")
|
||||
window.set_cursor(cursor)
|
||||
|
||||
self.hide_mouse_cursor(window, display)
|
||||
|
||||
@run_with_delay(3)
|
||||
def hide_mouse_cursor(self, window, display):
|
||||
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
|
||||
window.set_cursor(cursor)
|
||||
|
||||
@staticmethod
|
||||
def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
def make(name, mode, widget):
|
||||
""" Factory method. We will not use a separate factory to return a specific implementation.
|
||||
|
||||
@param name: implementation name.
|
||||
@param mode: current player mode [Built-in or windowed].
|
||||
@param widget: parent of video widget.
|
||||
@param buf_cb: buffering callback.
|
||||
@param position_cb: time (position) callback.
|
||||
@param error_cb: error callback.
|
||||
@param playing_cb: playing state callback.
|
||||
|
||||
Throws a NameError if there is no implementation for the given name.
|
||||
"""
|
||||
if name == "mpv":
|
||||
return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return MpvPlayer.get_instance(mode, widget)
|
||||
elif name == "gst":
|
||||
return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return GstPlayer.get_instance(mode, widget)
|
||||
elif name == "vlc":
|
||||
return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return VlcPlayer.get_instance(mode, widget)
|
||||
else:
|
||||
raise NameError("There is no such [{}] implementation.".format(name))
|
||||
|
||||
@@ -114,11 +181,15 @@ class MpvPlayer(Player):
|
||||
"""
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
from app.tools import mpv
|
||||
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget))))
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle()),
|
||||
input_default_bindings=False,
|
||||
input_cursor=False,
|
||||
cursor_autohide="no")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No libmpv is found. Check that it is installed!")
|
||||
@@ -129,25 +200,24 @@ class MpvPlayer(Player):
|
||||
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
|
||||
def on_open(event):
|
||||
log("Starting playback...")
|
||||
playing_cb()
|
||||
self.emit("played", 0)
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.END_FILE)
|
||||
def on_end(event):
|
||||
event = event.get("event", {})
|
||||
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
|
||||
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
|
||||
error_cb()
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if not mrl:
|
||||
return
|
||||
@@ -155,7 +225,6 @@ class MpvPlayer(Player):
|
||||
self._player.play(mrl)
|
||||
self._is_playing = True
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
self._player.stop()
|
||||
self._is_playing = True
|
||||
@@ -180,7 +249,8 @@ class GstPlayer(Player):
|
||||
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
import gi
|
||||
|
||||
@@ -189,29 +259,17 @@ class GstPlayer(Player):
|
||||
from gi.repository import Gst, GstVideo
|
||||
# Initialization of GStreamer.
|
||||
Gst.init(sys.argv)
|
||||
gtk_sink = Gst.ElementFactory.make("gtksink")
|
||||
if not gtk_sink:
|
||||
msg = "GStreamer error: gtksink plugin not installed!"
|
||||
log(msg)
|
||||
raise ImportError(msg)
|
||||
except (OSError, ValueError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No GStreamer is found. Check that it is installed!")
|
||||
else:
|
||||
self._error_cb = error_cb
|
||||
self._playing_cb = playing_cb
|
||||
|
||||
self.STATE = Gst.State
|
||||
self.STAT_RETURN = Gst.StateChangeReturn
|
||||
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
self._player = Gst.ElementFactory.make("playbin", "player")
|
||||
# Initialization of the playback widget.
|
||||
self._player.set_property("video-sink", gtk_sink)
|
||||
vid_widget = gtk_sink.props.widget
|
||||
widget.add(vid_widget)
|
||||
vid_widget.show()
|
||||
self._player.set_window_handle(self.get_window_handle())
|
||||
|
||||
bus = self._player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
@@ -220,9 +278,9 @@ class GstPlayer(Player):
|
||||
bus.connect("message::eos", self.on_eos)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
cls.__INSTANCE = GstPlayer(mode, widget)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
@@ -239,8 +297,11 @@ class GstPlayer(Player):
|
||||
ret = self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
if ret == self.STAT_RETURN.FAILURE:
|
||||
log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl))
|
||||
msg = "ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl)
|
||||
log(msg)
|
||||
self.emit("error", msg)
|
||||
else:
|
||||
self.emit("played", 0)
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
@@ -269,7 +330,7 @@ class GstPlayer(Player):
|
||||
def on_error(self, bus, msg):
|
||||
err, dbg = msg.parse_error()
|
||||
log(err)
|
||||
self._error_cb()
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
def on_state_changed(self, bus, msg):
|
||||
if not msg.src == self._player:
|
||||
@@ -279,7 +340,7 @@ class GstPlayer(Player):
|
||||
old_state, new_state, pending = msg.parse_state_changed()
|
||||
if new_state is self.STATE.PLAYING:
|
||||
log("Starting playback...")
|
||||
self._playing_cb()
|
||||
self.emit("played", 0)
|
||||
self.get_stream_info()
|
||||
|
||||
def on_eos(self, bus, msg):
|
||||
@@ -314,14 +375,20 @@ class VlcPlayer(Player):
|
||||
|
||||
__VLC_INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
|
||||
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
|
||||
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
|
||||
self._player = vlc.Instance(args).media_player_new()
|
||||
except (OSError, AttributeError) as e:
|
||||
vlc.libvlc_video_set_key_input(self._player, False)
|
||||
vlc.libvlc_video_set_mouse_input(self._player, False)
|
||||
except (OSError, AttributeError, NameError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No VLC is found. Check that it is installed!")
|
||||
else:
|
||||
@@ -329,45 +396,28 @@ class VlcPlayer(Player):
|
||||
self._is_playing = False
|
||||
|
||||
ev_mgr = self._player.event_manager()
|
||||
|
||||
if buf_cb:
|
||||
# TODO look other EventType options
|
||||
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
|
||||
lambda et, p: buf_cb(p.get_media().get_duration()),
|
||||
self._player)
|
||||
if position_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et, p: position_cb(p.get_time()),
|
||||
self._player)
|
||||
|
||||
if error_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
|
||||
lambda et, p: error_cb(),
|
||||
self._player)
|
||||
if playing_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
|
||||
lambda et, p: playing_cb(),
|
||||
self._player)
|
||||
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et: self.emit("position", self._player.get_time()))
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
|
||||
|
||||
self.init_video_widget(widget)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__VLC_INSTANCE:
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget)
|
||||
return cls.__VLC_INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if mrl:
|
||||
self._player.set_mrl(mrl)
|
||||
self._player.play()
|
||||
self._is_playing = True
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
if self._is_playing:
|
||||
self._player.stop()
|
||||
@@ -393,14 +443,34 @@ class VlcPlayer(Player):
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_audio_track(self, track):
|
||||
self._player.audio_set_track(track)
|
||||
|
||||
def get_audio_track(self):
|
||||
return self._player.audio_get_track()
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
self._player.video_set_spu(track)
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
self._player.video_set_aspect_ratio(ratio)
|
||||
|
||||
def on_playback_start(self, event):
|
||||
self.emit("played", self._player.get_media().get_duration())
|
||||
# Audio tracks
|
||||
a_desc = self._player.audio_get_track_description()
|
||||
self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc])
|
||||
# Subtitle
|
||||
s_desc = self._player.video_get_spu_description()
|
||||
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
|
||||
|
||||
def init_video_widget(self, widget):
|
||||
video_widget = self.get_video_widget(widget)
|
||||
if sys.platform == "linux":
|
||||
self._player.set_xwindow(video_widget.get_window().get_xid())
|
||||
self._player.set_xwindow(self.get_window_handle())
|
||||
elif sys.platform == "darwin":
|
||||
self._player.set_nsobject(self.get_window_handle(video_widget))
|
||||
self._player.set_nsobject(self.get_window_handle())
|
||||
else:
|
||||
log("Video widget initialization error: platform '{}' is not supported. ".format(sys.platform))
|
||||
self._player.set_hwnd(self.get_window_handle())
|
||||
|
||||
|
||||
class Recorder:
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from html.parser import HTMLParser
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import run_task, log
|
||||
from app.settings import SettingsType
|
||||
from app.settings import SettingsType, IS_LINUX, IS_WIN, IS_DARWIN, GTK_PATH
|
||||
from .satellites import _HEADERS
|
||||
|
||||
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
|
||||
@@ -18,6 +47,193 @@ Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid"
|
||||
Picon = namedtuple("Picon", ["ref", "ssid"])
|
||||
|
||||
|
||||
class PiconsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PiconsCzDownloader:
|
||||
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
|
||||
|
||||
_PERM_URL = "https://picon.cz/download/7337"
|
||||
_BASE_URL = "https://picon.cz/download/"
|
||||
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
|
||||
_HEADER = {"User-Agent": "DemonEditor/2.0.0", "Referer": ""}
|
||||
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
|
||||
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
|
||||
|
||||
def __init__(self, picon_ids=set(), appender=log):
|
||||
self._perm_links = {}
|
||||
self._providers = {}
|
||||
self._provider_logos = {}
|
||||
self._picon_ids = picon_ids
|
||||
self._appender = appender
|
||||
|
||||
def init(self):
|
||||
""" Initializes dict with values: download_id -> perm link and provider data. """
|
||||
if self._perm_links:
|
||||
return
|
||||
|
||||
self._HEADER["Referer"] = self._PERM_URL
|
||||
|
||||
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
logo_map = self.get_logos_map()
|
||||
name_map = self.get_name_map()
|
||||
|
||||
for line in request.iter_lines():
|
||||
data = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
|
||||
if len(data) != 2:
|
||||
continue
|
||||
|
||||
l_id, perm_link = data
|
||||
self._perm_links[str(l_id)] = str(perm_link)
|
||||
data = re.match(self._LINK_PATTERN, perm_link)
|
||||
if data:
|
||||
sat_pos = data.group(3)
|
||||
# Logo url.
|
||||
logo = logo_map.get(data.group(2), None)
|
||||
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
|
||||
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
|
||||
|
||||
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
|
||||
if sat_pos in self._providers:
|
||||
self._providers[sat_pos].append(prv)
|
||||
else:
|
||||
self._providers[sat_pos] = [prv]
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}")
|
||||
raise PiconsError(request.reason)
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return self._providers
|
||||
|
||||
def get_sat_providers(self, url):
|
||||
return self._providers.get(url, [])
|
||||
|
||||
def download(self, provider, picons_path, picon_ids=None):
|
||||
self._HEADER["Referer"] = provider.url
|
||||
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
dest = f"{picons_path}{provider.on_id}.7z"
|
||||
self._appender(f"Downloading: {provider.url}\n")
|
||||
with open(dest, mode="bw") as f:
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
f.write(data)
|
||||
self._appender(f"Extracting: {provider.on_id}\n")
|
||||
self.extract(dest, picons_path, picon_ids)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {request.reason}")
|
||||
|
||||
def extract(self, src, dest, picon_ids=None):
|
||||
""" Extracts 7z archives. """
|
||||
# TODO: think about https://github.com/miurahr/py7zr
|
||||
exe = "7z"
|
||||
if IS_DARWIN and GTK_PATH:
|
||||
exe = "./7z"
|
||||
|
||||
if IS_LINUX and not os.path.isfile(f"/usr/bin/{exe}"):
|
||||
raise PiconsError("7-zip [7z] archiver not found!")
|
||||
|
||||
if IS_WIN:
|
||||
exe = f"C:{os.sep}Program Files{os.sep}7-Zip{os.sep}{exe}.exe"
|
||||
if not os.path.isfile(exe):
|
||||
raise PiconsError("7-Zip executable not found!")
|
||||
|
||||
cmd = [exe, "l", src]
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if err:
|
||||
log(f"{self.__class__.__name__} [extract] error: {err}")
|
||||
raise PiconsError(err)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [extract] error: {e}")
|
||||
raise PiconsError(e)
|
||||
|
||||
is_filter = bool(picon_ids)
|
||||
ids = picon_ids or self._picon_ids
|
||||
to_extract = []
|
||||
|
||||
for o in re.finditer(self._FILE_PATTERN, out):
|
||||
p_id = o.group(1).decode("utf-8", errors="ignore")
|
||||
if p_id in ids:
|
||||
to_extract.append(p_id)
|
||||
|
||||
if is_filter and not to_extract:
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
raise PiconsError("No matching picons found!")
|
||||
|
||||
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
|
||||
cmd.extend(to_extract)
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if err:
|
||||
log(f"{self.__class__.__name__} [extract] error: {err}")
|
||||
raise PiconsError(err)
|
||||
else:
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
raise PiconsError(e)
|
||||
|
||||
def get_logo_data(self, url):
|
||||
""" Returns the logo data if present. """
|
||||
return self._provider_logos.get(url, None)
|
||||
|
||||
def get_provider_logo(self, url):
|
||||
""" Retrieves package logo. """
|
||||
# Getting package logo.
|
||||
logo = self._provider_logos.get(url, None)
|
||||
if logo:
|
||||
return logo
|
||||
|
||||
try:
|
||||
with requests.get(url=url, stream=True) as logo_request:
|
||||
if logo_request.reason == "OK":
|
||||
data = logo_request.content
|
||||
self._provider_logos[url] = data
|
||||
return data
|
||||
else:
|
||||
log(f"Downloading package logo error: {logo_request.reason}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log(f"{self.__class__.__name__} error [get provider logo]: {e}")
|
||||
|
||||
def get_logos_map(self):
|
||||
return {"piconblack": "b50",
|
||||
"picontransparent": "t50",
|
||||
"piconwhite": "w50",
|
||||
"piconmirrorglass": "mr100",
|
||||
"piconnoName": "n100",
|
||||
"piconsrhd": "srhd100",
|
||||
"piconfreezeframe": "ff220",
|
||||
"piconfreezewhite": "fw100",
|
||||
"piconpoolrainbow": "r100",
|
||||
"piconsimpleblack": "s220",
|
||||
"piconjustblack": "jb220",
|
||||
"picondirtypaper": "dp220",
|
||||
"picongray": "g400",
|
||||
"piconmonochrom": "m220",
|
||||
"picontransparentwhite": "tw100",
|
||||
"picontransparentdark": "td220",
|
||||
"piconoled": "o96",
|
||||
"piconblack80": "b50",
|
||||
"piconblack3d": "b50"
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
return {"antiksat": "ANTIK",
|
||||
"digiczsk": "DIGI",
|
||||
"DTTitaly": "picon_trs-it",
|
||||
"dvbtCZSK": "picon_trs",
|
||||
"PolandDTT": "picon_trs-pl",
|
||||
"freeSAT": "UPC DIRECT",
|
||||
"orangesat": "ORANGE TV",
|
||||
"skylink": "M7 GROUP",
|
||||
}
|
||||
|
||||
|
||||
class PiconsParser(HTMLParser):
|
||||
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
@@ -65,15 +281,13 @@ class PiconsParser(HTMLParser):
|
||||
if self._single and ln == 4 and row[0].startswith("/logo/"):
|
||||
self.picons.append(Picon(row[0].strip(), "0"))
|
||||
else:
|
||||
if ln == 9:
|
||||
if ln > 8:
|
||||
url = None
|
||||
if row[0].startswith("/logo/"):
|
||||
url = row[0]
|
||||
elif row[1].startswith("/logo/"):
|
||||
url = row[1]
|
||||
if row[2].startswith("/logo/"):
|
||||
url = row[2]
|
||||
|
||||
if url and row[-3].isdigit():
|
||||
self.picons.append(Picon(url, row[-3]))
|
||||
if url and row[0].isdigit():
|
||||
self.picons.append(Picon(url, row[0]))
|
||||
|
||||
self._current_row = []
|
||||
|
||||
@@ -111,6 +325,9 @@ class PiconsParser(HTMLParser):
|
||||
namespace = "{:X}{:X}".format(int(pos), int(freq))
|
||||
else:
|
||||
namespace = "{:X}0000".format(int(pos))
|
||||
|
||||
if single and not ssid.isdigit():
|
||||
ssid = "".join(c for c in ssid if c.isdigit()) or "0"
|
||||
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
picons_data.append(("{}{}".format(PiconsParser._BASE_URL, p.ref), p_name))
|
||||
@@ -172,20 +389,11 @@ class ProviderParser(HTMLParser):
|
||||
url = attrs[0][1]
|
||||
if any(d in url for d in self._DOMAINS):
|
||||
self._current_row.append(url)
|
||||
if tag == "font" and len(attrs) == 1:
|
||||
atr = attrs[0]
|
||||
if len(atr) == 2 and atr[1] == "darkgreen":
|
||||
self._is_onid_tid = True
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
if self._is_onid_tid:
|
||||
m = self._ONID_TID_PATTERN.match(data)
|
||||
if m:
|
||||
self._on_id, tid = m.group().split("-")
|
||||
self._is_onid_tid = False
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
@@ -207,32 +415,34 @@ class ProviderParser(HTMLParser):
|
||||
|
||||
len_row = len(row)
|
||||
if len_row > 2:
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[1])
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[0])
|
||||
if m:
|
||||
self._freq = m.group().split()[0]
|
||||
|
||||
if len_row == 14:
|
||||
if len_row > 12:
|
||||
# Providers
|
||||
name = row[6]
|
||||
name = row[5]
|
||||
self._prv_names.add(name)
|
||||
m = self._ONID_TID_PATTERN.match(str(row[9]))
|
||||
m = self._ONID_TID_PATTERN.match(str(row[-5]))
|
||||
if m:
|
||||
on_id, tid = m.group().split("-")
|
||||
if on_id not in self._ids:
|
||||
self._on_id = on_id
|
||||
row[-2] = on_id
|
||||
self._ids.add(on_id)
|
||||
row[0] = self._positon
|
||||
if name + on_id not in self._prv_names:
|
||||
self._prv_names.add(name + on_id)
|
||||
logo_data = None
|
||||
req = requests.get(self._BASE_URL + row[3], timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.content
|
||||
else:
|
||||
log("Downloading provider logo error: {}".format(req.reason))
|
||||
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[5], on_id=on_id,
|
||||
if row[2].startswith("/logo/"):
|
||||
req = requests.get(self._BASE_URL + row[2], timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.content
|
||||
else:
|
||||
log("Downloading provider logo error: {}".format(req.reason))
|
||||
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[6], on_id=on_id,
|
||||
ssid=None, single=False, selected=True))
|
||||
elif 6 < len_row < 14:
|
||||
elif 6 < len_row < 12:
|
||||
# Single services
|
||||
name, url, ssid = None, None, None
|
||||
if row[0].startswith("http"):
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.eparser import Satellite, Transponder, is_transponder_valid
|
||||
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
|
||||
Service, CAS)
|
||||
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0"}
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0"}
|
||||
|
||||
|
||||
class SatelliteSource(Enum):
|
||||
@@ -77,6 +77,8 @@ class Cell:
|
||||
class SatellitesParser(HTMLParser):
|
||||
""" Parser for satellite html page. """
|
||||
|
||||
POS_PAT = re.compile(r".*?(\d+\.\d°?[EW]).*")
|
||||
|
||||
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
@@ -150,44 +152,25 @@ class SatellitesParser(HTMLParser):
|
||||
|
||||
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
extra_pattern = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html")
|
||||
base_url = "https://www.lyngsat.com/"
|
||||
sats = []
|
||||
names = set()
|
||||
current_pos = "0"
|
||||
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
|
||||
r_len = len(row)
|
||||
if r_len == 7:
|
||||
current_pos = self.parse_position(row[2])
|
||||
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
|
||||
if name not in names:
|
||||
# [all in one] satellites
|
||||
sats.append((name, current_pos, row[5], base_url + row[1], False))
|
||||
names.add(name)
|
||||
name = row[4]
|
||||
if name not in names:
|
||||
sats.append((name, current_pos, row[5], base_url + row[3], False))
|
||||
names.add(name)
|
||||
if r_len == 8: # for a very limited number of satellites
|
||||
data = list(filter(None, row))
|
||||
urls = set()
|
||||
sat_type = ""
|
||||
for d in data:
|
||||
url = re.match(extra_pattern, d)
|
||||
if url:
|
||||
urls.add(url.group(0))
|
||||
if d in ("C", "Ku", "CKu"):
|
||||
sat_type = d
|
||||
current_pos = self.parse_position(data[1])
|
||||
for url in urls:
|
||||
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
|
||||
sats.append((name, current_pos, sat_type, base_url + url, False))
|
||||
elif r_len == 5:
|
||||
sats.append((row[2], current_pos, row[3], base_url + row[1], False))
|
||||
cur_pos = "0"
|
||||
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
|
||||
if not row[0]:
|
||||
row = row[1:]
|
||||
|
||||
pos = self.parse_position(row[1])
|
||||
if not self.POS_PAT.match(pos):
|
||||
if len(row) == 4 and row[0].endswith(".html"):
|
||||
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
|
||||
continue
|
||||
|
||||
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
|
||||
cur_pos = pos
|
||||
return sats
|
||||
elif source is SatelliteSource.KINGOFSAT:
|
||||
def get_sat(r):
|
||||
return r[3], self.parse_position(r[1]), None, r[0], False
|
||||
return r[3], self.parse_position(r[1]), None, r[2], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
|
||||
|
||||
@@ -321,11 +304,10 @@ class SatellitesParser(HTMLParser):
|
||||
Since the *.ini file contains incomplete information, it is not used.
|
||||
"""
|
||||
zeros = "000"
|
||||
pos_pat = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
pat = re.compile(
|
||||
r"(\d+).00\s+([RLHV])\s+(DVB-S[2]?)\s+(?:T2-MI, PLP (\d+)\s+)?(.*PSK).*?(?:Stream\s+(\d+))?\s+(\d+)\s+(\d+/\d+)$")
|
||||
|
||||
for row in filter(lambda r: len(r) == 16 and pos_pat.match(r[0]), self._rows):
|
||||
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
|
||||
res = pat.search(" ".join((row[0], row[2], row[3], row[8], row[9], row[10])))
|
||||
if res:
|
||||
freq, sr, pol, fec, sys = res.group(1), res.group(7), res.group(2), res.group(8), res.group(3)
|
||||
@@ -343,9 +325,10 @@ class ServicesParser(HTMLParser):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "SD": "1", "MPEG-4 SD": "22", "HEVC SD": "22", "MPEG-4 HD": "25",
|
||||
"MPEG-4 HD 1080": "25", "MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC UHD": "31",
|
||||
"HEVC UHD 4K": "31"}
|
||||
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "MPEG-2/SD": "1", "SD": "1", "MPEG-4 SD": "22", "MPEG-4/SD": "22",
|
||||
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
|
||||
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
|
||||
"HEVC UHD": "31", "HEVC UHD 4K": "31", "3": "Data"}
|
||||
self._TR_PAT = re.compile(
|
||||
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
@@ -361,6 +344,25 @@ class ServicesParser(HTMLParser):
|
||||
self._current_cell = Cell()
|
||||
self._rows = []
|
||||
self._source = source
|
||||
self._t_url = ""
|
||||
self._use_short_names = True
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
return self._source
|
||||
|
||||
@source.setter
|
||||
def source(self, value):
|
||||
self._source = value
|
||||
self.reset()
|
||||
|
||||
@property
|
||||
def use_short_names(self):
|
||||
return self._use_short_names
|
||||
|
||||
@use_short_names.setter
|
||||
def use_short_names(self, value):
|
||||
self._use_short_names = value
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "td":
|
||||
@@ -368,10 +370,24 @@ class ServicesParser(HTMLParser):
|
||||
elif tag == "tr":
|
||||
self._is_th = True
|
||||
elif tag == "a" and not self._current_cell.url:
|
||||
self._current_cell.url = attrs[0][1]
|
||||
if attrs:
|
||||
for a in attrs:
|
||||
if a[0] == "href":
|
||||
self._current_cell.url = a[1]
|
||||
|
||||
if self._source is SatelliteSource.KINGOFSAT and self._use_short_names:
|
||||
if a[0] != "title":
|
||||
continue
|
||||
txt = a[1]
|
||||
if txt and txt.startswith("Id: "):
|
||||
# Saving the 'short' name.
|
||||
self._current_cell.text = txt.lstrip("Id: ")
|
||||
elif tag == "img":
|
||||
img_link = attrs[0][1]
|
||||
if img_link.startswith("/logo/"):
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
if img_link.startswith("/logo/"):
|
||||
self._current_cell.img = img_link
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self._current_cell.img = img_link
|
||||
|
||||
def handle_data(self, data):
|
||||
@@ -386,8 +402,9 @@ class ServicesParser(HTMLParser):
|
||||
self._is_th = False
|
||||
|
||||
if tag in ("td", "th"):
|
||||
final_cell = self._separator.join(self._current_cell_text).strip()
|
||||
self._current_cell.text = final_cell
|
||||
if not self._current_cell.text:
|
||||
txt = self._separator.join(self._current_cell_text).strip()
|
||||
self._current_cell.text = txt
|
||||
self._current_row.append(self._current_cell)
|
||||
self._current_cell_text = []
|
||||
self._current_cell = Cell()
|
||||
@@ -401,7 +418,7 @@ class ServicesParser(HTMLParser):
|
||||
|
||||
def init_data(self, url):
|
||||
""" Initializes data for the given URL. """
|
||||
if self._source is not SatelliteSource.LYNGSAT:
|
||||
if self._source not in (SatelliteSource.LYNGSAT, SatelliteSource.KINGOFSAT):
|
||||
raise ValueError("Unsupported source: {}!".format(self._source.name))
|
||||
|
||||
self._rows.clear()
|
||||
@@ -416,13 +433,26 @@ class ServicesParser(HTMLParser):
|
||||
def get_transponders_links(self, sat_url):
|
||||
""" Returns transponder links. """
|
||||
try:
|
||||
if self._source is SatelliteSource.KINGOFSAT:
|
||||
sat_url = "https://en.kingofsat.net/" + sat_url
|
||||
self.init_data(sat_url)
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
else:
|
||||
url = "https://www.lyngsat.com/muxes/"
|
||||
return [row[1] for row in
|
||||
filter(lambda x: x and len(x) > 8 and x[1].url and x[1].url.startswith(url), self._rows)]
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
url = "https://www.lyngsat.com/muxes/"
|
||||
return [row[0] for row in
|
||||
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].url.startswith(url), self._rows)]
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
trs = []
|
||||
for r in self._rows:
|
||||
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.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
|
||||
trs.append(t_cell)
|
||||
return trs
|
||||
return []
|
||||
|
||||
def get_transponder_services(self, tr_url, sat_position=None, use_pids=False):
|
||||
@@ -432,90 +462,155 @@ class ServicesParser(HTMLParser):
|
||||
@param sat_position: custom satellite position. Sometimes required to adjust the namespace.
|
||||
@param use_pids: if possible use additional pids [video, audio].
|
||||
"""
|
||||
services = []
|
||||
try:
|
||||
self._t_url = tr_url
|
||||
self.init_data(tr_url)
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
else:
|
||||
pos, freq, sr, fec, pol, namespace, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
|
||||
sys = "DVB-S"
|
||||
pos_found = False
|
||||
tr = None
|
||||
# Transponder
|
||||
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
|
||||
if not pos_found:
|
||||
pos_tr = re.match(self._POS_PAT, r[0].text)
|
||||
if not pos_tr:
|
||||
continue
|
||||
|
||||
if not sat_position:
|
||||
pos = int(SatellitesParser.get_position(
|
||||
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
|
||||
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
text = " ".join(c.text for c in r[1:])
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
if td.group(5):
|
||||
log("Detected T2-MI transponder!")
|
||||
continue
|
||||
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
|
||||
nid, tid = td.group(8), td.group(9)
|
||||
|
||||
neg_pos = False # POS = W
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
|
||||
inv = 2 # Default
|
||||
fec = get_key_by_value(FEC, _fec)
|
||||
sys = get_key_by_value(SYSTEM, sys)
|
||||
tr_flag = 1
|
||||
mod = get_key_by_value(MODULATION, mod)
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
|
||||
nid, tid = int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
if not tr:
|
||||
msg = "ServicesParser error [get transponder services]: {}"
|
||||
er = "Transponder [{}] not found or its type [T2-MI, etc] not supported yet.".format(freq)
|
||||
log(msg.format(er))
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
return self.get_lyngsat_services(sat_position, use_pids)
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
return self.get_kingofsat_services(sat_position, use_pids)
|
||||
else:
|
||||
return []
|
||||
|
||||
# Services
|
||||
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
def get_lyngsat_services(self, sat_position=None, use_pids=False):
|
||||
services = []
|
||||
pos, freq, sr, fec, pol, nsp, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
|
||||
sys = "DVB-S"
|
||||
pos_found = False
|
||||
tr = None
|
||||
# Transponder
|
||||
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
|
||||
if not pos_found:
|
||||
pos_tr = re.match(self._POS_PAT, r[0].text)
|
||||
if not pos_tr:
|
||||
continue
|
||||
|
||||
try:
|
||||
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
|
||||
sid = int(sid)
|
||||
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
|
||||
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
|
||||
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
|
||||
# Flags.
|
||||
flags = "p:{}".format(pkg)
|
||||
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
|
||||
if use_pids:
|
||||
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
|
||||
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
|
||||
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
|
||||
else:
|
||||
flags = ",".join(filter(None, (flags, cas)))
|
||||
if not sat_position:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
except ValueError as e:
|
||||
log("ServicesParser error [get transponder services]: {}".format(e))
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
text = " ".join(c.text for c in r[1:])
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
if td.group(5):
|
||||
log("Detected T2-MI transponder!")
|
||||
continue
|
||||
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
|
||||
nid, tid = td.group(8), td.group(9)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
|
||||
nid, tid = int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
if not tr:
|
||||
er = f"Transponder [{freq}] not found or its type [T2-MI, etc] not supported yet."
|
||||
log(f"ServicesParser error [get transponder services]: {er}")
|
||||
return services
|
||||
|
||||
# Services
|
||||
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
try:
|
||||
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
except ValueError as e:
|
||||
log(f"ServicesParser error [get transponder services]: {e}")
|
||||
|
||||
return services
|
||||
|
||||
def get_kingofsat_services(self, sat_position=None, use_pids=False):
|
||||
services = []
|
||||
# Transponder
|
||||
tr = list(filter(lambda r: len(r) == 13 and r[4].url and r[4].url.startswith("tp.php?tp="), self._rows))
|
||||
if not tr:
|
||||
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
|
||||
return services
|
||||
|
||||
tr = tr[0]
|
||||
s_pos, freq, pol, sys, mod, sr_fec = tr[0].text, tr[2].text, tr[3].text, tr[6].text, tr[7].text, tr[8].text
|
||||
tid, nid = tr[10].text, tr[11].text
|
||||
|
||||
pos = sat_position
|
||||
if not sat_position:
|
||||
pos_tr = re.match(self._POS_PAT, s_pos)
|
||||
if pos_tr:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
|
||||
sr, fec = sr_fec.split()
|
||||
pol = get_key_by_value(POLARIZATION, pol)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
|
||||
freq, nid, tid = int(float(freq)), int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
for r in filter(lambda x: len(x) == 14 and not x[1].text and x[7].text and x[7].text.isdigit(), self._rows):
|
||||
if r[1].img == "/radio.gif":
|
||||
s_type = ""
|
||||
elif r[8].img == "/hd.gif":
|
||||
s_type = "HEVC HD"
|
||||
elif r[1].img == "/data.gif":
|
||||
s_type = "Data"
|
||||
else:
|
||||
s_type = "SD"
|
||||
|
||||
s_type = self._S_TYPES.get(s_type, "3")
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
|
||||
|
||||
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
|
||||
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
|
||||
return services
|
||||
|
||||
def get_transponder_data(self, pos, fec, sys, mod):
|
||||
""" 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)
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = "{:04x}0000".format(3600 - pos if pos < 0 else pos)
|
||||
tr_flag = 1
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
|
||||
inv = 2 # Default
|
||||
|
||||
return sys, mod, fec, namespace, s2_flags, roll_off, pilot, inv
|
||||
|
||||
@staticmethod
|
||||
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
|
||||
sid = int(sid)
|
||||
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
|
||||
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
|
||||
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
|
||||
# Flags.
|
||||
flags = "p:{}".format(pkg)
|
||||
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
|
||||
if use_pids:
|
||||
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
|
||||
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
|
||||
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
|
||||
else:
|
||||
flags = ",".join(filter(None, (flags, cas)))
|
||||
|
||||
return flags, sid, fav_id, picon_id, data_id
|
||||
|
||||
@staticmethod
|
||||
def get_position(pos):
|
||||
return int(SatellitesParser.get_position("".join(c for c in pos if c.isdigit() or c.isalpha())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with YouTube service """
|
||||
import gzip
|
||||
import json
|
||||
@@ -12,6 +40,7 @@ from urllib.parse import unquote
|
||||
from urllib.request import Request, urlopen, urlretrieve
|
||||
|
||||
from app.commons import log
|
||||
from app.settings import SEP
|
||||
from app.ui.uicommons import show_notification
|
||||
|
||||
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
|
||||
@@ -230,7 +259,7 @@ class YouTubeDL:
|
||||
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
|
||||
|
||||
def __init__(self, settings, callback):
|
||||
self._path = settings.default_data_path + "tools/"
|
||||
self._path = "{}tools{}".format(settings.default_data_path, SEP)
|
||||
self._update = settings.enable_yt_dl_update
|
||||
self._supported = {"22", "18"}
|
||||
self._dl = None
|
||||
@@ -247,7 +276,7 @@ class YouTubeDL:
|
||||
return cls._DL_INSTANCE
|
||||
|
||||
def init(self):
|
||||
if not os.path.isfile(self._path + "youtube_dl/version.py"):
|
||||
if not os.path.isfile("{}youtube_dl{}version.py".format(self._path, SEP)):
|
||||
self.get_latest_release()
|
||||
|
||||
if self._path not in sys.path:
|
||||
|
||||
453
app/ui/app_menu.ui
Normal file
453
app/ui/app_menu.ui
Normal file
@@ -0,0 +1,453 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Import</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquet</attribute>
|
||||
<attribute name="action">app.on_import_bouquet</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets and services</attribute>
|
||||
<attribute name="action">app.on_import_bouquets</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import from Web</attribute>
|
||||
<attribute name="action">app.on_import_from_web</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">New empty configuration</attribute>
|
||||
<attribute name="action">app.on_new_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open</attribute>
|
||||
<attribute name="action">app.on_data_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Extract...</attribute>
|
||||
<attribute name="action">app.on_archive_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save</attribute>
|
||||
<attribute name="action">app.on_data_save</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save as</attribute>
|
||||
<attribute name="action">app.on_data_save_as</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<attribute name="action">app.on_download</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.on_settings</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.on_close_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Edit</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu id="view_menu">
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets</attribute>
|
||||
<attribute name="action">app.show_bouquets</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Satellites</attribute>
|
||||
<attribute name="action">app.show_satellites</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Picons</attribute>
|
||||
<attribute name="action">app.show_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG</attribute>
|
||||
<attribute name="action">app.show_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Timers</attribute>
|
||||
<attribute name="action">app.show_timers</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Recordings</attribute>
|
||||
<attribute name="action">app.show_recordings</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP</attribute>
|
||||
<attribute name="action">app.show_ftp</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Control</attribute>
|
||||
<attribute name="action">app.show_control</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Tools</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Backups</attribute>
|
||||
<attribute name="action">app.on_backup_tool_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Telnet</attribute>
|
||||
<attribute name="action">app.on_telnet_show</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP client</attribute>
|
||||
<attribute name="action">app.show_ftp_menu</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_ftp_client_close</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Help</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.on_about_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="mac_app_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.on_about_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.on_settings</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.on_close_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="mac_menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Play</attribute>
|
||||
<attribute name="action">app.on_play</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Stop</attribute>
|
||||
<attribute name="action">app.on_stop</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Import</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquet</attribute>
|
||||
<attribute name="action">app.on_import_bouquet</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets and services</attribute>
|
||||
<attribute name="action">app.on_import_bouquets</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import from Web</attribute>
|
||||
<attribute name="action">app.on_import_from_web</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">New empty configuration</attribute>
|
||||
<attribute name="action">app.on_new_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open</attribute>
|
||||
<attribute name="action">app.on_data_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Extract...</attribute>
|
||||
<attribute name="action">app.on_archive_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save</attribute>
|
||||
<attribute name="action">app.on_data_save</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save as</attribute>
|
||||
<attribute name="action">app.on_data_save_as</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<attribute name="action">app.on_download</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Edit</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets</attribute>
|
||||
<attribute name="action">app.show_bouquets</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Satellites</attribute>
|
||||
<attribute name="action">app.show_satellites</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Picons</attribute>
|
||||
<attribute name="action">app.show_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG</attribute>
|
||||
<attribute name="action">app.show_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Timers</attribute>
|
||||
<attribute name="action">app.show_timers</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Recordings</attribute>
|
||||
<attribute name="action">app.show_recordings</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP</attribute>
|
||||
<attribute name="action">app.show_ftp</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Control</attribute>
|
||||
<attribute name="action">app.show_control</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Tools</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Backups</attribute>
|
||||
<attribute name="action">app.on_backup_tool_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Telnet</attribute>
|
||||
<attribute name="action">app.on_telnet_show</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP client</attribute>
|
||||
<attribute name="action">app.show_ftp_menu</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_ftp_client_close</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="iptv_menu">
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Add IPTV or stream service</attribute>
|
||||
<attribute name="action">app.on_iptv</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
|
||||
<attribute name="action">app.on_import_yt_list</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import m3u</attribute>
|
||||
<attribute name="action">app.on_import_m3u</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_to_m3u</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG configuration</attribute>
|
||||
<attribute name="action">app.on_epg_list_configuration</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">List configuration</attribute>
|
||||
<attribute name="action">app.on_iptv_list_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Remove all unavailable</attribute>
|
||||
<attribute name="action">app.on_remove_all_unavailable</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="audio_menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Audio</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu id="audio_track_menu">
|
||||
<attribute name="label" translatable="yes">Audio Track</attribute>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="video_menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Video</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu id="aspect_ratio_menu">
|
||||
<attribute name="label" translatable="yes">Aspect ratio</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Default</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target"/>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">16:9</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">16:9</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">4:3</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">4:3</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">1:1</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">1:1</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">16:10</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">16:10</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">5:4</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">5:4</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="subtitle_menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Subtitle</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu id="subtitle_track_menu">
|
||||
<attribute name="label" translatable="yes">Subtitle Track</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Default</attribute>
|
||||
<attribute name="action">app.on_set_subtitle_track</attribute>
|
||||
<attribute name="target"/>
|
||||
</item>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
</interface>
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
@@ -7,10 +35,10 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SettingsType
|
||||
from app.ui.dialogs import show_dialog, DialogType
|
||||
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
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
|
||||
|
||||
class RestoreType(Enum):
|
||||
@@ -30,15 +58,12 @@ class BackupDialog:
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_release": self.on_key_release}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain("demon-editor")
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "backup_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "backup_dialog.glade", handlers)
|
||||
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
self._data_path = self._settings.data_local_path
|
||||
self._backup_path = self._settings.backup_local_path or self._data_path + "backup/"
|
||||
self._data_path = self._settings.profile_data_path
|
||||
self._backup_path = self._settings.profile_backup_path or "{}backup{}".format(self._data_path, os.sep)
|
||||
self._open_data_callback = callback
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
@@ -152,7 +177,7 @@ class BackupDialog:
|
||||
clear_data_path(self._data_path)
|
||||
shutil.unpack_archive(full_file_name, self._data_path)
|
||||
elif restore_type is RestoreType.BOUQUETS:
|
||||
tmp_dir = tempfile.gettempdir() + "/" + file_name
|
||||
tmp_dir = tempfile.gettempdir() + SEP + file_name
|
||||
cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml"
|
||||
shutil.unpack_archive(full_file_name, tmp_dir)
|
||||
for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)):
|
||||
@@ -176,7 +201,7 @@ class BackupDialog:
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_remove(view)
|
||||
@@ -191,7 +216,7 @@ def backup_data(path, backup_path, move=True):
|
||||
|
||||
Returns full path to the compressed file.
|
||||
"""
|
||||
backup_path = "{}{}/".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
|
||||
backup_path = "{}{}{}".format(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)
|
||||
|
||||
@@ -33,6 +33,12 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name date -->
|
||||
@@ -41,41 +47,148 @@ Author: Dmitriy Yefremov
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_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>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_all_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-select-all-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_bouquets_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="width_request">560</property>
|
||||
<property name="height_request">320</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Backups</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="icon_name">document-revert</property>
|
||||
<property name="gravity">center</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="header_bar">
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Backups</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_box">
|
||||
<object class="GtkBox" id="header_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_bouquets_header_button">
|
||||
<object class="GtkButtonBox" id="main_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Restore bouquets</property>
|
||||
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">15</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="restore_bouquets_header_button_image">
|
||||
<object class="GtkButton" id="restore_bouquets_header_button">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">restore_bouquets_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_all_header_button">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">restore_all_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_restore_all" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_header_button">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">remove_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="clicked"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -85,93 +198,43 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_all_header_button">
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="label" translatable="yes">Details</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Restore all</property>
|
||||
<signal name="clicked" handler="on_restore_all" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="restore_all_header_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-select-all</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
</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="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_header_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Remove</property>
|
||||
<signal name="clicked" handler="on_remove" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="info_check_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-dialog-info</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
@@ -248,7 +311,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -276,6 +339,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -301,57 +365,4 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_popup_menu_item_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-revert-to-saved</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_popup_menu_item_image2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-select-all</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">restore_popup_menu_item_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">restore_popup_menu_item_image2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_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>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
3998
app/ui/control.glade
3998
app/ui/control.glade
File diff suppressed because it is too large
Load Diff
1397
app/ui/control.py
1397
app/ui/control.py
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ Author: Dmitriy Yefremov
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
@@ -40,10 +40,10 @@ 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">1.0.6 Beta</property>
|
||||
<property name="version">2.0.0 Alpha</property>
|
||||
<property name="copyright">2018-2021 Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for GNU/Linux.</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
|
||||
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
|
||||
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
|
||||
Подробнее в <a href="http://opensource.org/licenses/mit-license.php">The MIT License (MIT)</a>.</property>
|
||||
@@ -65,7 +65,9 @@ Author: Dmitriy Yefremov
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="aboutdialog_action_area">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<property name="margin_left">50</property>
|
||||
<property name="margin_right">50</property>
|
||||
<property name="layout_style">expand</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -86,36 +88,61 @@ Author: Dmitriy Yefremov
|
||||
<property name="window_position">center</property>
|
||||
<property name="default_width">320</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="type_hint">utility</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="input_dialog_cancel_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="input_dialog_ok_button">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">4</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_dialog_cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_dialog_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="input_entry">
|
||||
<property name="visible">True</property>
|
||||
@@ -124,7 +151,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin_right">2</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
@@ -132,7 +159,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -142,7 +169,7 @@ Author: Dmitriy Yefremov
|
||||
<action-widget response="ok">input_dialog_ok_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<object class="GtkDialog" id="wait_dialog">
|
||||
<object class="GtkWindow" id="wait_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
@@ -153,68 +180,43 @@ Author: Dmitriy Yefremov
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="decorated">False</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="wait_dialog_vbox">
|
||||
<property name="width_request">120</property>
|
||||
<object class="GtkBox" id="wait_dialog_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog-action_area4">
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="width_request">150</property>
|
||||
<property name="height_request">45</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="opacity">0</property>
|
||||
<property name="layout_style">end</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="box4">
|
||||
<object class="GtkLabel" id="wait_dialog_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="width_request">150</property>
|
||||
<property name="height_request">45</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">True</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="wait_dialog_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="label" translatable="yes">Loading data...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="label" translatable="yes">Loading data...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</child> <!-- NOP -->
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Common module for showing dialogs """
|
||||
import locale
|
||||
import gettext
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -17,12 +47,11 @@ class Dialog(Enum):
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="default_width">320</property>
|
||||
<property name="width_request">250</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<property name="message_type">{message_type}</property>
|
||||
<property name="buttons">{buttons_type}</property>
|
||||
</object>
|
||||
@@ -89,11 +118,12 @@ def show_dialog(dialog_type, transient, text=None, settings=None, action_type=No
|
||||
return get_about_dialog(transient)
|
||||
|
||||
|
||||
def get_chooser_dialog(transient, settings, name, patterns, title=None):
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name(name)
|
||||
for p in patterns:
|
||||
file_filter.add_pattern(p)
|
||||
def get_chooser_dialog(transient, settings, name, patterns, title=None, file_filter=None):
|
||||
if not file_filter:
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name(name)
|
||||
for p in patterns:
|
||||
file_filter.add_pattern(p)
|
||||
|
||||
return show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
transient=transient,
|
||||
@@ -104,23 +134,21 @@ def get_chooser_dialog(transient, settings, name, patterns, title=None):
|
||||
|
||||
|
||||
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
|
||||
text = get_message(text) if text else ""
|
||||
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
|
||||
buttons = buttons or (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
dialog = Gtk.FileChooserDialog(text, transient, action_type, buttons, use_header_bar=IS_GNOME_SESSION)
|
||||
dialog.set_title(get_message(title) if title else "")
|
||||
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
|
||||
dialog.set_create_folders(dirs)
|
||||
dialog.set_modal(True)
|
||||
|
||||
if file_filter is not None:
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
dialog.set_current_folder(settings.data_local_path)
|
||||
dialog.set_current_folder(settings.profile_data_path)
|
||||
response = dialog.run()
|
||||
|
||||
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
path = Path(dialog.get_filename() or dialog.get_current_folder())
|
||||
if path.is_dir():
|
||||
response = "{}/".format(path.resolve())
|
||||
response = "{}{}".format(path.resolve(), SEP)
|
||||
elif path.is_file():
|
||||
response = str(path.resolve())
|
||||
dialog.destroy()
|
||||
@@ -176,13 +204,52 @@ def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
|
||||
|
||||
def get_message(message):
|
||||
""" returns translated message """
|
||||
return locale.dgettext(TEXT_DOMAIN, message)
|
||||
return gettext.dgettext(TEXT_DOMAIN, message)
|
||||
|
||||
|
||||
@lru_cache(maxsize=5)
|
||||
def get_dialogs_string(path):
|
||||
with open(path, "r") as f:
|
||||
return "".join(f)
|
||||
def get_dialogs_string(path, tag="property"):
|
||||
if IS_WIN:
|
||||
return translate_xml(path, tag)
|
||||
else:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return "".join(f)
|
||||
|
||||
|
||||
def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"):
|
||||
""" Creates and returns a Gtk.Builder instance. """
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
|
||||
if use_str:
|
||||
if objects:
|
||||
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION), objects)
|
||||
else:
|
||||
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION))
|
||||
else:
|
||||
if objects:
|
||||
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)
|
||||
else:
|
||||
builder.add_from_string(get_dialogs_string(path, tag))
|
||||
|
||||
builder.connect_signals(handlers or {})
|
||||
|
||||
return builder
|
||||
|
||||
|
||||
def translate_xml(path, tag="property"):
|
||||
"""
|
||||
Used to translate GUI from * .glade files in MS Windows.
|
||||
|
||||
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
|
||||
"""
|
||||
et = ET.parse(path)
|
||||
root = et.getroot()
|
||||
for e in root.iter(tag):
|
||||
if e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
|
||||
return ET.tostring(root, encoding="unicode", method="xml")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -30,119 +30,94 @@ Author: Dmitriy Yefremov
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="download_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="send_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-transmit-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="download_dialog_window">
|
||||
<property name="width_request">550</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">FTP-transfer</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="icon_name">mail-send-receive</property>
|
||||
<property name="icon_name">mail-send-receive-symbolic</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="header_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">FTP-transfer</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_left_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Receive</property>
|
||||
<signal name="clicked" handler="on_receive" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="receive_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-goto-bottom</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Send</property>
|
||||
<signal name="clicked" handler="on_send" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="send_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-goto-top</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="options_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Options</property>
|
||||
<signal name="clicked" handler="on_settings" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-properties</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_dialog_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="selection_data_box">
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label10">
|
||||
<object class="GtkBox" id="profile_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Profile:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="profile_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has_frame">False</property>
|
||||
<signal name="changed" handler="on_profile_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="options_button">
|
||||
<property name="label" translatable="yes">Options</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Options</property>
|
||||
<signal name="clicked" handler="on_settings" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -151,13 +126,84 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="all_radio_button">
|
||||
<property name="label" translatable="yes">All</property>
|
||||
<object class="GtkBox" id="selection_data_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label10">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="all_radio_button">
|
||||
<property name="label" translatable="yes">All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="bouquets_radio_button">
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="satellites_radio_button">
|
||||
<property name="label" translatable="yes">Satellites</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="webtv_radio_button">
|
||||
<property name="label" translatable="yes">WebTV</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -165,50 +211,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="bouquets_radio_button">
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="satellites_radio_button">
|
||||
<property name="label" translatable="yes">Satellites</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="webtv_radio_button">
|
||||
<property name="label" translatable="yes">WebTV</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -216,65 +221,23 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="profile_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Profile:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="profile_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has_frame">False</property>
|
||||
<signal name="changed" handler="on_profile_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="main_settings_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="label_xalign">0.019999999552965164</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_settings_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="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="main_settings_bo">
|
||||
@@ -424,6 +387,60 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">20</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<property name="label" translatable="yes">Receive</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Receive</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">download_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_receive" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="label" translatable="yes">Send</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Send</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">send_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_send" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="expander">
|
||||
<property name="visible">True</property>
|
||||
@@ -460,7 +477,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -516,7 +533,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GLib
|
||||
@@ -8,7 +36,7 @@ from app.settings import SettingsType
|
||||
from app.ui.backup import backup_data, restore_data
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.settings_dialog import show_settings_dialog
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .dialogs import show_dialog, DialogType, get_message, get_builder
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
@@ -27,9 +55,7 @@ class DownloadDialog:
|
||||
"on_remove_unused_bouquets_toggled": self.on_remove_unused_bouquets_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
|
||||
|
||||
self._dialog_window = builder.get_object("download_dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
@@ -46,7 +72,6 @@ class DownloadDialog:
|
||||
self._webtv_radio_button = builder.get_object("webtv_radio_button")
|
||||
self._use_http_switch = builder.get_object("use_http_switch")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
self._use_http_box = builder.get_object("use_http_box")
|
||||
self._profile_combo_box = builder.get_object("profile_combo_box")
|
||||
|
||||
self.init_settings()
|
||||
@@ -60,11 +85,10 @@ class DownloadDialog:
|
||||
|
||||
def init_ui_settings(self):
|
||||
self._host_entry.set_text(self._settings.host)
|
||||
self._data_path_entry.set_text(self._settings.data_local_path)
|
||||
self._data_path_entry.set_text(self._settings.profile_data_path)
|
||||
is_enigma = self._s_type is SettingsType.ENIGMA_2
|
||||
self._webtv_radio_button.set_visible(not is_enigma)
|
||||
self._use_http_box.set_visible(is_enigma)
|
||||
self._use_http_switch.set_active(is_enigma and self._settings.use_http)
|
||||
self._use_http_switch.set_active(self._settings.use_http)
|
||||
self._remove_unused_check_button.set_active(self._settings.remove_unused_bouquets)
|
||||
|
||||
def update_profiles(self):
|
||||
@@ -130,9 +154,9 @@ class DownloadDialog:
|
||||
try:
|
||||
if download:
|
||||
if backup and d_type is not DownloadType.SATELLITES:
|
||||
data_path = self._settings.data_local_path or self._data_path_entry.get_text()
|
||||
data_path = self._settings.profile_data_path or self._data_path_entry.get_text()
|
||||
os.makedirs(os.path.dirname(data_path), exist_ok=True)
|
||||
backup_path = self._settings.backup_local_path or data_path + "backup/"
|
||||
backup_path = self._settings.profile_backup_path or self._settings.default_backup_path
|
||||
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
|
||||
|
||||
download_data(settings=self._settings, download_type=d_type, callback=self.append_output)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import gzip
|
||||
import locale
|
||||
import os
|
||||
@@ -8,13 +36,15 @@ from enum import Enum
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.connections import download_data, DownloadType
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.settings import SEP
|
||||
from app.tools.epg import EPG, ChannelsParser
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
|
||||
from .main_helper import on_popup_menu, update_entry_data
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey
|
||||
|
||||
|
||||
class RefsSource(Enum):
|
||||
@@ -66,10 +96,7 @@ class EpgDialog:
|
||||
self._show_tooltips = True
|
||||
self._download_xml_is_active = False
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "epg_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "epg.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("epg_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -84,6 +111,7 @@ class EpgDialog:
|
||||
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
self._filter_entry = builder.get_object("filter_entry")
|
||||
self._services_filter_model = builder.get_object("services_filter_model")
|
||||
self._services_filter_model.set_visible_func(self.services_filter_function)
|
||||
@@ -482,7 +510,7 @@ class EpgDialog:
|
||||
# ***************** Options *********************#
|
||||
|
||||
def init_options(self):
|
||||
epg_dat_path = self._settings.data_local_path + "epg/"
|
||||
epg_dat_path = "{}epg{}".format(self._settings.profile_data_path, SEP)
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
default_epg_data_stb_path = "/etc/enigma2"
|
||||
epg_options = self._settings.epg_options
|
||||
721
app/ui/ftp.glade
721
app/ui/ftp.glade
@@ -87,34 +87,100 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkFrame" id="main_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="paned">
|
||||
<property name="width_request">320</property>
|
||||
<property name="height_request">240</property>
|
||||
<object class="GtkBox" id="main_ftp_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_bpx">
|
||||
<object class="GtkBox" id="ftp_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="bookmark_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">bookmarks_list_store</property>
|
||||
<property name="id_column">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="paned">
|
||||
<property name="width_request">320</property>
|
||||
<property name="height_request">240</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_bpx">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_info_box">
|
||||
<property name="visible">True</property>
|
||||
@@ -124,9 +190,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkLabel" id="ftp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="label">FTP:</property>
|
||||
<property name="yalign">1</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
@@ -142,7 +206,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max_width_chars">25</property>
|
||||
<property name="max_width_chars">75</property>
|
||||
<property name="yalign">1</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -159,24 +223,158 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_button_box">
|
||||
<object class="GtkScrolledWindow" id="ftp_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<object class="GtkTreeView" id="ftp_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">ftp_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="ftp_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_ftp_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_ftp_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_ftp_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="ftp_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_ftp_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_attr_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">85</property>
|
||||
<property name="title" translatable="yes">Attr.</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_attr_column_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="pc_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="label" translatable="yes">PC:</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -185,18 +383,11 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<object class="GtkLabel" id="pc_info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max_width_chars">75</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -204,189 +395,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="bookmark_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">bookmarks_list_store</property>
|
||||
<property name="id_column">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="ftp_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="ftp_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">ftp_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="ftp_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_ftp_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_ftp_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_ftp_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="ftp_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_ftp_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_attr_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Attr.</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_attr_column_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="pc_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="label" translatable="yes">PC:</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -395,156 +403,147 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_info_label">
|
||||
<object class="GtkScrolledWindow" id="file_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max_width_chars">32</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="file_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">file_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="file_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_file_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_file_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_file_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="file_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="file_icon_column_renderer">
|
||||
<property name="xalign">0.20000000298023224</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_name_column_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_file_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_type_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Path</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_path_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="file_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="file_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">file_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="file_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_file_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_file_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_file_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="file_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="file_icon_column_renderer">
|
||||
<property name="xalign">0.20000000298023224</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_name_column_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_file_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_type_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Path</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_path_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="main_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">FTP client</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
@@ -584,7 +583,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">rename_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_edit" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="F2" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Simple FTP client module. """
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
@@ -12,7 +40,7 @@ from gi.repository import GLib
|
||||
|
||||
from app.commons import log, run_task, run_idle
|
||||
from app.connections import UtfFTP
|
||||
from app.ui.dialogs import show_dialog, DialogType
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
|
||||
@@ -68,9 +96,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
"on_view_press": self.on_view_press,
|
||||
"on_view_release": self.on_view_release}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "ftp.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
|
||||
|
||||
self.add(builder.get_object("main_frame"))
|
||||
self._ftp_info_label = builder.get_object("ftp_info_label")
|
||||
@@ -142,7 +168,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
|
||||
@run_task
|
||||
def init_file_data(self, path=None):
|
||||
self.append_file_data(Path(path if path else self._settings.data_local_path))
|
||||
self.append_file_data(Path(path if path else self._settings.profile_data_path))
|
||||
|
||||
@run_idle
|
||||
def append_file_data(self, path: Path):
|
||||
@@ -215,7 +241,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
else:
|
||||
b_size = row[self.Column.EXTRA]
|
||||
if b_size.isdigit() and int(b_size) > self.MAX_SIZE:
|
||||
self._app.show_error_dialog("The file size is too large!")
|
||||
self._app.show_error_message("The file size is too large!")
|
||||
else:
|
||||
self.open_ftp_file(f_path)
|
||||
|
||||
@@ -268,7 +294,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
return
|
||||
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_dialog("Please, select only one item!")
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
renderer.set_property("editable", True)
|
||||
@@ -289,7 +315,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
def on_file_edit(self, renderer):
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_dialog("Please, select only one item!")
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
renderer.set_property("editable", True)
|
||||
@@ -308,7 +334,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
new_path = path.rename("{}/{}".format(path.parent, new_value))
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
self._app.show_error_dialog(str(e))
|
||||
self._app.show_error_message(str(e))
|
||||
else:
|
||||
if new_path.name == new_value:
|
||||
row[self.Column.NAME] = new_value
|
||||
@@ -365,7 +391,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
path.mkdir()
|
||||
except OSError as e:
|
||||
log(e)
|
||||
self._app.show_error_dialog(str(e))
|
||||
self._app.show_error_message(str(e))
|
||||
else:
|
||||
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
|
||||
renderer.set_property("editable", True)
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-undo</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="select_all_popup_item">
|
||||
<property name="label">gtk-select-all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
|
||||
<property name="label" translatable="yes">Remove selection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_selection_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="services_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">480</property>
|
||||
<property name="default_height">320</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="gravity">center</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="header_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Import</property>
|
||||
<property name="subtitle" translatable="yes">Bouquets and services</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="import_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Import</property>
|
||||
<signal name="clicked" handler="on_import" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="import_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-revert-to-saved</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="info_check_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-dialog-info</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="width_request">480</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="bouquets_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
|
||||
<property name="width_request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_name_column">
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_name_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="services_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="label" translatable="yes">Bouquet details</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
|
||||
<property name="width_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="services_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">services_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_name_column">
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_name_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
<property name="wrap">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
441
app/ui/imports.glade
Normal file
441
app/ui/imports.glade
Normal file
@@ -0,0 +1,441 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="import_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic-rtl</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-undo</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="select_all_popup_item">
|
||||
<property name="label">gtk-select-all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
|
||||
<property name="label" translatable="yes">Remove selection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_selection_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="services_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Import</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">480</property>
|
||||
<property name="default_height">320</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="width_request">480</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="toolbar_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">15</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="layout_style">start</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="import_button">
|
||||
<property name="label" translatable="yes">Import</property>
|
||||
<property name="visible">True</property>
|
||||
<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"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="label" translatable="yes">Details</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="secondary">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="bouquets_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="bouquets_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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
|
||||
<property name="width_request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_name_column">
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_name_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="services_box_frame">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="services_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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
|
||||
<property name="width_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="services_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">services_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_name_column">
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_name_renderer">
|
||||
<property name="xalign">0.02</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquet details</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
<property name="wrap">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -6,7 +6,7 @@ from app.eparser import get_bouquets, get_services, BouquetsReader
|
||||
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
|
||||
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
|
||||
from app.settings import SettingsType
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
|
||||
|
||||
@@ -71,7 +71,6 @@ class ImportDialog:
|
||||
def __init__(self, transient, path, settings, service_ids, appender, bouquets=None):
|
||||
handlers = {"on_import": self.on_import,
|
||||
"on_cursor_changed": self.on_cursor_changed,
|
||||
"on_info_button_toggled": self.on_info_button_toggled,
|
||||
"on_selected_toggled": self.on_selected_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_select_all": self.on_select_all,
|
||||
@@ -80,10 +79,7 @@ class ImportDialog:
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_press": self.on_key_press}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain("demon-editor")
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "import_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "imports.glade", handlers)
|
||||
|
||||
self._bq_services = {}
|
||||
self._services = {}
|
||||
@@ -99,8 +95,8 @@ class ImportDialog:
|
||||
self._main_view = builder.get_object("main_view")
|
||||
self._services_view = builder.get_object("services_view")
|
||||
self._services_model = builder.get_object("services_list_store")
|
||||
self._services_box = builder.get_object("services_box")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._info_check_button.bind_property("active", builder.get_object("services_box_frame"), "visible")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
window_size = self._settings.get("import_dialog_window_size")
|
||||
@@ -124,8 +120,15 @@ class ImportDialog:
|
||||
for bq in bqs.bouquets:
|
||||
self._main_model.append((bq.name, bq.type, True))
|
||||
self._bq_services[(bq.name, bq.type)] = bq.services
|
||||
# Note! Getting default format ver. 4
|
||||
services = get_services(path, self._profile, 4 if self._profile is SettingsType.ENIGMA_2 else 0)
|
||||
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
|
||||
elif self._profile is SettingsType.NEUTRINO_MP:
|
||||
services = get_services(path, self._profile, 0)
|
||||
else:
|
||||
self.show_info_message("Setting format not supported!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
for srv in services:
|
||||
self._services[srv.fav_id] = srv
|
||||
except FileNotFoundError as e:
|
||||
@@ -192,10 +195,6 @@ class ImportDialog:
|
||||
else:
|
||||
self._services_model.append((bq_srv.name, bq_srv.type.value))
|
||||
|
||||
def on_info_button_toggled(self, button):
|
||||
active = button.get_active()
|
||||
self._services_box.set_visible(active)
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
|
||||
1377
app/ui/iptv.glade
1377
app/ui/iptv.glade
File diff suppressed because it is too large
Load Diff
160
app/ui/iptv.py
160
app/ui/iptv.py
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import concurrent.futures
|
||||
import os
|
||||
import re
|
||||
@@ -6,6 +34,7 @@ from urllib.error import HTTPError
|
||||
from urllib.parse import urlparse, unquote, quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import requests
|
||||
from gi.repository import GLib, Gio, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task, log
|
||||
@@ -14,10 +43,9 @@ from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID
|
||||
parse_m3u)
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
|
||||
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION,
|
||||
KeyboardKey, get_yt_icon)
|
||||
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
@@ -66,11 +94,8 @@ class IptvDialog:
|
||||
self._yt_links = None
|
||||
self._yt_dl = None
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
|
||||
|
||||
self._dialog = builder.get_object("iptv_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -322,10 +347,8 @@ class SearchUnavailableDialog:
|
||||
def __init__(self, transient, model, fav_bouquet, iptv_rows, s_type):
|
||||
handlers = {"on_response": self.on_response}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade", ("search_unavailable_streams_dialog",))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "iptv.glade", handlers,
|
||||
objects=("search_unavailable_streams_dialog",))
|
||||
|
||||
self._dialog = builder.get_object("search_unavailable_streams_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -420,11 +443,8 @@ class IptvListDialog:
|
||||
|
||||
self._s_type = s_type
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("iptv_list_configuration_dialog", "stream_type_liststore"))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("iptv_list_configuration_dialog", "stream_type_liststore"))
|
||||
|
||||
self._dialog = builder.get_object("iptv_list_configuration_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -445,6 +465,8 @@ class IptvListDialog:
|
||||
self._list_nid_entry = builder.get_object("list_nid_entry")
|
||||
self._list_namespace_entry = builder.get_object("list_namespace_entry")
|
||||
self._apply_button = builder.get_object("list_configuration_apply_button")
|
||||
self._cancel_button = builder.get_object("cancel_config_list_button")
|
||||
self._ok_button = builder.get_object("list_configuration_ok_button")
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
@@ -596,7 +618,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
|
||||
self._app = app
|
||||
self._picons = app._picons
|
||||
self._pic_path = app._settings.picons_local_path
|
||||
self._pic_path = app._settings.profile_picons_path
|
||||
self._services = None
|
||||
self._url_count = 0
|
||||
self._errors_count = 0
|
||||
@@ -606,6 +628,8 @@ class M3uImportDialog(IptvListDialog):
|
||||
self._dialog.set_title(get_message("Playlist import"))
|
||||
self._dialog.connect("delete-event", self.on_close)
|
||||
self._apply_button.set_label(get_message("Import"))
|
||||
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
|
||||
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
|
||||
# Progress
|
||||
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
|
||||
self._spinner = Gtk.Spinner(active=False)
|
||||
@@ -687,6 +711,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
|
||||
self.download_picons(picons)
|
||||
else:
|
||||
GLib.idle_add(self._ok_button.set_visible, True)
|
||||
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
self._app.append_imported_services(services)
|
||||
@@ -703,50 +728,63 @@ class M3uImportDialog(IptvListDialog):
|
||||
self._max_count = self._url_count
|
||||
self._cancellable.reset()
|
||||
|
||||
for p in filter(None, picons):
|
||||
if not self._is_download:
|
||||
return
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {executor.submit(self.download_picon, p, picons.get(p, None)): p for p in filter(None, picons)}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_download and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
|
||||
f = Gio.File.new_for_uri(p)
|
||||
try:
|
||||
GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(f.read(cancellable=self._cancellable), 220, 132, False,
|
||||
self._cancellable,
|
||||
self.on_picon_load_done,
|
||||
picons.get(p, None))
|
||||
except GLib.GError as e:
|
||||
self.update_progress()
|
||||
self._errors_count += 1
|
||||
if e.code != Gio.IOErrorEnum.CANCELLED:
|
||||
log(str("Picon download error: {} [{}]").format(p, e))
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
|
||||
def on_picon_load_done(self, file, result, user_data):
|
||||
self.update_progress(self._url_count)
|
||||
self.on_done()
|
||||
|
||||
def download_picon(self, url, pic_data):
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
|
||||
req = requests.get(url, timeout=timeout)
|
||||
if req.status_code != 200:
|
||||
log(err_msg.format(url, req.reason))
|
||||
self.update_progress(1)
|
||||
else:
|
||||
self.on_picon_load_done(req.content, pic_data)
|
||||
|
||||
@run_idle
|
||||
def on_picon_load_done(self, data, user_data):
|
||||
try:
|
||||
self._info_label.set_text("Processing: {}".format(user_data))
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result)
|
||||
f = Gio.MemoryInputStream.new_from_data(data)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 220, 132, False, self._cancellable)
|
||||
path = "{}{}".format(self._pic_path, user_data)
|
||||
pixbuf.savev(path, "png", [], [])
|
||||
self._picons[user_data] = get_picon_pixbuf(path)
|
||||
except GLib.GError as e:
|
||||
self._errors_count += 1
|
||||
self.update_progress(1)
|
||||
if e.code != Gio.IOErrorEnum.CANCELLED:
|
||||
log("Loading picon [{}] data error: {}".format(user_data, e))
|
||||
finally:
|
||||
else:
|
||||
self.update_progress()
|
||||
|
||||
def update_progress(self):
|
||||
@run_idle
|
||||
def update_progress(self, error=0):
|
||||
self._errors_count += error
|
||||
self._url_count -= 1
|
||||
frac = 1 - self._url_count / self._max_count
|
||||
self._progress_bar.set_fraction(frac)
|
||||
|
||||
if self._url_count == 0:
|
||||
self._progress_bar.set_visible(False)
|
||||
self._progress_bar.set_fraction(0.0)
|
||||
self._apply_button.set_sensitive(True)
|
||||
self._info_label.set_text("{} {}.".format(get_message("Errors:"), self._errors_count))
|
||||
self._is_download = False
|
||||
@run_idle
|
||||
def on_done(self):
|
||||
self._progress_bar.set_visible(False)
|
||||
self._progress_bar.set_fraction(0.0)
|
||||
self._apply_button.set_sensitive(True)
|
||||
self._info_label.set_text("Errors: {}.".format(self._errors_count))
|
||||
self._is_download = False
|
||||
|
||||
gen = self.update_fav_model()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
gen = self.update_fav_model()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update_fav_model(self):
|
||||
services = self._app._services
|
||||
@@ -757,7 +795,9 @@ class M3uImportDialog(IptvListDialog):
|
||||
if s:
|
||||
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
|
||||
yield True
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self._ok_button.set_visible(True)
|
||||
yield True
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
@@ -799,12 +839,10 @@ class YtListImportDialog:
|
||||
self._settings = settings
|
||||
self._yt = None
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
|
||||
"yt_popup_menu", "remove_selection_image"))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
|
||||
"yt_popup_menu", "remove_selection_image", "yt_receive_image",
|
||||
"yt_import_image"))
|
||||
|
||||
self._dialog = builder.get_object("yt_import_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -823,10 +861,14 @@ class YtListImportDialog:
|
||||
self._import_button.bind_property("visible", self._quality_box, "visible")
|
||||
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
|
||||
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
|
||||
window_size = self._settings.get("yt_import_dialog_size")
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(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):
|
||||
@@ -871,7 +913,6 @@ class YtListImportDialog:
|
||||
self.update_active_elements(True)
|
||||
|
||||
def on_receive(self, item):
|
||||
self.show_invisible_elements()
|
||||
self.update_active_elements(False)
|
||||
self._model.clear()
|
||||
self._yt_count_label.set_text("0")
|
||||
@@ -930,11 +971,6 @@ class YtListImportDialog:
|
||||
self._url_entry.set_sensitive(sensitive)
|
||||
self._receive_button.set_sensitive(sensitive)
|
||||
|
||||
def show_invisible_elements(self):
|
||||
self._list_view_scrolled_window.set_visible(True)
|
||||
self._info_bar_box.set_visible(True)
|
||||
self._dialog.set_resizable(True)
|
||||
|
||||
def on_url_entry_changed(self, entry):
|
||||
url_str = entry.get_text()
|
||||
yt_id = YouTube.get_yt_list_id(url_str)
|
||||
@@ -990,7 +1026,9 @@ class YtListImportDialog:
|
||||
def on_close(self, window, event):
|
||||
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return True
|
||||
|
||||
self._download_task = False
|
||||
self._settings.add("yt_import_dialog_size", self._dialog.get_size())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
app/ui/lang/it/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/it/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/ui/lang/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
38
app/ui/mac_style.css
Normal file
38
app/ui/mac_style.css
Normal file
@@ -0,0 +1,38 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 5em;
|
||||
}
|
||||
|
||||
entry {
|
||||
min-height: 2.0em;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 1.5em;
|
||||
min-width: 1em;
|
||||
padding-left: 0.4em;
|
||||
padding-right: 0.4em;
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
spinbutton {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
toolbutton {
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
spinner {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
infobar {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
switch slider {
|
||||
min-height: 1.5em;
|
||||
min-width: 1.5em;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Helper module for the ui. """
|
||||
import os
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from urllib.parse import unquote
|
||||
|
||||
from gi.repository import GdkPixbuf, GLib
|
||||
|
||||
from app.commons import run_task
|
||||
from app.eparser import Service
|
||||
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
|
||||
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
|
||||
from app.settings import SettingsType
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
|
||||
from app.settings import SettingsType, SEP, IS_WIN
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog
|
||||
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
|
||||
|
||||
|
||||
@@ -384,6 +412,10 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
|
||||
if src_path == Gtk.ResponseType.CANCEL:
|
||||
return picons_files
|
||||
|
||||
if IS_WIN:
|
||||
src_path = src_path.lstrip("/")
|
||||
dst_path = dst_path.lstrip("/") if dst_path else dst_path
|
||||
|
||||
if not str(src_path).endswith(".png") or not os.path.isfile(src_path):
|
||||
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
|
||||
return picons_files
|
||||
@@ -402,7 +434,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
|
||||
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
|
||||
|
||||
if picon_id:
|
||||
picons_path = dst_path or settings.picons_local_path
|
||||
picons_path = dst_path or settings.profile_picons_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
picon_file = picons_path + picon_id
|
||||
try:
|
||||
@@ -499,8 +531,8 @@ def remove_all_unused_picons(settings, picons, services):
|
||||
|
||||
|
||||
def remove_picons(settings, picon_ids, picons):
|
||||
pions_path = settings.picons_local_path
|
||||
backup_path = settings.backup_local_path + "picons/"
|
||||
pions_path = settings.profile_picons_path
|
||||
backup_path = "{}{}{}".format(settings.profile_backup_path, "picons", SEP)
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
for p_id in picon_ids:
|
||||
picons[p_id] = None
|
||||
@@ -528,10 +560,16 @@ def get_picon_pixbuf(path, size=32):
|
||||
pass
|
||||
|
||||
|
||||
# ***************** Bouquets *********************#
|
||||
# ***************** Bouquets ********************* #
|
||||
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
|
||||
""" 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
|
||||
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
|
||||
""" Auto-generate and append list of bouquets """
|
||||
fav_id_index = Column.SRV_FAV_ID
|
||||
index = Column.SRV_TYPE
|
||||
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
|
||||
@@ -539,52 +577,41 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback)
|
||||
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
|
||||
index = Column.SRV_POS
|
||||
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
# 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))
|
||||
|
||||
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
|
||||
if gen_type in (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE):
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
service = Service(*model[paths][:Column.SRV_TOOLTIP])
|
||||
if service.service_type not in tv_types:
|
||||
bq_type = BqType.RADIO.value
|
||||
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
|
||||
[service.package if gen_type is BqGenType.PACKAGE else
|
||||
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
|
||||
else:
|
||||
wait_dialog = WaitDialog(transient)
|
||||
wait_dialog.show()
|
||||
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
|
||||
{row[index] for row in model}, s_type, wait_dialog)
|
||||
|
||||
|
||||
@run_task
|
||||
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, s_type, wait_dialog=None):
|
||||
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)
|
||||
bqs_model = bq_view.get_model()
|
||||
bouquets_names = get_bouquets_names(bqs_model)
|
||||
|
||||
for pos, name in enumerate(sorted(names)):
|
||||
if name not in bouquets_names:
|
||||
services = [BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0)
|
||||
for row in model if row[index] == name]
|
||||
callback(Bouquet(name=name, type=bq_type, services=services, locked=None, hidden=None),
|
||||
bqs_model.get_iter(bq_index))
|
||||
bq_names = get_bouquets_names(bq_view.get_model())
|
||||
|
||||
if wait_dialog is not None:
|
||||
wait_dialog.destroy()
|
||||
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)
|
||||
else:
|
||||
# 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)
|
||||
|
||||
|
||||
def get_bouquets_names(model):
|
||||
""" Returns all current bouquets names """
|
||||
bouquets_names = []
|
||||
bouquets_names = set()
|
||||
for row in model:
|
||||
itr = row.iter
|
||||
if model.iter_has_child(itr):
|
||||
num_of_children = model.iter_n_children(itr)
|
||||
for num in range(num_of_children):
|
||||
child_itr = model.iter_nth_child(itr, num)
|
||||
bouquets_names.append(model[child_itr][0])
|
||||
bouquets_names.add(model[child_itr][0])
|
||||
return bouquets_names
|
||||
|
||||
|
||||
@@ -625,7 +652,7 @@ def get_base_paths(paths, model):
|
||||
def get_model_data(view):
|
||||
""" Returns model name and base model from the given view """
|
||||
model = get_base_model(view.get_model())
|
||||
model_name = model.get_name()
|
||||
model_name = model.get_name() if model else ""
|
||||
return model_name, model
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
@@ -9,21 +37,30 @@ from gi.repository import GLib, GdkPixbuf, Gio
|
||||
|
||||
from app.commons import run_idle, run_task, run_with_delay
|
||||
from app.connections import upload_data, DownloadType, download_data, remove_picons
|
||||
from app.settings import SettingsType, Settings
|
||||
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to, download_picon
|
||||
from app.settings import SettingsType, Settings, SEP
|
||||
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
|
||||
PiconsError)
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .dialogs import show_dialog, DialogType, get_message, get_builder
|
||||
from .main_helper import (update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon,
|
||||
get_picon_pixbuf)
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page
|
||||
|
||||
|
||||
class PiconsDialog:
|
||||
def __init__(self, transient, settings, picon_ids, sat_positions, app):
|
||||
class PiconManager(Gtk.Box):
|
||||
class DownloadSource(Enum):
|
||||
LYNG_SAT = "lyngsat"
|
||||
PICON_CZ = "piconcz"
|
||||
|
||||
def __init__(self, app, settings, picon_ids, sat_positions, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("page-changed", self.update_picons_dest)
|
||||
self._app.connect("filter-toggled", self.on_app_filter_toggled)
|
||||
self._app.fav_view.connect("row-activated", self.on_fav_changed)
|
||||
self._picon_ids = picon_ids
|
||||
self._sat_positions = sat_positions
|
||||
self._app = app
|
||||
self._TMP_DIR = tempfile.gettempdir() + "/"
|
||||
self._BASE_URL = "www.lyngsat.com/packages/"
|
||||
self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
|
||||
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
|
||||
@@ -33,28 +70,29 @@ class PiconsDialog:
|
||||
self._filter_binding = None
|
||||
self._services = None
|
||||
self._current_picon_info = None
|
||||
self._filter_cache = {}
|
||||
# Downloader
|
||||
self._sats = None
|
||||
self._sat_names = None
|
||||
self._download_src = self.DownloadSource.PICON_CZ
|
||||
self._picon_cz_downloader = None
|
||||
|
||||
handlers = {"on_receive": self.on_receive,
|
||||
"on_load_providers": self.on_load_providers,
|
||||
handlers = {"on_tool_switched": self.on_tool_switched,
|
||||
"on_receive": self.on_receive,
|
||||
"on_cancel": self.on_cancel,
|
||||
"on_close": self.on_close,
|
||||
"on_send": self.on_send,
|
||||
"on_download": self.on_download,
|
||||
"on_remove": self.on_remove,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_picons_dir_open": self.on_picons_dir_open,
|
||||
"on_selected_toggled": self.on_selected_toggled,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
"on_picons_filter_changed": self.on_picons_filter_changed,
|
||||
"on_position_edited": self.on_position_edited,
|
||||
"on_visible_page": self.on_visible_page,
|
||||
"on_convert": self.on_convert,
|
||||
"on_picons_src_changed": self.on_picons_src_changed,
|
||||
"on_picons_dest_changed": self.on_picons_dest_changed,
|
||||
"on_picons_view_drag_data_get": self.on_picons_view_drag_data_get,
|
||||
"on_picons_src_view_drag_drop": self.on_picons_src_view_drag_drop,
|
||||
"on_picons_src_view_drag_data_received": self.on_picons_src_view_drag_data_received,
|
||||
"on_picons_src_view_drag_end": self.on_picons_src_view_drag_end,
|
||||
"on_picons_view_drag_drop": self.on_picons_view_drag_drop,
|
||||
"on_picons_view_drag_data_received": self.on_picons_view_drag_data_received,
|
||||
"on_picons_view_drag_end": self.on_picons_view_drag_end,
|
||||
"on_picon_info_image_drag_data_received": self.on_picon_info_image_drag_data_received,
|
||||
"on_send_button_drag_data_received": self.on_send_button_drag_data_received,
|
||||
"on_download_button_drag_data_received": self.on_download_button_drag_data_received,
|
||||
@@ -63,25 +101,24 @@ class PiconsDialog:
|
||||
"on_selective_download": self.on_selective_download,
|
||||
"on_selective_remove": self.on_selective_remove,
|
||||
"on_local_remove": self.on_local_remove,
|
||||
"on_picons_dest_view_realize": self.on_picons_dest_view_realize,
|
||||
"on_download_source_changed": self.on_download_source_changed,
|
||||
"on_satellites_view_realize": self.on_satellites_view_realize,
|
||||
"on_satellite_filter_toggled": self.on_satellite_filter_toggled,
|
||||
"on_providers_view_query_tooltip": self.on_providers_view_query_tooltip,
|
||||
"on_satellite_selection": self.on_satellite_selection,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_filter_toggled": self.on_filter_toggled,
|
||||
"on_fiter_srcs_toggled": self.on_fiter_srcs_toggled,
|
||||
"on_filter_services_switch": self.on_filter_services_switch,
|
||||
"on_picon_activated": self.on_picon_activated,
|
||||
"on_view_query_tooltip": self.on_view_query_tooltip,
|
||||
"on_tree_view_key_press": self.on_tree_view_key_press,
|
||||
"on_popup_menu": on_popup_menu}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "picons_manager.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "picons.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("picons_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._app_window = app.get_active_window()
|
||||
self._stack = builder.get_object("stack")
|
||||
self._picons_src_view = builder.get_object("picons_src_view")
|
||||
self._picons_dest_view = builder.get_object("picons_dest_view")
|
||||
self._providers_view = builder.get_object("providers_view")
|
||||
@@ -90,27 +127,16 @@ class PiconsDialog:
|
||||
self._picons_src_filter_model.set_visible_func(self.picons_src_filter_function)
|
||||
self._picons_dst_filter_model = builder.get_object("picons_dst_filter_model")
|
||||
self._picons_dst_filter_model.set_visible_func(self.picons_dst_filter_function)
|
||||
self._explorer_src_path_button = builder.get_object("explorer_src_path_button")
|
||||
self._explorer_dest_path_button = builder.get_object("explorer_dest_path_button")
|
||||
self._expander = builder.get_object("expander")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_button = builder.get_object("filter_button")
|
||||
self._src_filter_button = builder.get_object("src_filter_button")
|
||||
self._dst_filter_button = builder.get_object("dst_filter_button")
|
||||
self._picons_filter_entry = builder.get_object("picons_filter_entry")
|
||||
self._ip_entry = builder.get_object("ip_entry")
|
||||
self._picons_entry = builder.get_object("picons_entry")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._picons_dir_entry = builder.get_object("picons_dir_entry")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._picon_info_image = builder.get_object("picon_info_image")
|
||||
self._picon_info_label = builder.get_object("picon_info_label")
|
||||
self._load_providers_button = builder.get_object("load_providers_button")
|
||||
self._download_source_button = builder.get_object("download_source_button")
|
||||
self._receive_button = builder.get_object("receive_button")
|
||||
self._convert_button = builder.get_object("convert_button")
|
||||
self._enigma2_path_button = builder.get_object("enigma2_path_button")
|
||||
@@ -125,40 +151,52 @@ class PiconsDialog:
|
||||
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
|
||||
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
|
||||
self._satellite_label = builder.get_object("satellite_label")
|
||||
self._src_link_button = builder.get_object("src_link_button")
|
||||
self._satellite_filter_switch = builder.get_object("satellite_filter_switch")
|
||||
self._bouquet_filter_switch = builder.get_object("bouquet_filter_switch")
|
||||
self._providers_header_box = builder.get_object("providers_header_box")
|
||||
self._header_download_box = builder.get_object("header_download_box")
|
||||
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
|
||||
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
|
||||
self._satellite_label.bind_property("visible", self._download_source_button, "sensitive")
|
||||
self._satellite_label.bind_property("visible", self._satellites_view, "sensitive")
|
||||
self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
|
||||
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
|
||||
self._load_providers_button.bind_property("visible", self._receive_button, "visible")
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
self._explorer_src_path_button.bind_property("sensitive", builder.get_object("picons_view_sw"), "sensitive")
|
||||
self._filter_button.bind_property("active", builder.get_object("filter_service_box"), "visible")
|
||||
self._filter_button.bind_property("active", builder.get_object("src_title_grid"), "visible")
|
||||
self._filter_button.bind_property("active", builder.get_object("dst_title_grid"), "visible")
|
||||
self._download_source_button.bind_property("visible", self._receive_button, "visible")
|
||||
# Filter.
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._auto_filer_switch = builder.get_object("auto_filer_switch")
|
||||
self._filter_button = builder.get_object("filter_button")
|
||||
self._filter_button.bind_property("active", self._filter_bar, "visible")
|
||||
self._filter_button.bind_property("active", self._src_filter_button, "visible")
|
||||
self._filter_button.bind_property("active", self._dst_filter_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._info_check_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._send_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._download_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._remove_button, "visible")
|
||||
self._src_button = builder.get_object("src_button")
|
||||
self._src_button.bind_property("active", builder.get_object("explorer_dst_label"), "visible")
|
||||
self._src_button.bind_property("active", builder.get_object("src_picon_box_frame"), "visible")
|
||||
self._filter_button.bind_property("visible", self._src_button, "visible")
|
||||
explorer_info_bar = builder.get_object("explorer_info_bar")
|
||||
explorer_info_bar.bind_property("visible", builder.get_object("explorer_info_bar_frame"), "visible")
|
||||
self._info_check_button.bind_property("active", explorer_info_bar, "visible")
|
||||
# Header buttons. -> Used instead stack switcher.
|
||||
self._manager_button = builder.get_object("manager_button")
|
||||
self._manager_button.bind_property("active", builder.get_object("manager_label"), "visible")
|
||||
self._downloader_button = builder.get_object("downloader_button")
|
||||
self._downloader_button.bind_property("active", builder.get_object("downloader_label"), "visible")
|
||||
self._converter_button = builder.get_object("converter_button")
|
||||
self._converter_button.bind_property("active", builder.get_object("converter_label"), "visible")
|
||||
# Init drag-and-drop
|
||||
self.init_drag_and_drop()
|
||||
# Style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
# Settings
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
self._ip_entry.set_text(self._settings.host)
|
||||
self._picons_entry.set_text(self._settings.picons_path)
|
||||
self._picons_dir_entry.set_text(self._settings.picons_local_path)
|
||||
self._picons_dir_entry.set_text(self._settings.profile_picons_path)
|
||||
|
||||
window_size = self._settings.get("picons_downloader_window_size")
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
self.pack_start(builder.get_object("picon_manager_frame"), True, True, 0)
|
||||
self.show()
|
||||
|
||||
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
|
||||
message = get_message("To automatically set the identifiers for picons,\n"
|
||||
@@ -166,37 +204,52 @@ class PiconsDialog:
|
||||
self.show_info_message(message, Gtk.MessageType.WARNING)
|
||||
self._satellite_label.show()
|
||||
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
def on_tool_switched(self, button):
|
||||
if not button.get_active():
|
||||
return True
|
||||
|
||||
def on_picons_dest_view_realize(self, view):
|
||||
self._services = {s.picon_id: s for s in self._app.current_services.values() if s.picon_id}
|
||||
self._explorer_dest_path_button.select_filename(self._settings.picons_local_path)
|
||||
is_explorer = button is self._manager_button
|
||||
is_downloader = button is self._downloader_button
|
||||
is_converter = button is self._converter_button
|
||||
|
||||
def on_picons_src_changed(self, button):
|
||||
self.update_picons_data(self._picons_src_view, button)
|
||||
name = "explorer"
|
||||
if is_downloader:
|
||||
name = "downloader"
|
||||
elif is_converter:
|
||||
name = "converter"
|
||||
|
||||
def on_picons_dest_changed(self, button):
|
||||
self.update_picon_info()
|
||||
self.update_picons_data(self._picons_dest_view, button)
|
||||
self._stack.set_visible_child_name(name)
|
||||
|
||||
def update_picons_data(self, view, button):
|
||||
path = button.get_filename()
|
||||
if not path or not os.path.exists(path):
|
||||
self._convert_button.set_visible(is_converter)
|
||||
self._download_source_button.set_visible(is_downloader)
|
||||
self._filter_button.set_visible(is_explorer)
|
||||
if is_explorer:
|
||||
self.update_picons_data(self._picons_dest_view)
|
||||
|
||||
def on_open(self):
|
||||
""" Opens picons from local path [in src view]. """
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, title="Open folder")
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
GLib.idle_add(button.set_sensitive, False)
|
||||
gen = self.update_picons(path, view, button)
|
||||
self._src_button.set_active(True)
|
||||
self.update_picons_data(self._picons_src_view, response)
|
||||
|
||||
def update_picons_dest(self, app, page):
|
||||
if page is Page.PICONS:
|
||||
self._services = {s.picon_id: s for s in self._app.current_services.values() if s.picon_id}
|
||||
self.update_picons_data(self._picons_dest_view)
|
||||
|
||||
def update_picons_data(self, view, path=None):
|
||||
if view is self._picons_dest_view:
|
||||
self.update_picon_info()
|
||||
|
||||
gen = self.update_picons(path or self._settings.profile_picons_path, view)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update_picons(self, path, view, button):
|
||||
def update_picons(self, path, view):
|
||||
p_model = view.get_model()
|
||||
if not p_model:
|
||||
button.set_sensitive(True)
|
||||
return
|
||||
|
||||
model = get_base_model(p_model)
|
||||
view.set_model(None)
|
||||
factor = self._app.DEL_FACTOR
|
||||
|
||||
for index, itr in enumerate([row.iter for row in model]):
|
||||
@@ -204,17 +257,18 @@ class PiconsDialog:
|
||||
if index % factor == 0:
|
||||
yield True
|
||||
|
||||
if not os.path.isdir(path):
|
||||
return
|
||||
|
||||
for file in os.listdir(path):
|
||||
if self._terminate:
|
||||
return
|
||||
|
||||
p_path = "{}/{}".format(path, file)
|
||||
p_path = "{}{}{}".format(path, SEP, file)
|
||||
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
|
||||
if p:
|
||||
yield model.append((p, file, p_path))
|
||||
|
||||
view.set_model(p_model)
|
||||
button.set_sensitive(True)
|
||||
yield True
|
||||
|
||||
def update_picons_from_file(self, view, uri):
|
||||
@@ -231,7 +285,7 @@ class PiconsDialog:
|
||||
if p:
|
||||
model.append((p, path.name, f_path))
|
||||
elif path.is_dir():
|
||||
self._explorer_src_path_button.select_filename(f_path)
|
||||
self.update_picons_data(view, f_path)
|
||||
|
||||
def get_pixbuf_at_scale(self, path, width, height, p_ratio):
|
||||
try:
|
||||
@@ -251,6 +305,9 @@ class PiconsDialog:
|
||||
self._picons_src_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
|
||||
self._picons_src_view.drag_dest_add_text_targets()
|
||||
|
||||
self._picons_dest_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
|
||||
self._picons_dest_view.drag_dest_add_text_targets()
|
||||
|
||||
self._picon_info_image.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
||||
self._picon_info_image.drag_dest_add_uri_targets()
|
||||
|
||||
@@ -267,14 +324,14 @@ class PiconsDialog:
|
||||
model, path = view.get_selection().get_selected_rows()
|
||||
if path:
|
||||
data.set_uris([Path(model[path][-1]).as_uri(),
|
||||
Path(self._explorer_dest_path_button.get_filename()).as_uri()])
|
||||
Path(self._settings.profile_picons_path).as_uri()])
|
||||
|
||||
def on_picons_src_view_drag_drop(self, view, drag_context, x, y, time):
|
||||
def on_picons_view_drag_drop(self, view, drag_context, x, y, time):
|
||||
view.stop_emission_by_name("drag_drop")
|
||||
targets = drag_context.list_targets()
|
||||
view.drag_get_data(drag_context, targets[-1] if targets else Gdk.atom_intern("text/plain", False), time)
|
||||
|
||||
def on_picons_src_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
|
||||
def on_picons_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
|
||||
view.stop_emission_by_name("drag_data_received")
|
||||
txt = data.get_text()
|
||||
if not txt:
|
||||
@@ -301,7 +358,7 @@ class PiconsDialog:
|
||||
c_id = Column.SRV_FAV_ID
|
||||
|
||||
t_mod = target_view.get_model()
|
||||
dest_path = self._explorer_dest_path_button.get_filename() + "/"
|
||||
dest_path = self._settings.profile_picons_path
|
||||
self.update_picons_dest_view(self._app.on_assign_picon(target_view, model[path][-1], dest_path))
|
||||
self.show_assign_info([t_mod.get_value(t_mod.get_iter_from_string(itr), c_id) for itr in itr_str.split(",")])
|
||||
|
||||
@@ -333,7 +390,7 @@ class PiconsDialog:
|
||||
info = self._app.get_hint_for_srv_list(srv)
|
||||
self.append_output("Picon assignment for the service:\n{}\n{}\n".format(info, " * " * 30))
|
||||
|
||||
def on_picons_src_view_drag_end(self, view, drag_context):
|
||||
def on_picons_view_drag_end(self, view, drag_context):
|
||||
self.update_picons_dest_view(self._app.picons_buffer)
|
||||
|
||||
def on_picon_info_image_drag_data_received(self, img, drag_context, x, y, data, info, time):
|
||||
@@ -345,7 +402,7 @@ class PiconsDialog:
|
||||
if len(uris) == 2:
|
||||
name, fav_id = self._current_picon_info
|
||||
src = urlparse(unquote(uris[0])).path
|
||||
dst = "{}/{}".format(urlparse(unquote(uris[1])).path, name)
|
||||
dst = "{}{}{}".format(urlparse(unquote(uris[1])).path, SEP, name)
|
||||
if src != dst:
|
||||
shutil.copy(src, dst)
|
||||
for row in get_base_model(self._picons_dest_view.get_model()):
|
||||
@@ -401,7 +458,7 @@ class PiconsDialog:
|
||||
|
||||
def on_local_remove(self, view):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
|
||||
if paths and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.OK:
|
||||
itr = model.get_iter(paths.pop())
|
||||
p_path = Path(model.get_value(itr, 2)).resolve()
|
||||
if p_path.is_file():
|
||||
@@ -412,12 +469,9 @@ class PiconsDialog:
|
||||
base_model.remove(itr)
|
||||
|
||||
def on_send(self, item=None, files_filter=None, path=None):
|
||||
dest_path = path or self.check_dest_path()
|
||||
if not dest_path:
|
||||
return
|
||||
|
||||
dest_path = path or self._settings.profile_picons_path
|
||||
settings = Settings(self._settings.settings)
|
||||
settings.picons_local_path = "{}/".format(dest_path)
|
||||
settings.profile_picons_path = "{}{}".format(dest_path, SEP)
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self.run_func(lambda: upload_data(settings=settings,
|
||||
download_type=DownloadType.PICONS,
|
||||
@@ -427,19 +481,16 @@ class PiconsDialog:
|
||||
files_filter=files_filter))
|
||||
|
||||
def on_download(self, item=None, files_filter=None, path=None):
|
||||
path = path or self.check_dest_path()
|
||||
if not path:
|
||||
return
|
||||
|
||||
path = path or self._settings.profile_picons_path
|
||||
settings = Settings(self._settings.settings)
|
||||
settings.picons_local_path = path + "/"
|
||||
settings.profile_picons_path = path + SEP
|
||||
self.run_func(lambda: download_data(settings=settings,
|
||||
download_type=DownloadType.PICONS,
|
||||
callback=self.append_output,
|
||||
files_filter=files_filter), True)
|
||||
|
||||
def on_remove(self, item=None, files_filter=None):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
if show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.run_func(lambda: remove_picons(settings=self._settings,
|
||||
@@ -453,85 +504,169 @@ class PiconsDialog:
|
||||
if paths:
|
||||
return Path(model[paths.pop()][-1]).resolve()
|
||||
|
||||
def check_dest_path(self):
|
||||
""" Checks the destination path and returns if present. """
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
path = self._explorer_dest_path_button.get_filename()
|
||||
if not path:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
|
||||
return
|
||||
return path
|
||||
|
||||
# ******************** Downloader ************************* #
|
||||
|
||||
def on_download_source_changed(self, button):
|
||||
self._download_src = self.DownloadSource(button.get_active_id())
|
||||
self.set_providers_header()
|
||||
self._providers_header_box.set_sensitive(self._download_src is self.DownloadSource.PICON_CZ)
|
||||
GLib.idle_add(self._providers_view.get_model().clear)
|
||||
self.init_satellites(self._satellites_view)
|
||||
|
||||
def on_satellites_view_realize(self, view):
|
||||
self.set_providers_header()
|
||||
self.get_satellites(view)
|
||||
|
||||
def on_satellite_filter_toggled(self, button, state):
|
||||
self.init_satellites(self._satellites_view)
|
||||
|
||||
def on_providers_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
return False
|
||||
|
||||
dest = view.get_dest_row_at_pos(x, y)
|
||||
if not dest:
|
||||
return False
|
||||
|
||||
path, pos = dest
|
||||
model = view.get_model()
|
||||
itr = model.get_iter(path)
|
||||
logo_url = model.get_value(itr, 5)
|
||||
if logo_url:
|
||||
pix_data = self._picon_cz_downloader.get_logo_data(logo_url)
|
||||
if pix_data:
|
||||
pix = self.get_pixbuf(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))
|
||||
else:
|
||||
self.update_logo_data(itr, model, logo_url)
|
||||
tooltip.set_text(model.get_value(itr, 1))
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
return True
|
||||
|
||||
@run_task
|
||||
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)
|
||||
GLib.idle_add(model.set_value, itr, 0, pix if pix else TV_ICON)
|
||||
|
||||
@run_idle
|
||||
def set_providers_header(self):
|
||||
if self._download_src is self.DownloadSource.PICON_CZ:
|
||||
link = "https://picon.cz"
|
||||
tooltip = f"{link} (by Chocholoušek)"
|
||||
elif self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
link = "https://www.lyngsat.com"
|
||||
tooltip = f"{get_message('Providers')} [{link}]"
|
||||
else:
|
||||
link = ""
|
||||
tooltip = ""
|
||||
|
||||
self._src_link_button.set_uri(link)
|
||||
self._src_link_button.set_label(link)
|
||||
self._src_link_button.set_tooltip_text(tooltip)
|
||||
|
||||
@run_task
|
||||
def get_satellites(self, view):
|
||||
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
|
||||
if not sats:
|
||||
self._sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
|
||||
if not self._sats:
|
||||
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
|
||||
|
||||
self._sat_names = {s[1]: s[0] for s in self._sats} # position -> satellite name
|
||||
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids, self.append_output)
|
||||
self.init_satellites(view)
|
||||
|
||||
@run_task
|
||||
def init_satellites(self, view):
|
||||
sats = self._sats
|
||||
if self._download_src is self.DownloadSource.PICON_CZ:
|
||||
if not self._picon_cz_downloader:
|
||||
return
|
||||
try:
|
||||
self._picon_cz_downloader.init()
|
||||
except PiconsError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
providers = self._picon_cz_downloader.providers
|
||||
sats = ((self._sat_names.get(p, p), p, None, p, False) for p in providers)
|
||||
gen = self.append_satellites(view.get_model(), sats)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def append_satellites(self, model, sats):
|
||||
is_filter = self._satellite_filter_switch.get_active()
|
||||
if model:
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
for sat in sats:
|
||||
for sat in sorted(sats):
|
||||
pos = sat[1]
|
||||
name = "{} ({})".format(sat[0], pos)
|
||||
|
||||
if not self._terminate and model:
|
||||
if pos in self._sat_positions:
|
||||
yield model.append((name, sat[3], pos))
|
||||
if is_filter and pos not in self._sat_positions:
|
||||
continue
|
||||
if not model:
|
||||
return
|
||||
yield model.append((name, sat[3], pos))
|
||||
finally:
|
||||
self._satellite_label.show()
|
||||
|
||||
def on_satellite_selection(self, view, path, column):
|
||||
model = view.get_model()
|
||||
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
|
||||
|
||||
def on_load_providers(self, item):
|
||||
self.on_info_bar_close()
|
||||
model = self._providers_view.get_model()
|
||||
model.clear()
|
||||
self.get_providers(model)
|
||||
self._satellite_label.set_visible(False)
|
||||
self.get_providers(view.get_model()[path][1], model)
|
||||
|
||||
@run_task
|
||||
def get_providers(self, model):
|
||||
providers = parse_providers(self._url_entry.get_text())
|
||||
if providers:
|
||||
self.append_providers(providers, model)
|
||||
def get_providers(self, url, model):
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
providers = parse_providers(url)
|
||||
elif self._download_src is self.DownloadSource.PICON_CZ:
|
||||
providers = self._picon_cz_downloader.get_sat_providers(url)
|
||||
else:
|
||||
return
|
||||
|
||||
self.append_providers(providers or [], model)
|
||||
|
||||
@run_idle
|
||||
def append_providers(self, providers, model):
|
||||
for p in providers:
|
||||
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
|
||||
self.update_receive_button_state()
|
||||
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))
|
||||
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))
|
||||
|
||||
def get_pixbuf(self, img_data):
|
||||
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, 48, 32, True, None)
|
||||
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
|
||||
|
||||
def on_receive(self, item):
|
||||
self._cancel_button.show()
|
||||
self.start_download()
|
||||
|
||||
@run_task
|
||||
def start_download(self):
|
||||
if self._is_downloading:
|
||||
self.show_dialog("The task is already running!", DialogType.ERROR)
|
||||
self._app.show_error_message("The task is already running!")
|
||||
return
|
||||
|
||||
providers = self.get_selected_providers()
|
||||
|
||||
if self._download_src is self.DownloadSource.PICON_CZ and len(providers) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
self._cancel_button.show()
|
||||
self.start_download(providers)
|
||||
|
||||
@run_task
|
||||
def start_download(self, providers):
|
||||
self._is_downloading = True
|
||||
GLib.idle_add(self._expander.set_expanded, True)
|
||||
|
||||
providers = self.get_selected_providers()
|
||||
for prv in providers:
|
||||
if not self._POS_PATTERN.match(prv[2]):
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
|
||||
self.show_info_message(
|
||||
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
|
||||
scroll_to(prv.path, self._providers_view)
|
||||
@@ -540,42 +675,82 @@ class PiconsDialog:
|
||||
try:
|
||||
picons_path = self._picons_dir_entry.get_text()
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
picons = []
|
||||
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
providers = (Provider(*p) for p in providers)
|
||||
|
||||
import concurrent.futures
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
# Getting links to picons.
|
||||
futures = {executor.submit(self.process_provider, Provider(*p), picons_path): p for p in providers}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self._is_downloading:
|
||||
executor.shutdown()
|
||||
return
|
||||
|
||||
picons.extend(future.result())
|
||||
# Getting picon images.
|
||||
futures = {executor.submit(download_picon, *pic, self.append_output): pic for pic in picons}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_downloading and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT:
|
||||
self.get_picons_for_lyngsat(picons_path, providers)
|
||||
elif self._download_src is self.DownloadSource.PICON_CZ:
|
||||
self.get_picons_for_picon_cz(picons_path, providers)
|
||||
|
||||
if not self._is_downloading:
|
||||
return
|
||||
|
||||
if not self._resize_no_radio_button.get_active():
|
||||
self.resize(picons_path)
|
||||
else:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
finally:
|
||||
self._app.update_picons()
|
||||
GLib.idle_add(self._cancel_button.hide)
|
||||
self._is_downloading = False
|
||||
|
||||
def get_picons_for_lyngsat(self, path, providers):
|
||||
import concurrent.futures
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
picons = []
|
||||
# Getting links to picons.
|
||||
futures = {executor.submit(self.process_provider, p, path): p for p in providers}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self._is_downloading:
|
||||
executor.shutdown()
|
||||
return
|
||||
|
||||
pic = future.result()
|
||||
if pic:
|
||||
picons.extend(pic)
|
||||
# Getting picon images.
|
||||
futures = {executor.submit(download_picon, *pic, self.append_output): pic for pic in picons}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_downloading and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def get_picons_for_picon_cz(self, path, providers):
|
||||
p_ids = None
|
||||
if self._bouquet_filter_switch.get_active():
|
||||
p_ids = self.get_bouquet_picon_ids()
|
||||
if not p_ids:
|
||||
return
|
||||
|
||||
try:
|
||||
# We download it sequentially.
|
||||
for p in providers:
|
||||
self._picon_cz_downloader.download(p, path, p_ids)
|
||||
except PiconsError as e:
|
||||
self.append_output("Error: {}\n".format(str(e)))
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def get_bouquet_picon_ids(self):
|
||||
""" Returns picon ids for selected bouquet or None. """
|
||||
bq_selected = self._app.check_bouquet_selection()
|
||||
if not bq_selected:
|
||||
return
|
||||
|
||||
model, paths = self._app.bouquets_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one bouquet!")
|
||||
return
|
||||
|
||||
fav_bouquet = self._app.current_bouquets[bq_selected]
|
||||
services = self._app.current_services
|
||||
return {services.get(fav_id).picon_id for fav_id in fav_bouquet}
|
||||
|
||||
def process_provider(self, prv, picons_path):
|
||||
self.append_output("Getting links to picons for: {}.\n".format(prv.name))
|
||||
return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())
|
||||
@@ -604,7 +779,7 @@ class PiconsDialog:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def on_cancel(self, item=None):
|
||||
if self._is_downloading and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
if self._is_downloading and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
|
||||
return True
|
||||
|
||||
self.terminate_task()
|
||||
@@ -621,21 +796,8 @@ class PiconsDialog:
|
||||
|
||||
self._terminate = True
|
||||
self._is_downloading = False
|
||||
self.save_window_size(window)
|
||||
self.clean_data()
|
||||
self._app.update_picons()
|
||||
GLib.idle_add(self._dialog.destroy)
|
||||
|
||||
def save_window_size(self, window):
|
||||
size = window.get_size()
|
||||
height = size.height - self._text_view.get_allocated_height() - self._info_bar.get_allocated_height()
|
||||
self._settings.add("picons_downloader_window_size", (size.width, height))
|
||||
|
||||
@run_task
|
||||
def clean_data(self):
|
||||
path = self._TMP_DIR + "www.lyngsat.com"
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
GLib.idle_add(self._app_window.destroy)
|
||||
|
||||
@run_task
|
||||
def run_func(self, func, update=False):
|
||||
@@ -648,20 +810,13 @@ class PiconsDialog:
|
||||
finally:
|
||||
GLib.idle_add(self._header_download_box.set_sensitive, True)
|
||||
if update:
|
||||
self.on_picons_dest_changed(self._explorer_dest_path_button)
|
||||
self.update_picons_data(self._picons_dest_view)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(False)
|
||||
self._message_label.set_text(get_message(text))
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._info_bar.set_visible(True)
|
||||
self._app.show_info_message(text, message_type)
|
||||
|
||||
def on_picons_dir_open(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, settings=self._settings)
|
||||
update_entry_data(entry, self._app_window, settings=self._settings)
|
||||
|
||||
@run_idle
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
@@ -681,9 +836,17 @@ class PiconsDialog:
|
||||
|
||||
# *********************** Filter **************************** #
|
||||
|
||||
def on_app_filter_toggled(self, app, value):
|
||||
if app.page is Page.PICONS:
|
||||
self._filter_button.set_active(not self._filter_button.get_active())
|
||||
|
||||
def on_fav_changed(self, view, path, column):
|
||||
if self._app.page is Page.PICONS and self._auto_filer_switch.get_active():
|
||||
model = view.get_model()
|
||||
self._picons_filter_entry.set_text(model.get_value(model.get_iter(path), Column.FAV_SERVICE))
|
||||
|
||||
def on_filter_toggled(self, button):
|
||||
active = button.get_active()
|
||||
self._filter_bar.set_search_mode(active)
|
||||
active = self._filter_button.get_active()
|
||||
if not active:
|
||||
self._picons_filter_entry.set_text("")
|
||||
|
||||
@@ -691,18 +854,13 @@ class PiconsDialog:
|
||||
""" Activates re-filtering for model when filter check-button has toggled. """
|
||||
GLib.idle_add(filter_model.refilter, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_filter_services_switch(self, button, state):
|
||||
""" Activates or deactivates filtering in the main list of services. """
|
||||
if state:
|
||||
self._filter_binding = self._picons_filter_entry.bind_property("text", self._app.filter_entry, "text")
|
||||
self._app.filter_entry.set_text(self._picons_filter_entry.get_text())
|
||||
else:
|
||||
if self._filter_binding:
|
||||
self._filter_binding.unbind()
|
||||
self._app.filter_entry.set_text("")
|
||||
|
||||
@run_with_delay(1)
|
||||
@run_with_delay(0.5)
|
||||
def on_picons_filter_changed(self, entry):
|
||||
txt = entry.get_text().upper()
|
||||
self._filter_cache.clear()
|
||||
for s in self._app.current_services.values():
|
||||
self._filter_cache[s.picon_id] = txt in s.service.upper()
|
||||
|
||||
GLib.idle_add(self._picons_src_filter_model.refilter, priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._picons_dst_filter_model.refilter, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@@ -722,8 +880,7 @@ class PiconsDialog:
|
||||
return True
|
||||
|
||||
txt = self._picons_filter_entry.get_text().upper()
|
||||
return txt in t.upper() or t in (
|
||||
map(lambda s: s.picon_id, filter(lambda s: txt in s.service.upper(), self._app.current_services.values())))
|
||||
return txt in t.upper() or self._filter_cache.get(t, False)
|
||||
|
||||
def on_picon_activated(self, view):
|
||||
if self._info_check_button.get_active():
|
||||
@@ -780,31 +937,21 @@ class PiconsDialog:
|
||||
def on_url_changed(self, entry):
|
||||
suit = self._PATTERN.search(entry.get_text())
|
||||
entry.set_name("GtkEntry" if suit else "digit-entry")
|
||||
self._load_providers_button.set_sensitive(suit if suit else False)
|
||||
self._download_source_button.set_sensitive(suit if suit else False)
|
||||
|
||||
def on_position_edited(self, render, path, value):
|
||||
model = self._providers_view.get_model()
|
||||
model.set_value(model.get_iter(path), 2, value)
|
||||
|
||||
@run_idle
|
||||
def on_visible_page(self, stack: Gtk.Stack, param):
|
||||
name = stack.get_visible_child_name()
|
||||
self._convert_button.set_visible(name == "converter")
|
||||
self._load_providers_button.set_visible(name == "downloader")
|
||||
is_explorer = name == "explorer"
|
||||
self._filter_button.set_visible(is_explorer)
|
||||
if is_explorer:
|
||||
self.on_picons_dest_changed(self._explorer_dest_path_button)
|
||||
|
||||
@run_idle
|
||||
def on_convert(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
if show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
picons_path = self._enigma2_path_button.get_filename()
|
||||
save_path = self._save_to_button.get_filename()
|
||||
if not picons_path or not save_path:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
|
||||
self._app.show_error_message("Select paths!")
|
||||
return
|
||||
|
||||
self._expander.set_expanded(True)
|
||||
@@ -827,7 +974,7 @@ class PiconsDialog:
|
||||
|
||||
@run_idle
|
||||
def show_dialog(self, message, dialog_type):
|
||||
show_dialog(dialog_type, self._dialog, message)
|
||||
show_dialog(dialog_type, self._app_window, message)
|
||||
|
||||
def get_picons_format(self):
|
||||
picon_format = SettingsType.ENIGMA_2
|
||||
242
app/ui/playback.glade
Normal file
242
app/ui/playback.glade
Normal file
@@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkEventBox" id="event_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<signal name="button-press-event" handler="on_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_realize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkToolbar" id="tool_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="prev_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-previous</property>
|
||||
<signal name="clicked" handler="on_previous" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="play_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Play</property>
|
||||
<property name="action_name">app.on_play</property>
|
||||
<property name="stock_id">gtk-media-play</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Stop playback</property>
|
||||
<property name="action_name">app.on_stop</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-stop</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="next_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-next</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="player_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="rewind_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scale">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="restrict_to_fill_level">False</property>
|
||||
<property name="fill_level">0</property>
|
||||
<property name="draw_value">False</property>
|
||||
<property name="has_origin">False</property>
|
||||
<signal name="change-value" handler="on_rewind" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="full_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="extras_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="extras_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="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="audio_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="audio_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Audio Track</property>
|
||||
<property name="icon_name">multimedia-volume-control</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="video_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="video_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Aspect ratio</property>
|
||||
<property name="icon_name">view-restore</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="subtitle_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="subtitle_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Subtitle Track</property>
|
||||
<property name="icon_name">format-text-underline</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="full_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-fullscreen</property>
|
||||
<signal name="clicked" handler="on_full_screen" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Close playback</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-close</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
410
app/ui/playback.py
Normal file
410
app/ui/playback.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Additional module for playback. """
|
||||
from functools import lru_cache
|
||||
|
||||
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.tools.media import Player
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.main_helper import get_iptv_url
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class PlayerBox(Gtk.Box):
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Signals.
|
||||
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("playback-close", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("play", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-clicked", self.on_fav_clicked)
|
||||
self._app.connect("page-changed", self.on_page_changed)
|
||||
self._app.connect("play-current", self.on_play_current)
|
||||
self._app.connect("play-recording", self.on_play_recording)
|
||||
self._fav_view = app.fav_view
|
||||
self._player = None
|
||||
self._current_mrl = None
|
||||
self._full_screen = False
|
||||
self._playback_window = None
|
||||
self._audio_track_menu = None
|
||||
self._subtitle_track_menu = None
|
||||
self._play_mode = self._app.app_settings.play_streams_mode
|
||||
|
||||
handlers = {"on_realize": self.on_realize,
|
||||
"on_press": self.on_press,
|
||||
"on_next": self.on_next,
|
||||
"on_previous": self.on_previous,
|
||||
"on_rewind": self.on_rewind,
|
||||
"on_full_screen": self.on_full_screen,
|
||||
"on_close": self.on_close}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
|
||||
self.set_spacing(5)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self._event_box = builder.get_object("event_box")
|
||||
self.pack_start(self._event_box, True, True, 0)
|
||||
if not IS_DARWIN:
|
||||
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
|
||||
self._scale = builder.get_object("scale")
|
||||
self._full_time_label = builder.get_object("full_time_label")
|
||||
self._current_time_label = builder.get_object("current_time_label")
|
||||
self._rewind_box = builder.get_object("rewind_box")
|
||||
self._tool_bar = builder.get_object("tool_bar")
|
||||
self._prev_button = builder.get_object("prev_button")
|
||||
self._next_button = builder.get_object("next_button")
|
||||
self._audio_menu_button = builder.get_object("audio_menu_button")
|
||||
self._video_menu_button = builder.get_object("video_menu_button")
|
||||
self._subtitle_menu_button = builder.get_object("subtitle_menu_button")
|
||||
self._fav_view.bind_property("sensitive", self._prev_button, "sensitive")
|
||||
self._fav_view.bind_property("sensitive", self._next_button, "sensitive")
|
||||
|
||||
self.connect("delete-event", self.on_delete)
|
||||
self.connect("show", self.set_player_area_size)
|
||||
|
||||
def on_fav_clicked(self, app, mode):
|
||||
if mode is not FavClickMode.STREAM and not self._app.http_api:
|
||||
return
|
||||
|
||||
self._fav_view.set_sensitive(False)
|
||||
if mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
elif mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
|
||||
def on_play_current(self, app, url):
|
||||
self.on_watch()
|
||||
|
||||
def on_play_recording(self, app, url):
|
||||
self.play(url)
|
||||
|
||||
def on_page_changed(self, app, page):
|
||||
self.on_close()
|
||||
self.set_visible(False)
|
||||
|
||||
def on_realize(self, box):
|
||||
if not self._player:
|
||||
settings = self._app.app_settings
|
||||
try:
|
||||
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
|
||||
except (ImportError, NameError) as e:
|
||||
self._app.show_error_message(str(e))
|
||||
return True
|
||||
else:
|
||||
self.init_playback_elements()
|
||||
self.emit("play", self._current_mrl)
|
||||
finally:
|
||||
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.set_player_area_size(box)
|
||||
|
||||
def init_playback_elements(self):
|
||||
self._player.connect("error", self.on_error)
|
||||
self._player.connect("played", self.on_played)
|
||||
self._player.connect("audio-track", self.on_audio_track_changed)
|
||||
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
|
||||
self._app.app_window.connect("key-press-event", self.on_key_press)
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
|
||||
self._audio_track_menu = builder.get_object("audio_track_menu")
|
||||
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
|
||||
audio_menu = builder.get_object("audio_menu")
|
||||
video_menu = builder.get_object("video_menu")
|
||||
subtitle_menu = builder.get_object("subtitle_menu")
|
||||
|
||||
if not IS_GNOME_SESSION:
|
||||
menu_bar = self._app.get_menubar()
|
||||
menu_bar.insert_section(1, None, audio_menu)
|
||||
menu_bar.insert_section(2, None, video_menu)
|
||||
menu_bar.insert_section(3, None, subtitle_menu)
|
||||
|
||||
if not IS_DARWIN:
|
||||
self._player.connect("position", self.on_time_changed)
|
||||
self._audio_menu_button.set_menu_model(self._audio_track_menu)
|
||||
self._video_menu_button.set_menu_model(builder.get_object("aspect_ratio_menu"))
|
||||
self._subtitle_menu_button.set_menu_model(self._subtitle_track_menu)
|
||||
# Actions.
|
||||
self._app.set_action("on_play", self.on_play)
|
||||
self._app.set_action("on_stop", self.on_stop)
|
||||
audio_track_action = Gio.SimpleAction.new_stateful("on_set_audio_track", GLib.VariantType.new("i"),
|
||||
GLib.Variant("i", 0))
|
||||
audio_track_action.connect("activate", self.on_set_audio_track)
|
||||
self._app.add_action(audio_track_action)
|
||||
aspect_action = Gio.SimpleAction.new_stateful("on_set_aspect_ratio", GLib.VariantType.new("s"),
|
||||
GLib.Variant("s", ""))
|
||||
aspect_action.connect("activate", self.on_set_aspect_ratio)
|
||||
self._app.add_action(aspect_action)
|
||||
subtitle_track_action = Gio.SimpleAction.new_stateful("on_set_subtitle_track", GLib.VariantType.new("i"),
|
||||
GLib.Variant("i", -1))
|
||||
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
|
||||
self._app.add_action(subtitle_track_action)
|
||||
|
||||
def on_play(self, action=None, value=None):
|
||||
self.emit("play", None)
|
||||
|
||||
def on_stop(self, action=None, value=None):
|
||||
self.emit("stop", None)
|
||||
|
||||
def on_next(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_previous(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_rewind(self, scale, scroll_type, value):
|
||||
self._player.set_time(int(value))
|
||||
|
||||
def on_full_screen(self, item=None):
|
||||
self._full_screen = not self._full_screen
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self.emit("playback-full-screen", not self._full_screen)
|
||||
elif self._playback_window:
|
||||
if not IS_DARWIN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
|
||||
|
||||
def on_close(self, action=None, value=None):
|
||||
if self._playback_window:
|
||||
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
|
||||
self._playback_window.hide()
|
||||
|
||||
self.on_stop()
|
||||
self.hide()
|
||||
self.emit("playback-close", None)
|
||||
|
||||
return True
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_audio_track_changed(self, player, tracks):
|
||||
self._audio_track_menu.remove_all()
|
||||
for t in tracks:
|
||||
item = Gio.MenuItem.new(t[1], None)
|
||||
item.set_action_and_target_value("app.on_set_audio_track", GLib.Variant("i", t[0]))
|
||||
self._audio_track_menu.append_item(item)
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_subtitle_track_changed(self, player, tracks):
|
||||
self._subtitle_track_menu.remove_all()
|
||||
for t in tracks:
|
||||
item = Gio.MenuItem.new(t[1], None)
|
||||
item.set_action_and_target_value("app.on_set_subtitle_track", GLib.Variant("i", t[0]))
|
||||
self._subtitle_track_menu.append_item(item)
|
||||
|
||||
def on_set_audio_track(self, action, value):
|
||||
action.set_state(value)
|
||||
self._player.set_audio_track(value.get_int32())
|
||||
|
||||
def on_set_aspect_ratio(self, action, value):
|
||||
action.set_state(value)
|
||||
self._player.set_aspect_ratio(value.get_string())
|
||||
|
||||
def on_set_subtitle_track(self, action, value):
|
||||
action.set_state(value)
|
||||
self._player.set_subtitle_track(value.get_int32())
|
||||
|
||||
def on_press(self, area, event):
|
||||
if event.button == Gdk.BUTTON_PRIMARY:
|
||||
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_full_screen()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
if self._player and self.get_visible():
|
||||
key = event.keyval
|
||||
if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)):
|
||||
self.on_full_screen()
|
||||
|
||||
def on_delete(self, box):
|
||||
if self._player:
|
||||
self._player.release()
|
||||
|
||||
@run_with_delay(1)
|
||||
def set_player_action(self):
|
||||
click_mode = self._app.app_settings.fav_click_mode
|
||||
self._fav_view.set_sensitive(False)
|
||||
if click_mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
elif click_mode is FavClickMode.ZAP_PLAY:
|
||||
self.on_zap(self.on_watch)
|
||||
elif click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
|
||||
def update_buttons(self):
|
||||
if self._player:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
current_index = path[0]
|
||||
self._player_prev_button.set_sensitive(current_index != 0)
|
||||
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def on_duration_changed(self, duration):
|
||||
self._scale.set_value(0)
|
||||
self._scale.get_adjustment().set_upper(duration)
|
||||
GLib.idle_add(self._rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._full_time_label.set_text, self.get_time_str(duration),
|
||||
priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_time_changed(self, widget, t):
|
||||
if not self._full_screen and self._rewind_box.get_visible():
|
||||
GLib.idle_add(self._current_time_label.set_text, self.get_time_str(t),
|
||||
priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def get_time_str(self, duration):
|
||||
""" Returns a string representation of time from duration in milliseconds """
|
||||
m, s = divmod(duration // 1000, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
|
||||
|
||||
def set_player_area_size(self, widget):
|
||||
w, h = self._app.app_window.get_size()
|
||||
widget.set_size_request(w * 0.6, -1)
|
||||
|
||||
@run_idle
|
||||
def show_playback_window(self):
|
||||
width, height = 480, 240
|
||||
size = self._app.app_settings.get("playback_window_size")
|
||||
if size:
|
||||
width, height = size
|
||||
|
||||
if self._playback_window:
|
||||
self._playback_window.show()
|
||||
else:
|
||||
self._playback_window = Gtk.Window(title=self.get_playback_title(),
|
||||
window_position=Gtk.WindowPosition.CENTER,
|
||||
icon_name="demon-editor")
|
||||
|
||||
self._playback_window.connect("delete-event", self.on_close)
|
||||
self._playback_window.connect("key-press-event", self.on_key_press)
|
||||
self._playback_window.bind_property("visible", self._event_box, "visible")
|
||||
|
||||
if not IS_DARWIN:
|
||||
self._prev_button.set_visible(False)
|
||||
self._next_button.set_visible(False)
|
||||
|
||||
self.reparent(self._playback_window)
|
||||
self._playback_window.set_application(self._app)
|
||||
|
||||
self.show()
|
||||
self._playback_window.resize(width, height)
|
||||
self._playback_window.show()
|
||||
|
||||
def get_playback_title(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
return "DemonEditor [{}]".format(self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE])
|
||||
return "DemonEditor [Playback]"
|
||||
|
||||
def on_play_stream(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
row = self._fav_view.get_model()[path][:]
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
|
||||
self.on_error(None, "Not allowed in this context!")
|
||||
return
|
||||
|
||||
url = get_iptv_url(row, self._app.app_settings.setting_type)
|
||||
self.play(url) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_service(self, item=None):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._app.http_api:
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref(path)
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._player and self._player.is_playing():
|
||||
self.emit("stop", None)
|
||||
|
||||
s_type = self._app.app_settings.setting_type
|
||||
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
|
||||
self._app.http_api.send(req, ref, self.watch)
|
||||
|
||||
def on_watch(self, item=None):
|
||||
""" Switch to the channel and watch in the player. """
|
||||
s_type = self._app.app_settings.setting_type
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
|
||||
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
|
||||
self.watch))
|
||||
|
||||
def watch(self, data):
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
|
||||
|
||||
def play(self, url):
|
||||
if self._play_mode is PlayStreamsMode.M3U:
|
||||
self._app.save_stream_to_m3u(url)
|
||||
return
|
||||
|
||||
if self._play_mode is not self._app.app_settings.play_streams_mode:
|
||||
self.on_error(None, "Play mode has been changed!\nRestart the program to apply the settings.")
|
||||
return
|
||||
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.show()
|
||||
elif self._play_mode is PlayStreamsMode.WINDOW:
|
||||
self.show_playback_window()
|
||||
|
||||
if self._player:
|
||||
self.emit("play", url)
|
||||
else:
|
||||
self._current_mrl = url
|
||||
|
||||
def on_played(self, player, duration):
|
||||
GLib.idle_add(self._fav_view.set_sensitive, True)
|
||||
if not IS_DARWIN:
|
||||
self.on_duration_changed(duration)
|
||||
|
||||
def on_error(self, player, msg):
|
||||
self._app.show_error_message(msg)
|
||||
self._fav_view.set_sensitive(True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import concurrent.futures
|
||||
import re
|
||||
import time
|
||||
from math import fabs
|
||||
from pyexpat import ExpatError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
@@ -9,104 +38,90 @@ from app.commons import run_idle, run_task, log
|
||||
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from app.eparser.ecommons import PLS_MODE, get_key_by_value
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
|
||||
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog, get_message
|
||||
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from .main_helper import move_items, append_text_to_tview, get_base_model, on_popup_menu
|
||||
from .search import SearchProvider
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
|
||||
_UI_PATH = UI_RESOURCES_PATH + "satellites.glade"
|
||||
|
||||
|
||||
def show_satellites_dialog(transient, options):
|
||||
SatellitesDialog(transient, options).show()
|
||||
|
||||
|
||||
class SatellitesDialog:
|
||||
class SatellitesTool(Gtk.Box):
|
||||
_aggr = [None for x in range(9)] # aggregate
|
||||
|
||||
def __init__(self, transient, settings):
|
||||
self._data_path = settings.data_local_path + "satellites.xml"
|
||||
self._settings = settings
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
handlers = {"on_open": self.on_open,
|
||||
"on_remove": self.on_remove,
|
||||
"on_save": self.on_save,
|
||||
"on_save_as": self.on_save_as,
|
||||
self._app = app
|
||||
self._settings = settings
|
||||
self._current_sat_path = None
|
||||
|
||||
handlers = {"on_remove": self.on_remove,
|
||||
"on_update": self.on_update,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_button_press": self.on_button_press,
|
||||
"on_satellite_add": self.on_satellite_add,
|
||||
"on_transponder_add": self.on_transponder_add,
|
||||
"on_edit": self.on_edit,
|
||||
"on_key_release": self.on_key_release,
|
||||
"on_row_activated": self.on_row_activated,
|
||||
"on_resize": self.on_resize,
|
||||
"on_quit": self.on_quit}
|
||||
"on_satellite_selection": self.on_satellite_selection}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
|
||||
("satellites_editor_window", "satellites_tree_store", "popup_menu",
|
||||
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("satellite_editor_box", "satellite_view_model", "transponder_view_model",
|
||||
"satellite_popup_menu", "transponder_popup_menu", "left_header_menu",
|
||||
"popup_menu_add_image", "popup_menu_add_image_2"))
|
||||
|
||||
self._window = builder.get_object("satellites_editor_window")
|
||||
self._window.set_transient_for(transient)
|
||||
self._sat_view = builder.get_object("satellites_editor_tree_view")
|
||||
# Setting the last size of the dialog window if it was saved
|
||||
window_size = self._settings.get("sat_editor_window_size")
|
||||
if window_size:
|
||||
self._window.resize(*window_size)
|
||||
self._satellite_view = builder.get_object("satellite_view")
|
||||
self._transponder_view = builder.get_object("transponder_view")
|
||||
builder.get_object("sat_pos_column").set_cell_data_func(builder.get_object("sat_pos_renderer"),
|
||||
self.sat_pos_func)
|
||||
|
||||
self._stores = {3: builder.get_object("pol_store"),
|
||||
4: builder.get_object("fec_store"),
|
||||
5: builder.get_object("system_store"),
|
||||
6: builder.get_object("mod_store")}
|
||||
|
||||
self.load_satellites_list(self._sat_view.get_model())
|
||||
self.pack_start(builder.get_object("satellite_editor_box"), True, True, 0)
|
||||
self._app.connect("profile-changed", lambda a, m: self.load_satellites_list())
|
||||
self.show()
|
||||
self.load_satellites_list()
|
||||
|
||||
def load_satellites_list(self, model):
|
||||
gen = self.on_satellites_list_load(model)
|
||||
def load_satellites_list(self, path=None):
|
||||
gen = self.on_satellites_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def show(self):
|
||||
self._window.show()
|
||||
|
||||
def on_resize(self, window):
|
||||
""" Stores new size properties for dialog window after resize """
|
||||
if self._settings:
|
||||
self._settings.add("sat_editor_window_size", window.get_size())
|
||||
|
||||
@run_idle
|
||||
def on_quit(self, *args):
|
||||
self._window.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_open(self, model):
|
||||
response = get_chooser_dialog(self._window, self._settings, "satellites.xml", ("*.xml",))
|
||||
def on_open(self):
|
||||
response = get_chooser_dialog(self._app.app_window, self._settings, "satellites.xml", ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
if not str(response).endswith("satellites.xml"):
|
||||
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
|
||||
self._app.show_error_message("No satellites.xml file is selected!")
|
||||
return
|
||||
|
||||
self._data_path = response
|
||||
self.load_satellites_list(model)
|
||||
self.load_satellites_list(response)
|
||||
|
||||
@staticmethod
|
||||
def on_row_activated(view, path, column):
|
||||
if view.row_expanded(path):
|
||||
view.collapse_row(path)
|
||||
else:
|
||||
view.expand_row(path, column)
|
||||
def on_satellite_selection(self, view):
|
||||
model = self._transponder_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_sat_path, column = view.get_cursor()
|
||||
if self._current_sat_path:
|
||||
list(map(model.append, view.get_model()[self._current_sat_path][-1]))
|
||||
|
||||
def on_up(self, item):
|
||||
move_items(KeyboardKey.UP, self._sat_view)
|
||||
move_items(KeyboardKey.UP, self._satellite_view)
|
||||
|
||||
def on_down(self, item):
|
||||
move_items(KeyboardKey.DOWN, self._sat_view)
|
||||
move_items(KeyboardKey.DOWN, self._satellite_view)
|
||||
|
||||
def on_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_edit(self._satellite_view if self._satellite_view.is_focus() else self._transponder_view)
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
@@ -127,24 +142,28 @@ class SatellitesDialog:
|
||||
elif ctrl and key is KeyboardKey.T:
|
||||
self.on_transponder()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, self._sat_view)
|
||||
move_items(key, self._satellite_view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_satellites_list_load(self, model):
|
||||
def on_satellites_list_load(self, path=None):
|
||||
""" Load satellites data into model """
|
||||
model = self._satellite_view.get_model()
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
satellites = get_satellites(self._data_path)
|
||||
path = path or self._settings.profile_data_path + "satellites.xml"
|
||||
satellites = get_satellites(path)
|
||||
yield True
|
||||
except FileNotFoundError as e:
|
||||
show_dialog(DialogType.ERROR, self._window, getattr(e, "message", str(e)) +
|
||||
"\n\nPlease, download files from receiver or setup your path for read data!")
|
||||
return
|
||||
msg = get_message("Please, download files from receiver or setup your path for read data!")
|
||||
self._app.show_error_message(f"{e}\n{msg}")
|
||||
except ExpatError as e:
|
||||
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
|
||||
self._app.show_error_message(msg)
|
||||
else:
|
||||
model.clear()
|
||||
for sat in satellites:
|
||||
append_satellite(model, sat)
|
||||
yield True
|
||||
yield model.append(sat)
|
||||
|
||||
def on_add(self, view):
|
||||
""" Common adding """
|
||||
@@ -163,148 +182,104 @@ class SatellitesDialog:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
itr = model.get_iter(paths[0])
|
||||
row = model.get(itr, *[x for x in range(view.get_n_columns())])
|
||||
row = model[paths][:]
|
||||
itr = model.get_iter(paths)
|
||||
|
||||
if row[-1]: # satellite
|
||||
self.on_satellite(None if force else Satellite(row[0], None, row[-1], None), itr)
|
||||
else:
|
||||
self.on_transponder(None if force else Transponder(*row[1:-2]), itr)
|
||||
if view is self._satellite_view:
|
||||
self.on_satellite(None if force else Satellite(*row), itr)
|
||||
elif view is self._transponder_view:
|
||||
self.on_transponder(None if force else Transponder(*row), itr)
|
||||
|
||||
def on_satellite(self, satellite=None, edited_itr=None):
|
||||
""" Create or edit satellite"""
|
||||
sat_dialog = SatelliteDialog(self._window, satellite)
|
||||
sat_dialog = SatelliteDialog(self._app.get_active_window(), satellite)
|
||||
sat = sat_dialog.run()
|
||||
sat_dialog.destroy()
|
||||
|
||||
if sat:
|
||||
view = self._sat_view
|
||||
model = view.get_model()
|
||||
model, paths = self._satellite_view.get_selection().get_selected_rows()
|
||||
if satellite and edited_itr:
|
||||
model.set(edited_itr, {0: sat.name, 10: sat.flags, 11: sat.position})
|
||||
model.set(edited_itr, {i: v for i, v in enumerate(sat)})
|
||||
else:
|
||||
index = self.get_sat_position_index(sat.position, model)
|
||||
model.insert(None, index, [sat.name, *self._aggr, sat.flags, sat.position])
|
||||
scroll_to(index, view)
|
||||
if len(model):
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
model.insert(index, sat)
|
||||
else:
|
||||
model.append(sat)
|
||||
|
||||
def on_transponder(self, transponder=None, edited_itr=None):
|
||||
""" Create or edit transponder """
|
||||
|
||||
paths = self.check_selection(self._sat_view, "Please, select only one satellite!")
|
||||
paths = self.check_selection(self._satellite_view, "Please, select only one satellite!")
|
||||
if paths is None:
|
||||
return
|
||||
elif len(paths) == 0:
|
||||
show_dialog(DialogType.ERROR, self._window, "No satellite is selected!")
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
return
|
||||
|
||||
dialog = TransponderDialog(self._window, transponder)
|
||||
dialog = TransponderDialog(self._app.get_active_window(), transponder)
|
||||
tr = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
if tr:
|
||||
view = self._sat_view
|
||||
model = view.get_model()
|
||||
sat_model = self._satellite_view.get_model()
|
||||
transponders = sat_model[paths][-1]
|
||||
tr_model, tr_paths = self._transponder_view.get_selection().get_selected_rows()
|
||||
|
||||
if transponder and edited_itr:
|
||||
model.set(edited_itr, {1: tr.frequency, 2: tr.symbol_rate, 3: tr.polarization,
|
||||
4: tr.fec_inner, 5: tr.system, 6: tr.modulation,
|
||||
7: tr.pls_mode, 8: tr.pls_code, 9: tr.is_id})
|
||||
tr_model.set(edited_itr, {i: v for i, v in enumerate(tr)})
|
||||
transponders[tr_model.get_path(edited_itr).get_indices()[0]] = tr
|
||||
else:
|
||||
row = ["Transponder:", *tr, None, None]
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
itr = model.get_iter(paths[0])
|
||||
view.expand_row(paths[0], 0)
|
||||
# Get parent iter if selected transponder
|
||||
parent_itr = model.iter_parent(itr)
|
||||
if parent_itr:
|
||||
itr = parent_itr
|
||||
freq = int(tr.frequency if tr.frequency else 0)
|
||||
tr_itr = model.iter_children(itr)
|
||||
# Inserting according to frequency value.
|
||||
while tr_itr:
|
||||
cur_freq = int(model.get_value(tr_itr, 1))
|
||||
if freq <= cur_freq:
|
||||
path = model.get_path(tr_itr)
|
||||
index = path.get_indices()[1]
|
||||
model.insert(model.iter_parent(tr_itr), index, row)
|
||||
scroll_to(path, view)
|
||||
break
|
||||
else:
|
||||
tr_itr = model.iter_next(tr_itr)
|
||||
else:
|
||||
itr = model.append(itr, row)
|
||||
scroll_to(model.get_path(itr), view)
|
||||
|
||||
def get_sat_position_index(self, pos, model):
|
||||
""" Search and returns index after given position """
|
||||
pos = int(pos)
|
||||
row = next(filter(lambda r: int(r[-1]) >= pos, model), None)
|
||||
|
||||
return row.path[0] if row else len(model)
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
tr_model.insert(index, tr)
|
||||
transponders.insert(index, tr)
|
||||
|
||||
def check_selection(self, view, message):
|
||||
""" Checks if any row is selected. Shows error dialog if selected more than one.
|
||||
|
||||
returns selected path or None
|
||||
Returns selected path or None.
|
||||
"""
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
show_dialog(DialogType.ERROR, self._window, message)
|
||||
self._app.show_error_message(message)
|
||||
return
|
||||
|
||||
return paths
|
||||
|
||||
@run_idle
|
||||
def on_remove(self, view):
|
||||
""" Removal of selected satellites and transponders.
|
||||
|
||||
The satellites are removed first! Then transponders.
|
||||
"""
|
||||
""" Removes selected satellites and transponders. """
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
satellites = list(filter(model.iter_has_child, itrs))
|
||||
if len(satellites):
|
||||
# Removing selected satellites.
|
||||
list(map(model.remove, satellites))
|
||||
else:
|
||||
# Removing selected transponders.
|
||||
list(map(model.remove, itrs))
|
||||
|
||||
if view is self._satellite_view:
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
elif view is self._transponder_view:
|
||||
if self._current_sat_path:
|
||||
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
|
||||
list(map(trs.pop, sorted(map(lambda p: p.get_indices()[0], paths), reverse=True)))
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
else:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
|
||||
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'}")
|
||||
|
||||
@run_idle
|
||||
def on_save(self, view):
|
||||
if show_dialog(DialogType.QUESTION, self._window) == Gtk.ResponseType.CANCEL:
|
||||
def on_save(self):
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
satellites = []
|
||||
model.foreach(self.parse_data, satellites)
|
||||
write_satellites(satellites, self._data_path)
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
self._settings.profile_data_path + "satellites.xml")
|
||||
|
||||
def on_save_as(self, item):
|
||||
response = self.get_file_dialog_response(Gtk.FileChooserAction.SAVE)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
show_dialog(DialogType.ERROR, transient=self._window, text="Not implemented yet!")
|
||||
def on_save_as(self):
|
||||
show_dialog(DialogType.ERROR, transient=self._app.app_window, text="Not implemented yet!")
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item):
|
||||
SatellitesUpdateDialog(self._window, self._settings, self._sat_view.get_model()).show()
|
||||
|
||||
@staticmethod
|
||||
def parse_data(model, path, itr, sats):
|
||||
if model.iter_has_child(itr):
|
||||
num_of_children = model.iter_n_children(itr)
|
||||
transponders = []
|
||||
num_columns = model.get_n_columns()
|
||||
|
||||
for num in range(num_of_children):
|
||||
transponder_itr = model.iter_nth_child(itr, num)
|
||||
transponder = model.get(transponder_itr, *[item for item in range(num_columns)])
|
||||
transponders.append(Transponder(*transponder[1:-2]))
|
||||
|
||||
sat = model.get(itr, *[item for item in range(num_columns)])
|
||||
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
|
||||
sats.append(satellite)
|
||||
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
|
||||
|
||||
|
||||
# ***************** Transponder dialog *******************#
|
||||
@@ -315,13 +290,8 @@ class TransponderDialog:
|
||||
def __init__(self, transient, transponder: Transponder = None):
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
|
||||
"pls_mode_store"))
|
||||
builder.connect_signals(handlers)
|
||||
objects = ("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True, objects=objects)
|
||||
|
||||
self._dialog = builder.get_object("transponder_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -399,22 +369,20 @@ class TransponderDialog:
|
||||
class SatelliteDialog:
|
||||
""" Shows dialog for adding or edit satellite """
|
||||
|
||||
def __init__(self, transient, satellite: Satellite = None):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
def __init__(self, transient, satellite=None):
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
|
||||
self._dialog = builder.get_object("satellite_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._sat_name = builder.get_object("sat_name_entry")
|
||||
self._sat_position = builder.get_object("sat_position_button")
|
||||
self._side = builder.get_object("side_box")
|
||||
self._transponders = satellite.transponders if satellite else []
|
||||
|
||||
if satellite:
|
||||
self._sat_name.set_text(satellite.name[0:satellite.name.find("(")].strip())
|
||||
self._sat_name.set_text(satellite.name)
|
||||
pos = satellite.position
|
||||
pos = float("{}.{}".format(pos[:-1], pos[-1:]))
|
||||
pos = float(f"{pos[:-1]}.{pos[-1:]}")
|
||||
self._sat_position.set_value(fabs(pos))
|
||||
self._side.set_active(0 if pos >= 0 else 1) # E or W
|
||||
|
||||
@@ -431,10 +399,9 @@ class SatelliteDialog:
|
||||
name = self._sat_name.get_text()
|
||||
pos = round(self._sat_position.get_value(), 1)
|
||||
side = self._side.get_active()
|
||||
name = "{} ({}{})".format(name, pos, self._side.get_active_id())
|
||||
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
|
||||
|
||||
return Satellite(name=name, flags="0", position=pos, transponders=None)
|
||||
return Satellite(name=name, flags="0", position=pos, transponders=self._transponders)
|
||||
|
||||
|
||||
# ********************** Update dialogs ************************ #
|
||||
@@ -447,6 +414,7 @@ class UpdateDialog:
|
||||
"on_receive_data": self.on_receive_data,
|
||||
"on_cancel_receive": self.on_cancel_receive,
|
||||
"on_satellite_toggled": self.on_satellite_toggled,
|
||||
"on_satellite_changed": self.on_satellite_changed,
|
||||
"on_transponder_toggled": self.on_transponder_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_filter_toggled": self.on_filter_toggled,
|
||||
@@ -455,31 +423,27 @@ class UpdateDialog:
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_filter": self.on_filter,
|
||||
"on_search": self.on_search,
|
||||
"on_search_down": self.on_search_down,
|
||||
"on_search_up": self.on_search_up,
|
||||
"on_quit": self.on_quit}
|
||||
|
||||
self._settings = settings
|
||||
self._download_task = False
|
||||
self._parser = None
|
||||
self._size_name = "{}_window_size".format("_".join(re.findall("[A-Z][^A-Z]*", self.__class__.__name__))).lower()
|
||||
self._size_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}_window_size".lower()
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
|
||||
("satellites_update_window", "update_source_store", "update_sat_list_store",
|
||||
builder = get_builder(UI_RESOURCES_PATH + "satellites.glade", handlers,
|
||||
objects=("satellites_update_window", "update_source_store", "update_sat_list_store",
|
||||
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
|
||||
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
|
||||
"remove_selection_image", "update_transponder_store", "update_service_store"))
|
||||
builder.connect_signals(handlers)
|
||||
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
|
||||
"sat_update_filter_image", "sat_update_search_image", "sat_update_image",
|
||||
"update_transponder_store", "update_service_store"))
|
||||
|
||||
self._window = builder.get_object("satellites_update_window")
|
||||
self._window.set_transient_for(transient)
|
||||
if title:
|
||||
self._window.set_title(title)
|
||||
|
||||
self._transponder_paned = builder.get_object("sat_update_tr_paned")
|
||||
self._transponder_frame = builder.get_object("sat_update_tr_frame")
|
||||
self._sat_view = builder.get_object("sat_update_tree_view")
|
||||
self._transponder_view = builder.get_object("sat_update_tr_view")
|
||||
self._service_view = builder.get_object("sat_update_srv_view")
|
||||
@@ -490,6 +454,12 @@ class UpdateDialog:
|
||||
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
|
||||
self._info_bar_message_label = builder.get_object("info_bar_message_label")
|
||||
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
|
||||
update_button = builder.get_object("sat_update_button")
|
||||
self._sat_view.bind_property("sensitive", update_button, "sensitive")
|
||||
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")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("sat_update_filter_bar")
|
||||
self._from_pos_button = builder.get_object("from_pos_button")
|
||||
@@ -499,11 +469,15 @@ class UpdateDialog:
|
||||
self._filter_model = builder.get_object("update_sat_list_model_filter")
|
||||
self._filter_model.set_visible_func(self.filter_function)
|
||||
self._filter_positions = (0, 0)
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
# Search
|
||||
self._search_bar = builder.get_object("sat_update_search_bar")
|
||||
self._search_provider = SearchProvider((self._sat_view,),
|
||||
builder.get_object("sat_update_search_down_button"),
|
||||
builder.get_object("sat_update_search_up_button"))
|
||||
self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible")
|
||||
search_provider = SearchProvider(self._sat_view,
|
||||
builder.get_object("sat_update_search_entry"),
|
||||
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)
|
||||
|
||||
window_size = self._settings.get(self._size_name)
|
||||
if window_size:
|
||||
@@ -522,14 +496,17 @@ class UpdateDialog:
|
||||
self._receive_button.set_visible(not value)
|
||||
|
||||
@run_idle
|
||||
def on_update_satellites_list(self, item):
|
||||
def on_update_satellites_list(self, item=None):
|
||||
if self.is_download:
|
||||
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
|
||||
return
|
||||
|
||||
model = get_base_model(self._sat_view.get_model())
|
||||
model.clear()
|
||||
get_base_model(self._sat_view.get_model()).clear()
|
||||
self._transponder_view.get_model().clear()
|
||||
self._service_view.get_model().clear()
|
||||
|
||||
self.is_download = True
|
||||
self._sat_view.set_sensitive(False)
|
||||
src = self._source_box.get_active()
|
||||
if not self._parser:
|
||||
self._parser = SatellitesParser()
|
||||
@@ -555,6 +532,8 @@ class UpdateDialog:
|
||||
for sat in sats:
|
||||
model.append(sat)
|
||||
|
||||
self._sat_view.set_sensitive(True)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
if self.is_download:
|
||||
@@ -578,6 +557,9 @@ class UpdateDialog:
|
||||
def on_cancel_receive(self, item=None):
|
||||
self._download_task = False
|
||||
|
||||
def on_satellite_changed(self, box):
|
||||
self.on_update_satellites_list()
|
||||
|
||||
def on_satellite_toggled(self, toggle, path):
|
||||
model = self._sat_view.get_model()
|
||||
self.update_state(model, path, not toggle.get_active())
|
||||
@@ -629,15 +611,6 @@ class UpdateDialog:
|
||||
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
|
||||
return from_pos, to_pos
|
||||
|
||||
def on_search(self, entry):
|
||||
self._search_provider.search(entry.get_text())
|
||||
|
||||
def on_search_down(self, item):
|
||||
self._search_provider.on_search_down()
|
||||
|
||||
def on_search_up(self, item):
|
||||
self._search_provider.on_search_up()
|
||||
|
||||
def on_select_all(self, view):
|
||||
self.update_selection(view, True)
|
||||
|
||||
@@ -666,6 +639,7 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
super().__init__(transient=transient, settings=settings)
|
||||
|
||||
self._main_model = main_model
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
@@ -701,32 +675,29 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
sats.append(data)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send("Consumed: {:0.0f}s, {} satellites received.".format(time.time() - start, len(sats)))
|
||||
appender.close()
|
||||
sat_count = len(sats)
|
||||
|
||||
sats = {s[2]: s for s in sats} # key = position, v = satellite
|
||||
|
||||
for row in self._main_model:
|
||||
pos = row[-1]
|
||||
pos = row[2]
|
||||
if pos in sats:
|
||||
sat = sats.pop(pos)
|
||||
itr = row.iter
|
||||
self.update_satellite(itr, row, sat)
|
||||
appender.send(f"Updating satellite: {row[0]}\n")
|
||||
GLib.idle_add(self._main_model.set, row.iter, {i: v for i, v in enumerate(sat)})
|
||||
|
||||
for sat in sats.values():
|
||||
append_satellite(self._main_model, sat)
|
||||
for p, s in sats.items():
|
||||
appender.send(f"Adding satellite: {s.name}\n")
|
||||
self.append_satellite(s)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {sat_count} satellites received.\n")
|
||||
appender.close()
|
||||
self.is_download = False
|
||||
|
||||
@run_idle
|
||||
def update_satellite(self, itr, row, sat):
|
||||
if self._main_model.iter_has_child(itr):
|
||||
children = row.iterchildren()
|
||||
for ch in children:
|
||||
self._main_model.remove(ch.iter)
|
||||
|
||||
for tr in sat[3]:
|
||||
self._main_model.append(itr, ["Transponder:", *tr, None, None])
|
||||
def append_satellite(self, sat):
|
||||
self._main_model.append(sat)
|
||||
|
||||
|
||||
class ServicesUpdateDialog(UpdateDialog):
|
||||
@@ -741,11 +712,6 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self._services = {}
|
||||
self._selected_transponders = set()
|
||||
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
|
||||
|
||||
self._transponder_paned.set_visible(True)
|
||||
self._source_box.remove(0)
|
||||
self._source_box.remove(1)
|
||||
self._source_box.set_active(0)
|
||||
# Transponder view popup menu
|
||||
tr_popup_menu = Gtk.Menu()
|
||||
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
|
||||
@@ -762,6 +728,11 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
|
||||
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
|
||||
|
||||
self._transponder_frame.set_visible(True)
|
||||
self._source_box.remove(0)
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
self._source_box.set_active(0)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
if self.is_download:
|
||||
@@ -809,13 +780,13 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self.is_download = False
|
||||
return
|
||||
|
||||
appender.send("Getting transponders for: {}.\n".format(sat_names.get(futures[future])))
|
||||
appender.send(f"Getting transponders for: {sat_names.get(futures[future])}.\n")
|
||||
for t in future.result():
|
||||
t_urls.append(t.url)
|
||||
t_names[t.url] = t.text
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send("{} transponders received.\n\n".format(len(t_urls)))
|
||||
appender.send(f"{len(t_urls)} transponders received.\n\n")
|
||||
|
||||
non_cached_ts = []
|
||||
for tr in t_urls:
|
||||
@@ -831,11 +802,11 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self.is_download = False
|
||||
return
|
||||
|
||||
appender.send("Getting services for: {}.\n".format(t_names.get(futures[future], "")))
|
||||
appender.send(f"Getting services for: {t_names.get(futures[future], '')}.\n")
|
||||
list(map(services.append, future.result()))
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send("Consumed: {:0.0f}s, {} services received.".format(time.time() - start, len(services)))
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
|
||||
|
||||
try:
|
||||
from app.eparser.enigma.lamedb import LameDbReader
|
||||
@@ -843,7 +814,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
reader = LameDbReader(path=None)
|
||||
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
|
||||
except ValueError as e:
|
||||
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
|
||||
log(f"ServicesUpdateDialog [on receive data] error: {e}")
|
||||
else:
|
||||
self._callback(srvs)
|
||||
|
||||
@@ -851,7 +822,12 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sats = self._parser.get_satellites_list(SatelliteSource.LYNGSAT)
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
self._services_parser.source = sat_src
|
||||
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
if sats:
|
||||
callback(sats)
|
||||
self.is_download = False
|
||||
@@ -895,6 +871,9 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
|
||||
@run_task
|
||||
def on_activate_satellite(self, view, path, column):
|
||||
GLib.idle_add(self._transponder_view.get_model().clear)
|
||||
GLib.idle_add(self._service_view.get_model().clear)
|
||||
|
||||
model = view.get_model()
|
||||
itr = model.get_iter(path)
|
||||
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
|
||||
@@ -946,17 +925,5 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self.update_sat_state(m, s_path, select)
|
||||
|
||||
|
||||
# ************************* Commons ************************* #
|
||||
|
||||
|
||||
@run_idle
|
||||
def append_satellite(model, sat):
|
||||
""" Common function for append satellite to the model """
|
||||
name, flags, pos, transponders = sat
|
||||
parent = model.append(None, [name, *(None,) * 9, flags, pos])
|
||||
for transponder in transponders:
|
||||
model.append(parent, ["Transponder:", *transponder, None, None])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,31 +1,41 @@
|
||||
""" This is helper module for search features """
|
||||
from app.commons import run_with_delay
|
||||
|
||||
|
||||
class SearchProvider:
|
||||
def __init__(self, views, down_button, up_button):
|
||||
def __init__(self, view, entry, down_button, up_button, columns=None):
|
||||
self._paths = []
|
||||
self._current_index = -1
|
||||
self._max_indexes = 0
|
||||
self._views = views
|
||||
self._view = view
|
||||
self._entry = entry
|
||||
self._up_button = up_button
|
||||
self._down_button = down_button
|
||||
self._columns = columns
|
||||
|
||||
entry.connect("changed", self.on_search)
|
||||
self._down_button.connect("clicked", self.on_search_down)
|
||||
self._up_button.connect("clicked", self.on_search_up)
|
||||
|
||||
def search(self, text):
|
||||
self._current_index = -1
|
||||
self._paths.clear()
|
||||
for view in self._views:
|
||||
model = view.get_model()
|
||||
selection = view.get_selection()
|
||||
selection.unselect_all()
|
||||
if not text:
|
||||
continue
|
||||
model = self._view.get_model()
|
||||
selection = self._view.get_selection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
text = text.upper()
|
||||
for r in model:
|
||||
if text in str(r[:]).upper():
|
||||
path = r.path
|
||||
selection.select_path(r.path)
|
||||
self._paths.append((view, path))
|
||||
selection.unselect_all()
|
||||
if not text:
|
||||
return
|
||||
|
||||
text = text.upper()
|
||||
for r in model:
|
||||
data = [r[i] for i in self._columns] if self._columns else r[:]
|
||||
if next((s for s in data if text in str(s).upper()), False):
|
||||
path = r.path
|
||||
selection.select_path(r.path)
|
||||
self._paths.append(path)
|
||||
|
||||
self._max_indexes = len(self._paths) - 1
|
||||
if self._max_indexes > 0:
|
||||
@@ -34,16 +44,15 @@ class SearchProvider:
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def scroll_to(self, index):
|
||||
view, path = self._paths[index]
|
||||
view.scroll_to_cell(path, None)
|
||||
self._view.scroll_to_cell(self._paths[index], None)
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def on_search_down(self):
|
||||
def on_search_down(self, button=None):
|
||||
if self._current_index < self._max_indexes:
|
||||
self._current_index += 1
|
||||
self.scroll_to(self._current_index)
|
||||
|
||||
def on_search_up(self):
|
||||
def on_search_up(self, button=None):
|
||||
if self._current_index > -1:
|
||||
self._current_index -= 1
|
||||
self.scroll_to(self._current_index)
|
||||
@@ -52,6 +61,13 @@ class SearchProvider:
|
||||
self._up_button.set_sensitive(self._current_index > 0)
|
||||
self._down_button.set_sensitive(self._current_index < self._max_indexes)
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_search(self, entry):
|
||||
self.search(entry.get_text())
|
||||
|
||||
def on_search_toggled(self, action, value=None):
|
||||
self._entry.grab_focus() if action.get_active() else self._entry.set_text("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -40,37 +40,37 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">1/2</col>
|
||||
<col id="0">1/2</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">2/3</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/4</col>
|
||||
<col id="0">3/4</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">5/6</col>
|
||||
<col id="0">5/6</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">7/8</col>
|
||||
<col id="0">7/8</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">8/9</col>
|
||||
<col id="0">8/9</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/5</col>
|
||||
<col id="0">3/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">4/5</col>
|
||||
<col id="0">4/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">6/7</col>
|
||||
<col id="0">6/7</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">9/10</col>
|
||||
<col id="0">9/10</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -94,13 +94,13 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Off</col>
|
||||
<col id="0">Off</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">On</col>
|
||||
<col id="0">On</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -111,19 +111,19 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">QPSK</col>
|
||||
<col id="0">QPSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">8PSK</col>
|
||||
<col id="0">8PSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">16APSK</col>
|
||||
<col id="0">16APSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">32APSK</col>
|
||||
<col id="0">32APSK</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -134,13 +134,13 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Off</col>
|
||||
<col id="0">Off</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">On</col>
|
||||
<col id="0">On</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -151,13 +151,13 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Root</col>
|
||||
<col id="0">Root</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Gold</col>
|
||||
<col id="0">Gold</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Combo</col>
|
||||
<col id="0">Combo</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -168,16 +168,16 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">H</col>
|
||||
<col id="0">H</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">V</col>
|
||||
<col id="0">V</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">R</col>
|
||||
<col id="0">R</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">L</col>
|
||||
<col id="0">L</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -188,16 +188,16 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">35%</col>
|
||||
<col id="0">35%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">25%</col>
|
||||
<col id="0">25%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">20%</col>
|
||||
<col id="0">20%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -253,10 +253,10 @@ Author: Dmitriy Yefremov
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">DVB-S</col>
|
||||
<col id="0">DVB-S</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">DVB-S2</col>
|
||||
<col id="0">DVB-S2</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -272,45 +272,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Cancel</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label">gtk-save</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Save current service</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="create_button">
|
||||
<property name="label">gtk-new</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_create_new" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog_vbox">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -321,6 +285,55 @@ Author: Dmitriy Yefremov
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Save current service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="create_button">
|
||||
<property name="label" translatable="yes">Create</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_create_new" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -345,7 +358,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="srv_grid">
|
||||
<property name="visible">True</property>
|
||||
@@ -366,7 +379,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkEntry" id="name_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
@@ -388,7 +401,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkEntry" id="package_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@@ -412,7 +425,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">10</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -457,7 +470,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">7</property>
|
||||
<property name="max_width_chars">7</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="changed" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -807,10 +820,14 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="flags_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="flags_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="flags_label">
|
||||
@@ -883,6 +900,47 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="extra_flags_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="extra_pids_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Extra:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="extra_pids_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">20</property>
|
||||
<property name="max_width_chars">20</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">c:000000,etc.</property>
|
||||
<signal name="changed" handler="on_extra_pids_entry_changed" 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="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="caids_grid">
|
||||
<property name="visible">True</property>
|
||||
@@ -893,9 +951,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text">C:0000,C:a1b2,etc.</property>
|
||||
<property name="width_chars">15</property>
|
||||
<property name="max_width_chars">26</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="width_chars">20</property>
|
||||
<property name="max_width_chars">20</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property>
|
||||
<signal name="changed" handler="on_cas_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -920,7 +978,7 @@ Author: Dmitriy Yefremov
|
||||
<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>
|
||||
@@ -998,7 +1056,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1024,7 +1082,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1103,7 +1161,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -1141,7 +1199,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -1168,7 +1226,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -1377,7 +1435,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1403,7 +1461,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1429,7 +1487,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1558,7 +1616,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="stock">gtk-edit</property>
|
||||
<property name="icon_name">document-edit-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1586,9 +1644,6 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">cancel_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<object class="GtkListStore" id="transponder_services_liststore">
|
||||
<columns>
|
||||
@@ -1618,30 +1673,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="tr_services_no_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="tr_services_ok_button">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="tr_services_dialog_vbox">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -1650,6 +1684,35 @@ Author: Dmitriy Yefremov
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="tr_services_no_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="tr_services_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1676,7 +1739,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin_right">20</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Changes will be applied to all services of this transponder!
|
||||
<property name="label" translatable="yes">Changes will be applied to all services of this transponder!
|
||||
Continue?</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="lines">2</property>
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -6,11 +34,12 @@ from app.eparser import Service
|
||||
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
|
||||
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
|
||||
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
|
||||
HIERARCHY)
|
||||
HIERARCHY, A_MODULATION)
|
||||
from app.eparser.neutrino import get_attributes, SP, KSP
|
||||
from app.settings import SettingsType
|
||||
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
|
||||
from .dialogs import show_dialog, DialogType, Action, get_builder
|
||||
from .main_helper import get_base_model
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
|
||||
|
||||
@@ -24,8 +53,6 @@ class ServiceDetailsDialog:
|
||||
|
||||
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
|
||||
|
||||
_NEUTRINO_TRANSPONDER_DATA = "{:04x}:{:04x}:{}:{}:{}:{}:{}:{}:{}"
|
||||
|
||||
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
|
||||
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
|
||||
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
|
||||
@@ -42,22 +69,20 @@ class ServiceDetailsDialog:
|
||||
"on_tr_edit_toggled": self.on_tr_edit_toggled,
|
||||
"update_reference": self.update_reference,
|
||||
"on_cas_entry_changed": self.on_cas_entry_changed,
|
||||
"on_extra_pids_entry_changed": self.on_extra_pids_entry_changed,
|
||||
"on_digit_entry_changed": self.on_digit_entry_changed,
|
||||
"on_non_empty_entry_changed": self.on_non_empty_entry_changed,
|
||||
"on_cancel": lambda item: self._dialog.destroy()}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True)
|
||||
self._builder = builder
|
||||
|
||||
self._dialog = builder.get_object("service_details_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._s_type = settings.setting_type
|
||||
self._tr_type = TrType.Satellite
|
||||
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
|
||||
self._picons_dir_path = settings.picons_local_path
|
||||
self._satellites_xml_path = settings.profile_data_path + "satellites.xml"
|
||||
self._picons_path = settings.profile_picons_path
|
||||
self._services_view = srv_view
|
||||
self._fav_view = fav_view
|
||||
self._action = action
|
||||
@@ -72,6 +97,7 @@ class ServiceDetailsDialog:
|
||||
self._DIGIT_PATTERN = re.compile("\\D")
|
||||
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
|
||||
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
|
||||
# Buttons
|
||||
self._apply_button = builder.get_object("apply_button")
|
||||
self._create_button = builder.get_object("create_button")
|
||||
@@ -107,6 +133,7 @@ class ServiceDetailsDialog:
|
||||
self._stream_id_entry = self._digit_elements.get("stream_id_entry")
|
||||
self._tr_flag_entry = self._digit_elements.get("tr_flag_entry")
|
||||
self._namespace_entry = self._non_empty_elements.get("namespace_entry")
|
||||
self._extra_pids_entry = builder.get_object("extra_pids_entry")
|
||||
# Service elements
|
||||
self._name_entry = builder.get_object("name_entry")
|
||||
self._package_entry = builder.get_object("package_entry")
|
||||
@@ -195,8 +222,7 @@ class ServiceDetailsDialog:
|
||||
self._package_entry.set_text(srv.package)
|
||||
self._sid_entry.set_text(str(int(srv.ssid, 16)))
|
||||
# Transponder
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._tr_type = TrType(srv.transponder_type)
|
||||
self._tr_type = TrType(srv.transponder_type)
|
||||
self._freq_entry.set_text(srv.freq)
|
||||
self._rate_entry.set_text(srv.rate)
|
||||
self.select_active_text(self._pol_combo_box, srv.pol)
|
||||
@@ -206,6 +232,8 @@ class ServiceDetailsDialog:
|
||||
self.update_ui_for_terrestrial()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
self.update_ui_for_cable()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
self.update_ui_for_atsc()
|
||||
else:
|
||||
self.set_sat_positions(srv.pos)
|
||||
|
||||
@@ -245,6 +273,7 @@ class ServiceDetailsDialog:
|
||||
def init_enigma2_pids(self, flags):
|
||||
pids = list(filter(lambda x: x.startswith("c:"), flags))
|
||||
if pids:
|
||||
extra_pids = []
|
||||
for pid in pids:
|
||||
if pid.startswith(Pids.VIDEO.value):
|
||||
self._video_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
@@ -257,15 +286,19 @@ class ServiceDetailsDialog:
|
||||
elif pid.startswith(Pids.AC3.value):
|
||||
self._ac3_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.VIDEO_TYPE.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.BIT_STREAM_DELAY.value):
|
||||
self._bitstream_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.PCM_DELAY.value):
|
||||
self._pcm_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.SUBTITLE.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
else:
|
||||
extra_pids.append(pid)
|
||||
|
||||
self._extra_pids_entry.set_text(",".join(extra_pids))
|
||||
|
||||
def init_enigma2_transponder_data(self, srv):
|
||||
""" Transponder data initialisation """
|
||||
@@ -307,16 +340,23 @@ class ServiceDetailsDialog:
|
||||
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
|
||||
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
|
||||
elif tr_type is TrType.ATSC:
|
||||
self._sys_combo_box.set_active(0)
|
||||
self.select_active_text(self._mod_combo_box, A_MODULATION.get(tr_data[2]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[1]).name)
|
||||
|
||||
# Should be called last to properly initialize the reference
|
||||
self._srv_type_entry.set_text(data[4])
|
||||
|
||||
# ***************** Init Neutrino data *********************#
|
||||
|
||||
def init_neutrino_data(self, srv):
|
||||
tr_data = srv.transponder.split(":")
|
||||
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
|
||||
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
|
||||
if self._tr_type is not TrType.Satellite:
|
||||
return
|
||||
tr_data = get_attributes(srv.transponder)
|
||||
self._transponder_id_entry.set_text(str(int(tr_data.get("id", "0"), 16)))
|
||||
self._network_id_entry.set_text(str(int(tr_data.get("on", "0"), 16)))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data.get("inv", "2")).name)
|
||||
self.select_active_text(self._service_type_combo_box, srv.service_type)
|
||||
self.update_reference_entry()
|
||||
|
||||
@@ -328,6 +368,7 @@ class ServiceDetailsDialog:
|
||||
tr_grid.set_margin_bottom(5)
|
||||
self._builder.get_object("tr_extra_expander").set_visible(False)
|
||||
self._builder.get_object("srv_separator").set_visible(False)
|
||||
self._package_entry.set_sensitive(False)
|
||||
|
||||
# ***************** Init Sat positions *********************#
|
||||
|
||||
@@ -367,6 +408,10 @@ class ServiceDetailsDialog:
|
||||
self.save_data()
|
||||
|
||||
def save_data(self):
|
||||
if self._s_type is SettingsType.NEUTRINO_MP and self._tr_type is not TrType.Satellite:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
return
|
||||
|
||||
if not self.is_data_correct():
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
@@ -396,6 +441,8 @@ class ServiceDetailsDialog:
|
||||
transponder = self.get_terrestrial_transponder_data()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
transponder = self.get_cable_transponder_data()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
transponder = self.get_atsc_transponder_data()
|
||||
except Exception as e:
|
||||
log("Edit service error: {}".format(e))
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
|
||||
@@ -457,13 +504,13 @@ class ServiceDetailsDialog:
|
||||
Column.FAV_PICON: new_service.picon})
|
||||
|
||||
def update_picon_name(self, old_name, new_name):
|
||||
if not os.path.isdir(self._picons_dir_path):
|
||||
if not os.path.isdir(self._picons_path):
|
||||
return
|
||||
|
||||
for file_name in os.listdir(self._picons_dir_path):
|
||||
for file_name in os.listdir(self._picons_path):
|
||||
if file_name == old_name:
|
||||
old_file = os.path.join(self._picons_dir_path, old_name)
|
||||
new_file = os.path.join(self._picons_dir_path, new_name)
|
||||
old_file = os.path.join(self._picons_path, old_name)
|
||||
new_file = os.path.join(self._picons_path, new_name)
|
||||
os.rename(old_file, new_file)
|
||||
break
|
||||
|
||||
@@ -496,9 +543,9 @@ class ServiceDetailsDialog:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
return self.get_enigma2_flags()
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = self._old_service.flags_cas.split(":")
|
||||
flags[1] = self.get_sat_position()
|
||||
return ":".join(flags)
|
||||
flags = get_attributes(self._old_service.flags_cas)
|
||||
flags["position"] = self.get_sat_position()
|
||||
return SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
|
||||
|
||||
def get_enigma2_flags(self):
|
||||
flags = ["p:{}".format(self._package_entry.get_text())]
|
||||
@@ -528,6 +575,9 @@ class ServiceDetailsDialog:
|
||||
pcm_pid = self._pcm_entry.get_text()
|
||||
if pcm_pid:
|
||||
flags.append("{}{:04x}".format(Pids.PCM_DELAY.value, int(pcm_pid)))
|
||||
extra_pids = self._extra_pids_entry.get_text()
|
||||
if extra_pids:
|
||||
flags.append(extra_pids)
|
||||
# flags
|
||||
f_flags = Flag.KEEP.value if self._keep_check_button.get_active() else 0
|
||||
f_flags = f_flags + Flag.HIDE.value if self._hide_check_button.get_active() else f_flags
|
||||
@@ -549,10 +599,12 @@ class ServiceDetailsDialog:
|
||||
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
|
||||
return fav_id, data_id
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
data = get_attributes(self._old_service.data_id)
|
||||
data["n"] = self._name_entry.get_text()
|
||||
data["t"] = "{:x}".format(int(service_type))
|
||||
data["i"] = "{:04x}".format(ssid)
|
||||
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
|
||||
data_id = self._old_service.data_id.split(":")
|
||||
data_id[1] = "{:x}".format(int(service_type))
|
||||
return fav_id, ":".join(data_id)
|
||||
return fav_id, SP.join("{}{}{}".format(k, KSP, v) for k, v in data.items())
|
||||
|
||||
# ***************** Transponder ********************* #
|
||||
|
||||
@@ -560,6 +612,7 @@ class ServiceDetailsDialog:
|
||||
freq = self._freq_entry.get_text()
|
||||
fec = self._fec_combo_box.get_active_id()
|
||||
system = self._sys_combo_box.get_active_id()
|
||||
o_srv = self._old_service
|
||||
|
||||
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
|
||||
freq = self._freq_entry.get_text()
|
||||
@@ -567,11 +620,9 @@ class ServiceDetailsDialog:
|
||||
pol = self._pol_combo_box.get_active_id()
|
||||
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
|
||||
return freq, rate, pol, fec, system, pos
|
||||
elif self._tr_type is TrType.Terrestrial:
|
||||
o_srv = self._old_service
|
||||
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
|
||||
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
|
||||
elif self._tr_type is TrType.Cable:
|
||||
o_srv = self._old_service
|
||||
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
|
||||
|
||||
def get_satellite_transponder_data(self):
|
||||
@@ -598,12 +649,19 @@ class ServiceDetailsDialog:
|
||||
pls_code = self._pls_code_entry.get_text()
|
||||
st_id = self._stream_id_entry.get_text()
|
||||
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
|
||||
|
||||
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
|
||||
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None
|
||||
srv_sys = None
|
||||
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
|
||||
tr_data = get_attributes(self._old_service.transponder)
|
||||
tr_data["frq"] = freq
|
||||
tr_data["sr"] = rate
|
||||
tr_data["pol"] = pol
|
||||
tr_data["fec"] = fec
|
||||
tr_data["on"] = "{:04x}".format(int(self._network_id_entry.get_text()))
|
||||
tr_data["id"] = "{:04x}".format(int(self._transponder_id_entry.get_text()))
|
||||
tr_data["inv"] = inv
|
||||
|
||||
return SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_data.items())
|
||||
|
||||
def get_sat_position(self):
|
||||
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
|
||||
@@ -625,6 +683,7 @@ class ServiceDetailsDialog:
|
||||
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
|
||||
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def get_cable_transponder_data(self):
|
||||
@@ -636,6 +695,16 @@ class ServiceDetailsDialog:
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
|
||||
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def get_atsc_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, inversion, modulation, system
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def update_transponder_services(self, transponder, sat_pos):
|
||||
@@ -652,9 +721,9 @@ class ServiceDetailsDialog:
|
||||
continue
|
||||
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = srv[Column.SRV_CAS_FLAGS].split(":")
|
||||
flags[1] = sat_pos
|
||||
srv[Column.SRV_CAS_FLAGS] = ":".join(flags)
|
||||
flags = get_attributes(srv[Column.SRV_CAS_FLAGS])
|
||||
flags["position"] = sat_pos
|
||||
srv[Column.SRV_CAS_FLAGS] = SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
|
||||
|
||||
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
|
||||
self._current_model.set_row(itr, srv)
|
||||
@@ -677,6 +746,9 @@ class ServiceDetailsDialog:
|
||||
def on_cas_entry_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self._CAID_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
|
||||
|
||||
def on_extra_pids_entry_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self._PIDS_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
|
||||
|
||||
def get_value_from_combobox_id(self, box: Gtk.ComboBox, dc: dict):
|
||||
cb_id = box.get_active_id()
|
||||
return get_key_by_value(dc, cb_id)
|
||||
@@ -695,7 +767,7 @@ class ServiceDetailsDialog:
|
||||
return
|
||||
|
||||
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
|
||||
or self._old_service.transponder_type in "tc"))
|
||||
or self._old_service.transponder_type in "tca"))
|
||||
|
||||
for elem in self._TRANSPONDER_ELEMENTS:
|
||||
elem.set_sensitive(active)
|
||||
@@ -709,6 +781,8 @@ class ServiceDetailsDialog:
|
||||
return False
|
||||
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
if self._extra_pids_entry.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_reference(self, entry, event=None):
|
||||
@@ -826,6 +900,18 @@ class ServiceDetailsDialog:
|
||||
self._transponder_id_entry.set_max_width_chars(8)
|
||||
self._network_id_entry.set_max_width_chars(8)
|
||||
|
||||
def update_ui_for_atsc(self):
|
||||
self.update_ui_for_cable()
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(1)
|
||||
tr_grid.remove_column(1)
|
||||
# Init models
|
||||
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
|
||||
system_model.append((TrType.ATSC.name,))
|
||||
[modulation_model.append((v,)) for k, v in A_MODULATION.items()]
|
||||
# Extra
|
||||
self._namespace_entry.set_max_width_chars(25)
|
||||
|
||||
def get_transponder_grid_for_non_satellite(self):
|
||||
self._pids_grid.set_visible(False)
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
@@ -844,10 +930,7 @@ class ServiceDetailsDialog:
|
||||
|
||||
class TransponderServicesDialog:
|
||||
def __init__(self, transient, services_view, transponder, tr_iters):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("tr_services_dialog", "transponder_services_liststore"))
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("tr_services_dialog", "transponder_services_liststore"))
|
||||
self._dialog = builder.get_object("tr_services_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._srv_model = builder.get_object("transponder_services_liststore")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog
|
||||
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
|
||||
|
||||
@@ -23,7 +51,6 @@ class SettingsDialog:
|
||||
"on_reset": self.on_reset,
|
||||
"on_response": self.on_response,
|
||||
"apply_settings": self.apply_settings,
|
||||
"on_apply_profile_settings": self.on_apply_profile_settings,
|
||||
"on_connection_test": self.on_connection_test,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_set_color_switch": self.on_set_color_switch,
|
||||
@@ -32,7 +59,6 @@ class SettingsDialog:
|
||||
"on_experimental_switch": self.on_experimental_switch,
|
||||
"on_yt_dl_switch": self.on_yt_dl_switch,
|
||||
"on_default_path_mode_switch": self.on_default_path_mode_switch,
|
||||
"on_default_data_path_changed": self.on_default_data_path_changed,
|
||||
"on_profile_add": self.on_profile_add,
|
||||
"on_profile_edit": self.on_profile_edit,
|
||||
"on_profile_remove": self.on_profile_remove,
|
||||
@@ -41,6 +67,8 @@ class SettingsDialog:
|
||||
"on_profile_edited": self.on_profile_edited,
|
||||
"on_profile_selected": self.on_profile_selected,
|
||||
"on_profile_set_default": self.on_profile_set_default,
|
||||
"on_add_picon_path": self.on_add_picon_path,
|
||||
"on_remove_picon_path": self.on_remove_picon_path,
|
||||
"on_lang_changed": self.on_lang_changed,
|
||||
"on_main_settings_visible": self.on_main_settings_visible,
|
||||
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
|
||||
@@ -64,15 +92,14 @@ class SettingsDialog:
|
||||
self._profiles = self._settings.profiles
|
||||
self._s_type = self._settings.setting_type
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "settings_dialog.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("settings_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._header_bar = builder.get_object("header_bar")
|
||||
self._dialog.set_border_width(0)
|
||||
self._dialog.set_margin_left(0)
|
||||
self._main_stack = builder.get_object("main_stack")
|
||||
# Network
|
||||
# Network.
|
||||
self._host_field = builder.get_object("host_field")
|
||||
self._port_field = builder.get_object("port_field")
|
||||
self._login_field = builder.get_object("login_field")
|
||||
@@ -81,31 +108,34 @@ class SettingsDialog:
|
||||
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
|
||||
self._telnet_port_field = builder.get_object("telnet_port_field")
|
||||
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
|
||||
# Test
|
||||
self._reset_button = builder.get_object("reset_button")
|
||||
# Test.
|
||||
self._ftp_radio_button = builder.get_object("ftp_radio_button")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
# Paths
|
||||
# Network paths.
|
||||
self._services_field = builder.get_object("services_field")
|
||||
self._user_bouquet_field = builder.get_object("user_bouquet_field")
|
||||
self._satellites_xml_field = builder.get_object("satellites_xml_field")
|
||||
self._data_dir_field = builder.get_object("data_dir_field")
|
||||
self._picons_field = builder.get_object("picons_field")
|
||||
self._picons_dir_field = builder.get_object("picons_dir_field")
|
||||
self._backup_dir_field = builder.get_object("backup_dir_field")
|
||||
self._default_data_dir_field = builder.get_object("default_data_dir_field")
|
||||
self._record_data_dir_field = builder.get_object("record_data_dir_field")
|
||||
self._picons_paths_box = builder.get_object("picons_paths_box")
|
||||
self._remove_picon_path_button = builder.get_object("remove_picon_path_button")
|
||||
# Paths.
|
||||
self._picons_path_field = builder.get_object("picons_path_field")
|
||||
self._data_path_field = builder.get_object("data_path_field")
|
||||
self._backup_path_field = builder.get_object("backup_path_field")
|
||||
self._record_data_path_field = builder.get_object("record_data_path_field")
|
||||
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
|
||||
# Info bar
|
||||
self._default_data_paths_switch.bind_property("active", self._backup_path_field, "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", self._picons_path_field, "sensitive", 4)
|
||||
# Info bar.
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._test_spinner = builder.get_object("test_spinner")
|
||||
# Settings type
|
||||
# Settings type.
|
||||
self._enigma_radio_button = builder.get_object("enigma_radio_button")
|
||||
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
|
||||
self._support_ver5_switch = builder.get_object("support_ver5_switch")
|
||||
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
|
||||
# Streaming
|
||||
header_separator = builder.get_object("header_separator")
|
||||
self._apply_presets_button = builder.get_object("apply_presets_button")
|
||||
self._transcoding_switch = builder.get_object("transcoding_switch")
|
||||
self._edit_preset_switch = builder.get_object("edit_preset_switch")
|
||||
@@ -117,7 +147,6 @@ class SettingsDialog:
|
||||
self._audio_channels_combo_box = builder.get_object("audio_channels_combo_box")
|
||||
self._audio_sample_rate_combo_box = builder.get_object("audio_sample_rate_combo_box")
|
||||
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
|
||||
self._apply_presets_button.bind_property("visible", header_separator, "visible")
|
||||
self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
|
||||
@@ -128,14 +157,14 @@ class SettingsDialog:
|
||||
self._gst_lib_button = builder.get_object("gst_lib_button")
|
||||
self._vlc_lib_button = builder.get_object("vlc_lib_button")
|
||||
self._mpv_lib_button = builder.get_object("mpv_lib_button")
|
||||
# Program
|
||||
# Program.
|
||||
self._before_save_switch = builder.get_object("before_save_switch")
|
||||
self._before_downloading_switch = builder.get_object("before_downloading_switch")
|
||||
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
|
||||
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
|
||||
self._services_hints_switch = builder.get_object("services_hints_switch")
|
||||
self._lang_combo_box = builder.get_object("lang_combo_box")
|
||||
# Appearance
|
||||
# Appearance.
|
||||
self._list_font_button = builder.get_object("list_font_button")
|
||||
self._picons_size_button = builder.get_object("picons_size_button")
|
||||
self._tooltip_logo_size_button = builder.get_object("tooltip_logo_size_button")
|
||||
@@ -143,7 +172,7 @@ class SettingsDialog:
|
||||
self._set_color_switch = builder.get_object("set_color_switch")
|
||||
self._new_color_button = builder.get_object("new_color_button")
|
||||
self._extra_color_button = builder.get_object("extra_color_button")
|
||||
# Extra
|
||||
# Extra.
|
||||
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")
|
||||
@@ -155,25 +184,23 @@ class SettingsDialog:
|
||||
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
|
||||
# EXPERIMENTAL
|
||||
# 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
|
||||
# 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("enable_http_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("enable_experimental_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
|
||||
# Profiles
|
||||
# Profiles.
|
||||
self._profile_view = builder.get_object("profile_tree_view")
|
||||
self._profile_add_button = builder.get_object("profile_add_button")
|
||||
self._profile_remove_button = builder.get_object("profile_remove_button")
|
||||
self._apply_profile_button = builder.get_object("apply_profile_button")
|
||||
self._apply_profile_button.bind_property("visible", header_separator, "visible")
|
||||
# Style
|
||||
# Style.
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
|
||||
@@ -181,13 +208,14 @@ class SettingsDialog:
|
||||
for el in self._digit_elems:
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self.init_ui_elements(self._s_type)
|
||||
self.init_ui_elements()
|
||||
self.init_profiles()
|
||||
|
||||
if self._settings.is_darwin:
|
||||
# Themes
|
||||
if not IS_LINUX:
|
||||
# Themes.
|
||||
builder.get_object("style_frame").set_visible(IS_WIN)
|
||||
builder.get_object("themes_support_frame").set_visible(True)
|
||||
self._layout_switch = builder.get_object("layout_switch")
|
||||
self._layout_switch.bind_property("active", builder.get_object("bouquet_box"), "sensitive")
|
||||
self._layout_switch.set_active(self._ext_settings.alternate_layout)
|
||||
self._theme_frame = builder.get_object("theme_frame")
|
||||
self._theme_frame.set_visible(True)
|
||||
@@ -195,15 +223,16 @@ class SettingsDialog:
|
||||
self._theme_combo_box = builder.get_object("theme_combo_box")
|
||||
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
|
||||
self._dark_mode_switch = builder.get_object("dark_mode_switch")
|
||||
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
|
||||
self._themes_support_switch = builder.get_object("themes_support_switch")
|
||||
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
|
||||
self.init_themes()
|
||||
|
||||
@run_idle
|
||||
def init_ui_elements(self, s_type):
|
||||
is_enigma_profile = s_type is SettingsType.ENIGMA_2
|
||||
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_header_bar()
|
||||
def init_ui_elements(self):
|
||||
is_enigma_profile = self._s_type is SettingsType.ENIGMA_2
|
||||
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_picon_paths()
|
||||
self.update_title()
|
||||
http_active = self._support_http_api_switch.get_active()
|
||||
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
|
||||
self._lang_combo_box.set_active_id(self._ext_settings.language)
|
||||
@@ -221,12 +250,21 @@ class SettingsDialog:
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
|
||||
|
||||
def update_header_bar(self):
|
||||
label, sep, st = self._header_bar.get_subtitle().partition(":")
|
||||
def update_title(self):
|
||||
title = "{} [{}]"
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._header_bar.set_subtitle("{}: {}".format(label, self._enigma_radio_button.get_label()))
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label()))
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
|
||||
|
||||
def update_picon_paths(self):
|
||||
model = self._picons_paths_box.get_model()
|
||||
model.clear()
|
||||
list(map(lambda p: model.append((p, p)), self._settings.picons_paths))
|
||||
if self._settings.picons_path in self._settings.picons_paths:
|
||||
self._picons_paths_box.set_active_id(self._settings.picons_path)
|
||||
else:
|
||||
self._picons_paths_box.set_active(0)
|
||||
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
@@ -247,7 +285,7 @@ class SettingsDialog:
|
||||
self._settings.setting_type = s_type
|
||||
self._s_type = s_type
|
||||
self.on_reset()
|
||||
self.init_ui_elements(s_type)
|
||||
self.init_ui_elements()
|
||||
|
||||
def on_reset(self, item=None):
|
||||
self._settings.reset()
|
||||
@@ -266,12 +304,11 @@ class SettingsDialog:
|
||||
self._services_field.set_text(self._settings.services_path)
|
||||
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
|
||||
self._satellites_xml_field.set_text(self._settings.satellites_xml_path)
|
||||
self._picons_field.set_text(self._settings.picons_path)
|
||||
self._data_dir_field.set_text(self._settings.data_local_path)
|
||||
self._picons_dir_field.set_text(self._settings.picons_local_path)
|
||||
self._backup_dir_field.set_text(self._settings.backup_local_path)
|
||||
self._default_data_dir_field.set_text(self._settings.default_data_path)
|
||||
self._record_data_dir_field.set_text(self._settings.records_path)
|
||||
self._picons_paths_box.set_active_id(self._settings.picons_path)
|
||||
self._data_path_field.set_text(self._settings.default_data_path)
|
||||
self._picons_path_field.set_text(self._settings.default_picon_path)
|
||||
self._backup_path_field.set_text(self._settings.default_backup_path)
|
||||
self._record_data_path_field.set_text(self._settings.records_path)
|
||||
self._before_save_switch.set_active(self._settings.backup_before_save)
|
||||
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
|
||||
self.set_fav_click_mode(self._settings.fav_click_mode)
|
||||
@@ -327,10 +364,7 @@ class SettingsDialog:
|
||||
self._settings.services_path = self._services_field.get_text()
|
||||
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
|
||||
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
|
||||
self._settings.picons_path = self._picons_field.get_text()
|
||||
self._settings.data_local_path = self._data_dir_field.get_text()
|
||||
self._settings.picons_local_path = self._picons_dir_field.get_text()
|
||||
self._settings.backup_local_path = self._backup_dir_field.get_text()
|
||||
self._settings.picons_path = self._picons_paths_box.get_active_id()
|
||||
|
||||
def apply_settings(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
|
||||
@@ -348,15 +382,17 @@ class SettingsDialog:
|
||||
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
|
||||
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
|
||||
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
|
||||
self._ext_settings.default_data_path = self._default_data_dir_field.get_text()
|
||||
self._ext_settings.records_path = self._record_data_dir_field.get_text()
|
||||
self._ext_settings.default_data_path = self._data_path_field.get_text()
|
||||
self._ext_settings.default_backup_path = self._backup_path_field.get_text()
|
||||
self._ext_settings.default_picon_path = self._picons_path_field.get_text()
|
||||
self._ext_settings.records_path = self._record_data_path_field.get_text()
|
||||
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
|
||||
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
|
||||
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
|
||||
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
|
||||
self._ext_settings.list_font = self._list_font_button.get_font()
|
||||
|
||||
if self._ext_settings.is_darwin:
|
||||
if not IS_LINUX:
|
||||
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
|
||||
self._ext_settings.alternate_layout = self._layout_switch.get_active()
|
||||
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
|
||||
@@ -396,7 +432,8 @@ class SettingsDialog:
|
||||
host, port = self._host_field.get_text(), self._http_port_field.get_text()
|
||||
use_ssl = self._http_use_ssl_check_button.get_active()
|
||||
try:
|
||||
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO)
|
||||
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl, s_type=self._s_type),
|
||||
Gtk.MessageType.INFO)
|
||||
except TestException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
except HttpApiException as e:
|
||||
@@ -419,7 +456,7 @@ class SettingsDialog:
|
||||
host, port = self._host_field.get_text(), self._port_field.get_text()
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
try:
|
||||
self.show_info_message("OK. {}".format(test_ftp(host, port, user, password)), Gtk.MessageType.INFO)
|
||||
self.show_info_message(f"OK. {test_ftp(host, port, user, password)}", Gtk.MessageType.INFO)
|
||||
except TestException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
@@ -427,9 +464,10 @@ class SettingsDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_visible(False)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(get_message(text))
|
||||
self._info_bar.set_visible(True)
|
||||
|
||||
@run_idle
|
||||
def show_spinner(self, show):
|
||||
@@ -471,9 +509,6 @@ class SettingsDialog:
|
||||
def on_default_path_mode_switch(self, switch, state):
|
||||
self._settings.profile_folder_is_default = state
|
||||
|
||||
def on_default_data_path_changed(self, entry):
|
||||
self._settings.default_data_path = entry.get_text()
|
||||
|
||||
def on_profile_add(self, item):
|
||||
model = self._profile_view.get_model()
|
||||
count = 0
|
||||
@@ -520,29 +555,8 @@ class SettingsDialog:
|
||||
if p_settings:
|
||||
row[0] = new_value
|
||||
self._profiles[new_value] = p_settings
|
||||
self.update_local_paths(new_value, old_name)
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
|
||||
def update_local_paths(self, p_name, old_name, force_rename=False):
|
||||
data_path = self._settings.data_local_path
|
||||
picons_path = self._settings.picons_local_path
|
||||
backup_path = self._settings.backup_local_path
|
||||
|
||||
self._settings.data_local_path = p_name.join(data_path.rsplit(old_name, 1))
|
||||
self._settings.picons_local_path = p_name.join(picons_path.rsplit(old_name, 1))
|
||||
self._settings.backup_local_path = p_name.join(backup_path.rsplit(old_name, 1))
|
||||
|
||||
if force_rename:
|
||||
try:
|
||||
if os.path.isdir(picons_path):
|
||||
os.rename(picons_path, self._settings.picons_local_path)
|
||||
if os.path.isdir(data_path):
|
||||
os.rename(data_path, self._settings.data_local_path)
|
||||
if os.path.isdir(backup_path):
|
||||
os.rename(backup_path, self._settings.backup_local_path)
|
||||
except OSError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_profile_selected(self, view, force=True):
|
||||
if force:
|
||||
self.on_apply_profile_settings()
|
||||
@@ -564,14 +578,43 @@ class SettingsDialog:
|
||||
def on_profile_inserted(self, model, path, itr):
|
||||
self._profile_remove_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_add_picon_path(self, button):
|
||||
response = show_dialog(DialogType.INPUT, self._dialog, self._settings.picons_path)
|
||||
if response is Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if response 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)
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_remove_picon_path(self, button):
|
||||
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{'Are you sure?'}"
|
||||
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model = self._picons_paths_box.get_model()
|
||||
active = self._picons_paths_box.get_active_iter()
|
||||
if active:
|
||||
model.remove(active)
|
||||
|
||||
self._picons_paths_box.set_active(0)
|
||||
self._remove_picon_path_button.set_sensitive(len(model) > 1)
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_lang_changed(self, box):
|
||||
if box.get_active_id() != self._settings.language:
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_main_settings_visible(self, stack, param):
|
||||
name = stack.get_visible_child_name()
|
||||
self._apply_profile_button.set_visible(name == "profiles")
|
||||
self._apply_presets_button.set_visible(name == "streaming")
|
||||
self._reset_button.set_visible(name == "profiles")
|
||||
|
||||
def on_http_use_ssl_toggled(self, button):
|
||||
active = button.get_active()
|
||||
@@ -625,7 +668,6 @@ class SettingsDialog:
|
||||
|
||||
@run_idle
|
||||
def set_play_stream_mode(self, mode):
|
||||
self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin)
|
||||
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
|
||||
self._play_in_window_radio_button.set_active(mode is PlayStreamsMode.WINDOW)
|
||||
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
|
||||
@@ -713,7 +755,7 @@ class SettingsDialog:
|
||||
|
||||
@run_idle
|
||||
def set_theme_thumbnail_image(self, theme_name):
|
||||
img_path = "{}{}/gtk-3.0/thumbnail.png".format(self._ext_settings.themes_path, theme_name)
|
||||
img_path = "{}{}{}gtk-3.0{}thumbnail.png".format(self._ext_settings.themes_path, theme_name, SEP, SEP)
|
||||
self._theme_thumbnail_image.set_from_pixbuf(get_picon_pixbuf(img_path, 96))
|
||||
|
||||
def on_theme_add(self, button):
|
||||
@@ -796,7 +838,6 @@ class SettingsDialog:
|
||||
|
||||
@run_idle
|
||||
def init_themes(self):
|
||||
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
|
||||
t_support = self._ext_settings.is_themes_support
|
||||
self._themes_support_switch.set_active(t_support)
|
||||
if t_support:
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
}
|
||||
|
||||
#status-bar-button {
|
||||
padding: 1px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
@@ -55,4 +58,15 @@ paned > separator {
|
||||
border-radius: 0;
|
||||
border-left-width: 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.stack-switcher > button > label {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.stack-switcher > button.text-button {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
183
app/ui/telnet.glade
Normal file
183
app/ui/telnet.glade
Normal file
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkTextTagTable" id="tag_table">
|
||||
<child type="tag">
|
||||
<object class="GtkTextTag" id="end_tag">
|
||||
<property name="font">Normal</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkTextBuffer" id="text_buffer">
|
||||
<property name="tag_table">tag_table</property>
|
||||
</object>
|
||||
<object class="GtkFrame" id="telnet_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="telnet_main_box">
|
||||
<property name="width_request">480</property>
|
||||
<property name="height_request">180</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="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="commands_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="name">textview-large</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wrap_mode">char</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="buffer">text_buffer</property>
|
||||
<property name="overwrite">True</property>
|
||||
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
<property name="monospace">True</property>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Telnet</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
224
app/ui/telnet.py
Normal file
224
app/ui/telnet.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import re
|
||||
import selectors
|
||||
import socket
|
||||
from collections import deque
|
||||
from telnetlib import Telnet
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
|
||||
|
||||
class ExtTelnet(Telnet):
|
||||
|
||||
def __init__(self, output_callback, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._output_callback = output_callback
|
||||
|
||||
def interact(self):
|
||||
""" Interaction function, emulates a very dumb telnet client. """
|
||||
with selectors.DefaultSelector() as selector:
|
||||
selector.register(self, selectors.EVENT_READ)
|
||||
|
||||
while True:
|
||||
for key, events in selector.select():
|
||||
if key.fileobj is self:
|
||||
try:
|
||||
text = self.read_very_eager()
|
||||
except EOFError as e:
|
||||
msg = "\n*** Connection closed by remote host ***\n"
|
||||
self._output_callback(msg)
|
||||
log(msg)
|
||||
raise e
|
||||
else:
|
||||
if text:
|
||||
self._output_callback(text)
|
||||
|
||||
|
||||
class TelnetClient(Gtk.Box):
|
||||
""" Very simple telnet client. """
|
||||
_COLOR_PATTERN = re.compile("\x1b.*?m") # Color info
|
||||
_ERASING_PATTERN = re.compile("\x1b.*?K") # Erase to right
|
||||
_APP_MODE_PATTERN = re.compile("\x1b.*?(1h)|(1l)") # h - on, l - off
|
||||
_ALL_PATTERN = re.compile(r'(\x1b\[|\x9b)[0-?]*[@-~]')
|
||||
_NOT_SUPPORTED = {"mc", "mcedit", "vi", "nano"}
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._app = app
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
|
||||
self._tn = None
|
||||
self._app_mode = False
|
||||
self._commands = deque(maxlen=10)
|
||||
|
||||
self._handlers = {"on_clear": self.on_clear,
|
||||
"on_text_view_realize": self.on_text_view_realize,
|
||||
"on_view_key_press": self.on_view_key_press,
|
||||
"on_connect": self.on_connect,
|
||||
"on_disconnect": self.on_disconnect}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "telnet.glade")
|
||||
builder.connect_signals(self._handlers)
|
||||
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._buf = builder.get_object("text_buffer")
|
||||
self._end_tag = builder.get_object("end_tag")
|
||||
self._connect_button = builder.get_object("connect_button")
|
||||
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
|
||||
|
||||
main_frame = builder.get_object("telnet_frame")
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
main_frame.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
self.pack_start(main_frame, True, True, 0)
|
||||
self.show()
|
||||
|
||||
def on_profile_changed(self, app, data):
|
||||
self.on_clear()
|
||||
self.on_disconnect()
|
||||
self.on_connect()
|
||||
|
||||
def on_text_view_realize(self, view):
|
||||
self.on_connect()
|
||||
|
||||
@run_task
|
||||
def on_connect(self, item=None):
|
||||
try:
|
||||
GLib.idle_add(self._connect_button.set_visible, False)
|
||||
settings = self._app.app_settings
|
||||
user, password, timeout = settings.user, settings.password, settings.telnet_timeout
|
||||
self._tn = ExtTelnet(self.append_output, host=settings.host, port=settings.telnet_port, timeout=timeout)
|
||||
|
||||
if user != "":
|
||||
self._tn.read_until(b"login: ")
|
||||
self._tn.write(user.encode("utf-8") + b"\n")
|
||||
if password != "":
|
||||
self._tn.read_until(b"Password: ")
|
||||
self._tn.write(password.encode("utf-8") + b"\n")
|
||||
|
||||
self._tn.interact()
|
||||
except (OSError, EOFError, socket.timeout, ConnectionRefusedError) as e:
|
||||
log(f"{self.__class__.__name__}: {e}")
|
||||
self._app.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
GLib.idle_add(self._connect_button.set_visible, True)
|
||||
|
||||
@run_task
|
||||
def on_disconnect(self, item=None):
|
||||
if self._tn:
|
||||
GLib.idle_add(self._connect_button.set_visible, True)
|
||||
self._tn.close()
|
||||
|
||||
def on_command_done(self, entry):
|
||||
command = entry.get_text()
|
||||
entry.set_text("")
|
||||
if command and self._tn:
|
||||
self._tn.write(command.encode("ascii") + b"\r")
|
||||
|
||||
def on_clear(self, item=None):
|
||||
self._buf.delete(self._buf.get_start_iter(), self._buf.get_end_iter())
|
||||
|
||||
def on_view_key_press(self, view, event):
|
||||
""" Handling keystrokes on press. """
|
||||
if event.keyval == Gdk.KEY_Return:
|
||||
self.do_command()
|
||||
return True
|
||||
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
if ctrl and key is KeyboardKey.C:
|
||||
if self._tn and self._tn.sock:
|
||||
self._tn.write(b"\x03") # interrupt
|
||||
|
||||
# Last commands navigation.
|
||||
if key is KeyboardKey.UP:
|
||||
self.delete_last_command()
|
||||
if self._commands:
|
||||
cmd = self._commands.pop()
|
||||
self._commands.appendleft(cmd)
|
||||
self._buf.insert_at_cursor(cmd, -1)
|
||||
return True
|
||||
elif key is KeyboardKey.DOWN:
|
||||
self.delete_last_command()
|
||||
if self._commands:
|
||||
cmd = self._commands.popleft()
|
||||
self._commands.append(cmd)
|
||||
self._buf.insert_at_cursor(cmd, -1)
|
||||
return True
|
||||
|
||||
def delete_last_command(self):
|
||||
end = self._buf.get_end_iter()
|
||||
if end.ends_tag(self._end_tag):
|
||||
return
|
||||
|
||||
if end.backward_to_tag_toggle(self._end_tag):
|
||||
self._buf.delete(self._buf.get_end_iter(), end)
|
||||
|
||||
def do_command(self):
|
||||
count = self._buf.get_line_count()
|
||||
begin = self._buf.get_iter_at_line(count)
|
||||
end = self._buf.get_end_iter()
|
||||
command = []
|
||||
|
||||
while end.backward_to_tag_toggle(self._end_tag):
|
||||
command.append(self._buf.get_text(end, begin, False))
|
||||
break
|
||||
else: # if buf is empty
|
||||
command.append(self._buf.get_text(begin, end, False))
|
||||
|
||||
# To preventing duplication of the command in the buf.
|
||||
self._buf.delete(end, begin)
|
||||
|
||||
if command and self._tn.sock:
|
||||
cmd = command[0]
|
||||
if cmd in self._NOT_SUPPORTED:
|
||||
self._app.show_info_message(f"'{cmd}' is not supported by this client.", Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self._tn.write(cmd.encode("ascii") + b"\r")
|
||||
self._commands.append(cmd)
|
||||
|
||||
@run_idle
|
||||
def append_output(self, txt):
|
||||
t = txt.decode("ascii", errors="ignore")
|
||||
|
||||
ap = re.search(self._APP_MODE_PATTERN, t)
|
||||
if ap:
|
||||
on, of = ap.group(1), ap.group(2)
|
||||
if on:
|
||||
self._app_mode = True
|
||||
elif of:
|
||||
self._app_mode = False
|
||||
self.on_clear()
|
||||
|
||||
t = re.sub(self._ALL_PATTERN, "", t) # Removing [replacing] ascii escape sequences.
|
||||
|
||||
if self._app_mode:
|
||||
start, end = self._buf.get_start_iter(), self._buf.get_end_iter()
|
||||
count = self._buf.get_line_count()
|
||||
new_lines = t.split("\r\n")
|
||||
ext_lines = self._buf.get_text(start, end, True).split("\r\n")
|
||||
if count < len(new_lines):
|
||||
self._buf.set_text(re.sub(self._ERASING_PATTERN, "", t))
|
||||
else:
|
||||
for i, line in enumerate(new_lines):
|
||||
if line:
|
||||
ext_lines[i] = re.sub(self._ERASING_PATTERN, "", line)
|
||||
self._buf.set_text("\r\n".join(ext_lines))
|
||||
else:
|
||||
self._buf.insert_at_cursor(t, -1)
|
||||
|
||||
insert = self._buf.get_insert()
|
||||
self._text_view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
|
||||
self._buf.apply_tag(self._end_tag, self._buf.get_start_iter(), self._buf.get_end_iter())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,141 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2 -->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<object class="GtkBox" id="timer_row_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">2</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="timer_name_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="timer_name_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="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="timer_description_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="timer_description_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<attributes>
|
||||
<attribute name="style" value="italic"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</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="GtkBox" id="timer_service_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="timer_service_name_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="timer_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<attributes>
|
||||
<attribute name="size" value="8000"/>
|
||||
</attributes>
|
||||
</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">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator" id="timer_row_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -7,6 +7,7 @@ from gi.repository import GLib
|
||||
from app.commons import log
|
||||
from app.connections import HttpAPI
|
||||
from app.tools.yt import YouTube
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.iptv import get_yt_icon
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
|
||||
@@ -34,9 +35,7 @@ class LinksTransmitter:
|
||||
self._app_window = app_window
|
||||
self._is_status_icon = True
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "transmitter.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "transmitter.glade", handlers)
|
||||
|
||||
self._main_window = builder.get_object("main_window")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
|
||||
@@ -7,21 +7,24 @@ import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Notify", "0.7")
|
||||
from gi.repository import Gtk, Gdk, Notify
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
|
||||
|
||||
# Init notify
|
||||
Notify.init("DemonEditor")
|
||||
# Setting mod mask for the keyboard depending on the platform.
|
||||
# Setting mod mask for keyboard depending on platform
|
||||
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
|
||||
# Paths.
|
||||
BASE_PATH = "app/ui/"
|
||||
EX_PATH = "/usr/share/demoneditor/app/ui/" if IS_LINUX else "ui/"
|
||||
# Path to *.glade files.
|
||||
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
|
||||
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
|
||||
UI_RESOURCES_PATH = BASE_PATH if os.path.exists(BASE_PATH) else EX_PATH
|
||||
# Translation.
|
||||
LANG_PATH = UI_RESOURCES_PATH + "lang"
|
||||
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()
|
||||
@@ -29,18 +32,48 @@ except SettingsException:
|
||||
pass
|
||||
else:
|
||||
os.environ["LANGUAGE"] = settings.language
|
||||
if UI_RESOURCES_PATH == "app/ui/":
|
||||
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang")
|
||||
|
||||
st = Gtk.Settings().get_default()
|
||||
APP_FONT = st.get_property("gtk-font-name")
|
||||
if not settings.list_font:
|
||||
settings.list_font = APP_FONT
|
||||
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
|
||||
|
||||
if settings.is_themes_support:
|
||||
st.set_property("gtk-theme-name", settings.theme)
|
||||
st.set_property("gtk-icon-theme-name", settings.icon_theme)
|
||||
else:
|
||||
if not IS_LINUX:
|
||||
if IS_DARWIN:
|
||||
s_path = f"{GTK_PATH + '/' + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH}mac_style.css"
|
||||
else:
|
||||
s_path = f"{UI_RESOURCES_PATH}win_style.css"
|
||||
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(s_path)
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
|
||||
if IS_LINUX:
|
||||
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# Init notify
|
||||
try:
|
||||
gi.require_version("Notify", "0.7")
|
||||
from gi.repository import Notify
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
NOTIFY_IS_INIT = Notify.init("DemonEditor")
|
||||
elif IS_DARWIN:
|
||||
import gettext
|
||||
|
||||
if GTK_PATH:
|
||||
LANG_PATH = GTK_PATH + "/share/locale"
|
||||
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# For launching from the bundle.
|
||||
if os.getcwd() == "/" and GTK_PATH:
|
||||
os.chdir(GTK_PATH)
|
||||
else:
|
||||
locale.setlocale(locale.LC_NUMERIC, "C")
|
||||
|
||||
# Icons.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(UI_RESOURCES_PATH + "icons")
|
||||
|
||||
@@ -74,7 +107,7 @@ def get_yt_icon(icon_name, size=24):
|
||||
if n_theme.has_icon(icon_name):
|
||||
return n_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
return default_theme.load_icon("info", size, 0)
|
||||
return default_theme.load_icon("emblem-important-symbolic", size, 0)
|
||||
|
||||
|
||||
def show_notification(message, timeout=10000, urgency=1):
|
||||
@@ -84,55 +117,27 @@ def show_notification(message, timeout=10000, urgency=1):
|
||||
@param timeout: milliseconds
|
||||
@param urgency: 0 - low, 1 - normal, 2 - critical
|
||||
"""
|
||||
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
|
||||
notify.set_urgency(urgency)
|
||||
notify.set_timeout(timeout)
|
||||
notify.show()
|
||||
if IS_DARWIN:
|
||||
# Since NSUserNotification has been deprecated, osascript will be used.
|
||||
os.system("""osascript -e 'display notification "{}" with title "DemonEditor"'""".format(message))
|
||||
elif NOTIFY_IS_INIT:
|
||||
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
|
||||
notify.set_urgency(urgency)
|
||||
notify.set_timeout(timeout)
|
||||
notify.show()
|
||||
|
||||
|
||||
class KeyboardKey(Enum):
|
||||
""" The raw(hardware) codes of the keyboard keys. """
|
||||
E = 26
|
||||
R = 27
|
||||
T = 28
|
||||
P = 33
|
||||
S = 39
|
||||
F = 41
|
||||
X = 53
|
||||
C = 54
|
||||
V = 55
|
||||
W = 25
|
||||
Z = 52
|
||||
INSERT = 118
|
||||
HOME = 110
|
||||
END = 115
|
||||
UP = 111
|
||||
DOWN = 116
|
||||
PAGE_UP = 112
|
||||
PAGE_DOWN = 117
|
||||
LEFT = 113
|
||||
RIGHT = 114
|
||||
F2 = 68
|
||||
F7 = 73
|
||||
SPACE = 65
|
||||
DELETE = 119
|
||||
BACK_SPACE = 22
|
||||
CTRL_L = 37
|
||||
CTRL_R = 105
|
||||
# Laptop codes
|
||||
HOME_KP = 79
|
||||
END_KP = 87
|
||||
PAGE_UP_KP = 81
|
||||
PAGE_DOWN_KP = 89
|
||||
|
||||
@classmethod
|
||||
def value_exist(cls, value):
|
||||
return value in (val.value for val in cls.__members__.values())
|
||||
|
||||
|
||||
# Keys for move in lists. KEY_KP_(NAME) for laptop!!!
|
||||
MOVE_KEYS = (KeyboardKey.UP, KeyboardKey.PAGE_UP, KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN, KeyboardKey.HOME,
|
||||
KeyboardKey.END, KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP)
|
||||
class Page(Enum):
|
||||
""" Main stack widget page. """
|
||||
INFO = "info"
|
||||
SERVICES = "services"
|
||||
SATELLITE = "satellite"
|
||||
PICONS = "picons"
|
||||
EPG = "epg"
|
||||
TIMERS = "timers"
|
||||
RECORDINGS = "recordings"
|
||||
FTP = "ftp"
|
||||
CONTROL = "control"
|
||||
|
||||
|
||||
class FavClickMode(IntEnum):
|
||||
@@ -212,11 +217,143 @@ class Column(IntEnum):
|
||||
ALT_FAV_ID = 5
|
||||
ALT_ID = 6
|
||||
ALT_ITER = 7
|
||||
# Recordings view
|
||||
REC_SERVICE = 0
|
||||
REC_TITLE = 1
|
||||
REC_TIME = 2
|
||||
REC_LEN = 3
|
||||
REC_FILE = 4
|
||||
REC_DESC = 5
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
return self.value
|
||||
|
||||
|
||||
# *************** Keyboard keys *************** #
|
||||
|
||||
class BaseKeyboardKey(Enum):
|
||||
@classmethod
|
||||
def value_exist(cls, value):
|
||||
return value in (val.value for val in cls.__members__.values())
|
||||
|
||||
|
||||
if IS_LINUX:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [Linux] of the keyboard keys. """
|
||||
E = 26
|
||||
R = 27
|
||||
T = 28
|
||||
P = 33
|
||||
S = 39
|
||||
F = 41
|
||||
X = 53
|
||||
C = 54
|
||||
V = 55
|
||||
W = 25
|
||||
Z = 52
|
||||
INSERT = 118
|
||||
HOME = 110
|
||||
END = 115
|
||||
UP = 111
|
||||
DOWN = 116
|
||||
PAGE_UP = 112
|
||||
PAGE_DOWN = 117
|
||||
LEFT = 113
|
||||
RIGHT = 114
|
||||
F2 = 68
|
||||
F7 = 73
|
||||
SPACE = 65
|
||||
DELETE = 119
|
||||
BACK_SPACE = 22
|
||||
CTRL_L = 37
|
||||
CTRL_R = 105
|
||||
# Laptop codes
|
||||
HOME_KP = 79
|
||||
END_KP = 87
|
||||
PAGE_UP_KP = 81
|
||||
PAGE_DOWN_KP = 89
|
||||
|
||||
elif IS_DARWIN:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [macOS] of the keyboard keys. """
|
||||
F = 3
|
||||
E = 14
|
||||
R = 15
|
||||
T = 17
|
||||
P = 35
|
||||
S = 1
|
||||
H = 4
|
||||
L = 37
|
||||
X = 7
|
||||
C = 8
|
||||
V = 9
|
||||
W = 13
|
||||
Z = 6
|
||||
INSERT = -1
|
||||
HOME = -1
|
||||
END = -1
|
||||
UP = 126
|
||||
DOWN = 125
|
||||
PAGE_UP = -1
|
||||
PAGE_DOWN = -1
|
||||
LEFT = 123
|
||||
RIGHT = 123
|
||||
F2 = 120
|
||||
F7 = 98
|
||||
SPACE = 49
|
||||
DELETE = 51
|
||||
BACK_SPACE = 76
|
||||
CTRL_L = 55
|
||||
CTRL_R = 55
|
||||
# Laptop codes.
|
||||
HOME_KP = -1
|
||||
END_KP = -1
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
else:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [Windows] of the keyboard keys. """
|
||||
E = 69
|
||||
R = 82
|
||||
T = 84
|
||||
P = 80
|
||||
S = 83
|
||||
F = 70
|
||||
X = 88
|
||||
C = 67
|
||||
V = 86
|
||||
W = 87
|
||||
Z = 90
|
||||
INSERT = 45
|
||||
HOME = 36
|
||||
END = 35
|
||||
UP = 38
|
||||
DOWN = 40
|
||||
PAGE_UP = 33
|
||||
PAGE_DOWN = 34
|
||||
LEFT = 37
|
||||
RIGHT = 39
|
||||
F2 = 113
|
||||
F7 = 118
|
||||
SPACE = 32
|
||||
DELETE = 46
|
||||
BACK_SPACE = 8
|
||||
CTRL_L = 17
|
||||
CTRL_R = 163
|
||||
# Laptop codes.
|
||||
HOME_KP = -1
|
||||
END_KP = -1
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
# Keys for move in lists. KEY_KP_(NAME) for laptop!
|
||||
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
|
||||
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
|
||||
KeyboardKey.HOME, KeyboardKey.END,
|
||||
KeyboardKey.HOME_KP, KeyboardKey.END_KP,
|
||||
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
8
app/ui/win_style.css
Normal file
8
app/ui/win_style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 6;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
7
build/BUILDING.md
Normal file
7
build/BUILDING.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Building DemonEditor
|
||||
This directory contains build scripts and additional files for various platforms and distributions.
|
||||
|
||||
### Supported platforms
|
||||
* GNU/Linux
|
||||
* macOS
|
||||
* MS Windows
|
||||
@@ -0,0 +1,49 @@
|
||||
diff -Nru demon-editor-2.0-development-orig/DemonEditor.desktop demon-editor-2.0-development/DemonEditor.desktop
|
||||
--- demon-editor-2.0-development-orig/DemonEditor.desktop 2021-10-14 21:32:56.000000000 +0300
|
||||
+++ demon-editor-2.0-development/DemonEditor.desktop 2021-09-29 13:19:24.000000000 +0300
|
||||
@@ -6,8 +6,8 @@
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Icon=demon-editor
|
||||
-Exec=bash -c 'cd $(dirname %k) && ./start.py'
|
||||
+Exec=demon-editor
|
||||
Terminal=false
|
||||
Type=Application
|
||||
-Categories=Utility;Application;
|
||||
+Categories=Utility;
|
||||
StartupNotify=false
|
||||
diff -Nru demon-editor-2.0-development-orig/start.py demon-editor-2.0-development/start.py
|
||||
--- demon-editor-2.0-development-orig/start.py 2021-10-14 21:32:56.000000000 +0300
|
||||
+++ demon-editor-2.0-development/start.py 2021-09-29 13:19:24.000000000 +0300
|
||||
@@ -1,29 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
-import os
|
||||
+from app.ui.main import start_app
|
||||
|
||||
-
|
||||
-def update_icon():
|
||||
- need_update = False
|
||||
- icon_name = "DemonEditor.desktop"
|
||||
-
|
||||
- with open(icon_name, "r") as f:
|
||||
- lines = f.readlines()
|
||||
- for i, line in enumerate(lines):
|
||||
- if line.startswith("Icon="):
|
||||
- icon_path = line.lstrip("Icon=")
|
||||
- current_path = "{}/app/ui/icons/hicolor/96x96/apps/demon-editor.png".format(os.getcwd())
|
||||
- if icon_path != current_path:
|
||||
- need_update = True
|
||||
- lines[i] = "Icon={}\n".format(current_path)
|
||||
- break
|
||||
-
|
||||
- if need_update:
|
||||
- with open(icon_name, "w") as f:
|
||||
- f.writelines(lines)
|
||||
-
|
||||
-
|
||||
-if __name__ == "__main__":
|
||||
- from app.ui.main import start_app
|
||||
-
|
||||
- update_icon()
|
||||
- start_app()
|
||||
+start_app()
|
||||
86
build/linux/ALTLinux spec/demon-editor.spec
Normal file
86
build/linux/ALTLinux spec/demon-editor.spec
Normal file
@@ -0,0 +1,86 @@
|
||||
Name: demon-editor
|
||||
Version: 2.0
|
||||
Release: slava0
|
||||
BuildArch: noarch
|
||||
Summary: Enigma2 channel and satellite list editor
|
||||
Url: https://github.com/DYefremov/DemonEditor
|
||||
License: MIT
|
||||
Group: Other
|
||||
Source: %name-%version-development.tar.gz
|
||||
Patch0: %name-%version-development-startfix.patch
|
||||
AutoReq: no
|
||||
Requires: python3 python3-module-requests python3-module-pygobject3 python3-module-chardet libmpv1
|
||||
BuildRequires: python3-dev python3-module-mpl_toolkits
|
||||
|
||||
%description
|
||||
Enigma2 channel and satellites list editor for GNU/Linux.
|
||||
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
|
||||
|
||||
Main features of the program:
|
||||
Editing bouquets, channels, satellites.
|
||||
Import function.
|
||||
Backup function.
|
||||
Support of picons.
|
||||
Importing services, downloading picons and updating satellites from the Web.
|
||||
Extended support of IPTV.
|
||||
Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
Export of bouquets with IPTV services in m3u.
|
||||
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
Playback of IPTV or other streams directly from the bouquet list.
|
||||
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
Simple FTP client (experimental).
|
||||
|
||||
%prep
|
||||
%setup -n %name-%version-development
|
||||
%patch0 -p1
|
||||
|
||||
%install
|
||||
%__install -d %buildroot%_datadir/demoneditor/app
|
||||
%__install -m644 app/*py %buildroot%_datadir/demoneditor/app
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/eparser
|
||||
%__install -m644 app/eparser/*py %buildroot%_datadir/demoneditor/app/eparser
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/eparser/enigma
|
||||
%__install -m644 app/eparser/enigma/*py %buildroot%_datadir/demoneditor/app/eparser/enigma
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/eparser/neutrino
|
||||
%__install -m644 app/eparser/neutrino/*py %buildroot%_datadir/demoneditor/app/eparser/neutrino
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/tools
|
||||
%__install -m644 app/tools/*py %buildroot%_datadir/demoneditor/app/tools
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*py %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*glade %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*css %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*ui %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m755 start.py %buildroot%_datadir/demoneditor
|
||||
|
||||
%__install -d %buildroot%_iconsdir/hicolor/96x96/apps
|
||||
%__install -d %buildroot%_iconsdir/hicolor/scalable/apps
|
||||
%__install -m644 app/ui/icons/hicolor/96x96/apps/%name.* %buildroot%_iconsdir/hicolor/96x96/apps
|
||||
%__install -m644 app/ui/icons/hicolor/scalable/apps%name.* -d %buildroot%_iconsdir/hicolor/scalable/apps
|
||||
|
||||
%__install -d %buildroot%_datadir/locale
|
||||
cp -r app/ui/lang/* %buildroot%_datadir/locale
|
||||
|
||||
%__install -d %buildroot%_bindir
|
||||
echo "#!/bin/bash
|
||||
python3 %_datadir/demoneditor/start.py $1" > %buildroot%_bindir/%name
|
||||
chmod 755 %buildroot%_bindir/%name
|
||||
|
||||
%__install -d %buildroot%_desktopdir
|
||||
%__install -m644 DemonEditor.desktop %buildroot%_desktopdir/DemonEditor.desktop
|
||||
|
||||
%find_lang %name
|
||||
|
||||
%files -f %name.lang
|
||||
%doc deb/DEBIAN/README.source
|
||||
%_bindir/%name
|
||||
%_datadir/demoneditor
|
||||
%_iconsdir/*/*/*/%name.*
|
||||
%_desktopdir/DemonEditor.desktop
|
||||
|
||||
%changelog
|
||||
* Wed Sep 29 2021 Viacheslav Dikonov <sdiconov@mail.ru> 1.0.10-slava0
|
||||
- ALTLinux package
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
VER="1.0.6_Beta"
|
||||
VER="2.0.0_Alpha"
|
||||
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 -arv app $DEB_PATH
|
||||
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
|
||||
|
||||
cd dist
|
||||
fakeroot dpkg-deb --build DemonEditor
|
||||
@@ -1,7 +1,7 @@
|
||||
demon-editor for Debian
|
||||
----------------------
|
||||
DemonEditor
|
||||
Enigma2 channel and satellites list editor for GNU/Linux.
|
||||
Enigma2 channel and satellite list editor for GNU/Linux.
|
||||
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
|
||||
@@ -10,13 +10,15 @@ Main features of the program:
|
||||
Editing bouquets, channels, satellites.
|
||||
Import function.
|
||||
Backup function.
|
||||
Extended support of IPTV.
|
||||
Support of picons.
|
||||
Downloading of picons and updating of satellites (transponders) from the web.
|
||||
Importing services, downloading picons and updating satellites from the Web.
|
||||
Extended support of IPTV.
|
||||
Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
Export of bouquets with IPTV services in m3u.
|
||||
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed VLC).
|
||||
Playback of IPTV or other streams directly from the bouquet list.
|
||||
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
Simple FTP client (experimental).
|
||||
|
||||
Keyboard shortcuts:
|
||||
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
|
||||
@@ -43,7 +45,7 @@ Keyboard shortcuts:
|
||||
For multiple selection with the mouse, press and hold the Ctrl key!
|
||||
|
||||
Minimum requirements:
|
||||
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings, python3-requests.
|
||||
Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.
|
||||
|
||||
Important:
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
|
||||
@@ -55,4 +57,6 @@ Important:
|
||||
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
|
||||
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
|
||||
|
||||
For streams playback, this app supports VLC, MPV and GStreamer.
|
||||
Depending on your distro, you may need to install additional packages and libraries.
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
Package: demon-editor
|
||||
Version: 1.0.6-Beta
|
||||
Version: 2.0.0-Alpha
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Essential: no
|
||||
Depends: python3 (>= 3.5), python3-requests
|
||||
Recommends: gstreamer1.0-gtk3, python3-gi-cairo, python3-chardet
|
||||
Depends: python3 (>= 3.6),
|
||||
python3-requests,
|
||||
python3-gi,
|
||||
python3-gi-cairo,
|
||||
gir1.2-notify-0.7
|
||||
Recommends: libmpv1,
|
||||
python3-chardet
|
||||
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
|
||||
Description: Enigma2 channel and satellite list editor
|
||||
4
build/linux/deb/usr/share/demoneditor/start.py
Executable file
4
build/linux/deb/usr/share/demoneditor/start.py
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
from app.ui.main import start_app
|
||||
|
||||
start_app()
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
build/linux/deb/usr/share/locale/be/LC_MESSAGES/demon-editor.mo
Normal file
BIN
build/linux/deb/usr/share/locale/be/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
build/linux/deb/usr/share/locale/de/LC_MESSAGES/demon-editor.mo
Normal file
BIN
build/linux/deb/usr/share/locale/de/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
build/linux/deb/usr/share/locale/it/LC_MESSAGES/demon-editor.mo
Normal file
BIN
build/linux/deb/usr/share/locale/it/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
build/linux/deb/usr/share/locale/ru/LC_MESSAGES/demon-editor.mo
Normal file
BIN
build/linux/deb/usr/share/locale/ru/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
build/linux/deb/usr/share/locale/tr/LC_MESSAGES/demon-editor.mo
Normal file
BIN
build/linux/deb/usr/share/locale/tr/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
75
build/mac/DemonEditor.spec
Normal file
75
build/mac/DemonEditor.spec
Normal file
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
import datetime
|
||||
import distutils.util
|
||||
|
||||
EXE_NAME = 'start.py'
|
||||
DIR_PATH = os.getcwd()
|
||||
COMPILING_PLATFORM = distutils.util.get_platform()
|
||||
PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
|
||||
STRIP = True
|
||||
BUILD_DATE = datetime.datetime.now().strftime("%y%m%d")
|
||||
|
||||
block_cipher = None
|
||||
|
||||
excludes = ['app.tools.mpv',
|
||||
'gi.repository.Gst',
|
||||
'gi.repository.GstBase',
|
||||
'gi.repository.GstVideo',
|
||||
'youtube_dl',
|
||||
'tkinter']
|
||||
|
||||
ui_files = [('app/ui/*.glade', 'ui'),
|
||||
('app/ui/*.css', 'ui'),
|
||||
('app/ui/*.ui', 'ui'),
|
||||
('app/ui/lang*', 'share/locale'),
|
||||
('app/ui/icons*', 'share/icons')
|
||||
]
|
||||
|
||||
a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=None,
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=excludes,
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher)
|
||||
|
||||
pyz = PYZ(a.pure,
|
||||
a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=True,
|
||||
name='DemonEditor',
|
||||
debug=False,
|
||||
strip=STRIP,
|
||||
upx=True,
|
||||
console=False)
|
||||
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=STRIP,
|
||||
upx=True,
|
||||
name='DemonEditor')
|
||||
|
||||
app = BUNDLE(coll,
|
||||
name='DemonEditor.app',
|
||||
icon='icon.icns',
|
||||
bundle_identifier=None,
|
||||
info_plist={
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'CFBundleName': 'DemonEditor',
|
||||
'CFBundleDisplayName': 'DemonEditor',
|
||||
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
|
||||
'LSApplicationCategoryType': 'public.app-category.utilities',
|
||||
'LSMinimumSystemVersion': '10.13',
|
||||
'CFBundleShortVersionString': f"2.0.0.{BUILD_DATE} Alpha",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2021, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false'
|
||||
})
|
||||
BIN
build/mac/icon.icns
Normal file
BIN
build/mac/icon.icns
Normal file
Binary file not shown.
8
build/mac/start.py
Normal file
8
build/mac/start.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
if __name__ == "__main__":
|
||||
from multiprocessing import set_start_method
|
||||
from app.ui.main import start_app
|
||||
|
||||
set_start_method("fork") # For compatibility [Python > 3.7]
|
||||
start_app()
|
||||
57
build/win/DemonEditor.spec
Normal file
57
build/win/DemonEditor.spec
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
EXE_NAME = 'start.py'
|
||||
DIR_PATH = os.getcwd()
|
||||
PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
excludes = ['app.tools.mpv',
|
||||
'gi.repository.Gst',
|
||||
'gi.repository.GstBase',
|
||||
'gi.repository.GstVideo',
|
||||
'youtube_dl',
|
||||
'tkinter']
|
||||
|
||||
|
||||
ui_files = [('app\\ui\\*.glade', 'ui'),
|
||||
('app\\ui\\*.css', 'ui'),
|
||||
('app\\ui\\*.ui', 'ui'),
|
||||
('app\\ui\\lang*', 'share\\locale'),
|
||||
('app\\ui\\icons*', 'share\\icons')
|
||||
]
|
||||
|
||||
|
||||
a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=[],
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=excludes,
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='DemonEditor',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, icon='icon.ico')
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='DemonEditor')
|
||||
BIN
build/win/icon.ico
Normal file
BIN
build/win/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
8
build/win/start.py
Normal file
8
build/win/start.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
if __name__ == "__main__":
|
||||
from multiprocessing import freeze_support
|
||||
from app.ui.main import start_app
|
||||
|
||||
freeze_support()
|
||||
start_app()
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from app.ui.main_app_window import start_app
|
||||
|
||||
start_app()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1228,3 +1228,24 @@ msgstr "Памер пiконаў у спісах:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Памер лагатыпа ва ўсплыўных падказках:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Захаваць як"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Адзначыць дублікаты"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Загрузіць толькі для абранага букета"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "Заданне скасавана!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Выконваецца загрузка дадзеных!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Запісы"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Даведка"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
# Charly, 2019.
|
||||
# Dmitriy Yefremov, 2020-2021.
|
||||
# Thomas Schmidt, 2021
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Dmitriy Yefremov\n"
|
||||
@@ -12,7 +13,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "Charly\nDmitriy Yefremov"
|
||||
msgstr "Charly\nDmitriy Yefremov\nThomas Schmidt"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -666,16 +667,16 @@ msgid "Test connection"
|
||||
msgstr "Test Verbindung"
|
||||
|
||||
msgid "Double click on the service in the bouquet list:"
|
||||
msgstr "Doppelklicke auf das Service in der Bouquetliste:"
|
||||
msgstr "Doppelklick auf das Service in der Bouquet-Liste:"
|
||||
|
||||
msgid "Zap"
|
||||
msgstr "Zap"
|
||||
|
||||
msgid "Play stream"
|
||||
msgstr "Play Stream"
|
||||
msgstr "Stream abspielen"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Ausgeschaltet"
|
||||
msgstr "Deaktiviert"
|
||||
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Lamedb ver. 5 Unterstützung aktivieren"
|
||||
@@ -693,7 +694,7 @@ msgid "Play IPTV or other stream in the program(Ctrl + P)"
|
||||
msgstr "Wiedergabe von IPTV oder anderen Streams im Programm(Strg + P)"
|
||||
|
||||
msgid "Export to m3u"
|
||||
msgstr "Export nach m3u"
|
||||
msgstr "Exportieren nach m3u"
|
||||
|
||||
msgid "EPG configuration"
|
||||
msgstr "EPG Konfiguration"
|
||||
@@ -720,13 +721,13 @@ msgid "Url to *.xml.gz file:"
|
||||
msgstr "Url zur *.xml.gz Datei:"
|
||||
|
||||
msgid "Enable filtering"
|
||||
msgstr "Filterung einschalten"
|
||||
msgstr "Filter aktivieren"
|
||||
|
||||
msgid "Filter by presence in the epg.dat file."
|
||||
msgstr "Filtern nach dem Vorhandensein in der epg.dat Datei."
|
||||
|
||||
msgid "Paths to the epg.dat file:"
|
||||
msgstr "Pfade zur epg.dat Datei:"
|
||||
msgstr "Pfad zur epg.dat Datei:"
|
||||
|
||||
msgid "Local path:"
|
||||
msgstr "Local path:"
|
||||
@@ -750,7 +751,7 @@ msgid "Unsupported file type:"
|
||||
msgstr "Nicht unterstützter Dateityp:"
|
||||
|
||||
msgid "Unpacking data error."
|
||||
msgstr "Fehler beim Entpacken von Daten."
|
||||
msgstr "Fehler beim Entpacken der Daten."
|
||||
|
||||
msgid "XML parsing error:"
|
||||
msgstr "XML Parsing-Fehler:"
|
||||
@@ -765,7 +766,7 @@ msgid "Use HTTP"
|
||||
msgstr "HTTP verwenden"
|
||||
|
||||
msgid "Close playback"
|
||||
msgstr "Wiedergabe schliessen"
|
||||
msgstr "Wiedergabe schließen"
|
||||
|
||||
msgid "Import YouTube playlist"
|
||||
msgstr "YouTube-Wiedergabeliste importieren"
|
||||
@@ -774,8 +775,8 @@ msgid ""
|
||||
"Found a link to the YouTube resource!\n"
|
||||
"Try to get a direct link to the video?"
|
||||
msgstr ""
|
||||
"Ich habe einen Link zur YouTube-Ressource gefunden!\n"
|
||||
"Versuchen einen direkten Link zum Video zu bekommen?"
|
||||
"Link zur YouTube-Ressource gefunden!\n"
|
||||
"Soll versucht werden, einen Direkt-Link zum Video zu erzeugen?"
|
||||
|
||||
msgid "Playlist import"
|
||||
msgstr "Playlist-Import"
|
||||
@@ -784,7 +785,7 @@ msgid "Getting link error:"
|
||||
msgstr "Link-Fehler erhalten:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Extra"
|
||||
msgstr "Extras"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Profileinstellungen anwenden"
|
||||
@@ -793,7 +794,7 @@ msgid "Settings type:"
|
||||
msgstr "Art der Einstellungen:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Standard setzen"
|
||||
msgstr "Standard wiederherstellen"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Sprache:"
|
||||
@@ -805,16 +806,16 @@ msgid "Enable direct playback bar"
|
||||
msgstr "Aktivieren der direkten Wiedergabeleiste"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Box"
|
||||
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Receiver"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Gucken den Kanal im Programm an"
|
||||
msgstr "Kanal im Programm ansehen"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Zap und Abspielen"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Ziehe den Link hierher oder füge ihn ein"
|
||||
msgstr "Link hineinziehen oder einfügen"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Hinzugefügte Links in der Wiedergabeliste entfernen"
|
||||
@@ -832,13 +833,13 @@ msgid "Reset"
|
||||
msgstr "Reset"
|
||||
|
||||
msgid "File"
|
||||
msgstr "Ablage"
|
||||
msgstr "Datei"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Picons-Manager"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr ""
|
||||
msgstr "Explorer"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "Satellit URL:"
|
||||
@@ -916,13 +917,13 @@ msgid "Height (px):"
|
||||
msgstr "Höhe (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Kanälen:"
|
||||
msgstr "Kanäle:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Samplerate (Hz):"
|
||||
msgstr "Sample-Rate (Hz):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Streams Abspielen-Modus:"
|
||||
msgstr "Stream-Abspielmodus:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Integrierter Player"
|
||||
@@ -934,22 +935,22 @@ msgid "Only get m3u file"
|
||||
msgstr "Nur m3u-Datei erhalten"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
msgstr "Speicher und starte das Programm neu, um die Einstellungen zu übernehmen."
|
||||
msgstr "Änderungen speichern und anschließend das Programm neustarten, um die Einstellungen zu übernehmen."
|
||||
|
||||
msgid "Some images may have problems displaying the favorites list!"
|
||||
msgstr "Einige Images können Probleme mit der Anzeige der Favoritenliste haben!"
|
||||
msgstr "Einige Bilder können Probleme in der Anzeige der Favoritenliste haben!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Arbeitet im Standby-Modus oder auf dem aktuell aktiven Transponder!"
|
||||
msgstr "Arbeitet im Standby-Modus oder der Transponder ist bereits in Verwendung!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Keine Verbindung zum Box!"
|
||||
msgstr "Keine Verbindung zum Receiver!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Signalpegel"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Box-Info"
|
||||
msgstr "Receiver-Info"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
msgstr "Ein Profil mit diesem Namen existiert!"
|
||||
@@ -958,10 +959,10 @@ msgid "Show short info as hints in the main services list"
|
||||
msgstr "Kurzinfos als Tooltips in der Hauptliste anzeigen"
|
||||
|
||||
msgid "Show detailed info as hints in the bouquet list"
|
||||
msgstr "Detaillierteinfos als Tooltips in der Bouquetliste anzeigen"
|
||||
msgstr "Detaillierte Informationen als Tooltips in der Bouquetliste anzeigen"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Aktivieren der Alternativerbenennung für Bouquet-Dateien"
|
||||
msgstr "Aktivieren der alternativen Benennung für Bouquet-Dateien"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Ermöglicht Bouquet-Dateien mit ihren Namen zu benennen."
|
||||
@@ -970,7 +971,7 @@ msgid "Appearance"
|
||||
msgstr "Aussehen"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Unterstützung von Themen aktivieren"
|
||||
msgstr "Theme Unterstützung aktivieren"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Gtk3-Theme:"
|
||||
@@ -979,7 +980,7 @@ msgid "Icon Theme:"
|
||||
msgstr "Icon-Theme:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 Themes und Icons:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Daten löschen..."
|
||||
@@ -988,7 +989,7 @@ msgid "Download from the receiver"
|
||||
msgstr "Downloaden vom Receiver"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Alle Picons aus dem Receiver entfernen"
|
||||
msgstr "Alle Picons vom dem Receiver entfernen"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Kanalreferenz"
|
||||
@@ -1021,7 +1022,7 @@ msgid "Are you sure you want to change the order\n\t of services in this bouquet
|
||||
msgstr "Bist du sicher, dass du die Reihenfolge der Dienstleistungen\n\t in diesem Bouquet ändern willst?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Aus dem Receiver entfernen"
|
||||
msgstr "Vom Receiver entfernen"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Screenshot"
|
||||
@@ -1039,7 +1040,7 @@ msgid "Can't Playback!"
|
||||
msgstr "Kann nicht abgespielt werden!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Dunkelmodus aktivieren"
|
||||
msgstr "Dunkler Modus aktivieren"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Entpacken..."
|
||||
@@ -1051,7 +1052,7 @@ msgid "Combine with the current data?"
|
||||
msgstr "Mit den aktuellen Daten kombinieren?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "Daten importieren erledigt!"
|
||||
msgstr "Daten-Import abgeschlossen!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Aktueller Service"
|
||||
@@ -1063,13 +1064,13 @@ msgid "Open archive"
|
||||
msgstr "Archiv öffnen"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Import aus dem Web"
|
||||
msgstr "Import aus dem Internet"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Steuerung"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Timers"
|
||||
msgstr "Timer"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Timer"
|
||||
@@ -1111,7 +1112,7 @@ msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Screenshot schnappen"
|
||||
msgstr "Screenshot aufnehmen"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Aktiviert:"
|
||||
@@ -1138,7 +1139,7 @@ msgid "Ends:"
|
||||
msgstr "Endet:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Wiederhole:"
|
||||
msgstr "Wiederholt:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Aktion:"
|
||||
@@ -1216,7 +1217,7 @@ msgid "A similar service is already in this list!"
|
||||
msgstr "Ein ähnlicher Dienst ist bereits in dieser Liste enthalten!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "Abspielen-Modus wurde geändert!\nStarte das Programm neu, um die Einstellungen zu übernehmen."
|
||||
msgstr "Abspiel-Modus wurde geändert!\nStarte das Programm neu, um die Einstellungen zu übernehmen."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Stelle die Werte für TID, NID und Namespace für die korrekte Benennung der Picons ein!"
|
||||
@@ -1241,3 +1242,24 @@ msgstr "Picons Größe in den Listen:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Logo-Größe in Tooltips:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Speichern als"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Duplikate markieren"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Nur für ausgewähltes Bouquet laden"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "Der Task wird abgebrochen!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Daten werden geladen!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Aufnahmen"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Hilfe"
|
||||
|
||||
1248
po/it/demon-editor.po
Normal file
1248
po/it/demon-editor.po
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user