Compare commits

...

87 Commits

Author SHA1 Message Date
DYefremov
6c6948ce23 added new order to alternate layout 2020-12-25 09:38:11 +03:00
DYefremov
573d755e31 added display option for bouquet details list 2020-12-24 23:31:37 +03:00
DYefremov
912083f203 README update 2020-12-23 09:39:08 +03:00
DYefremov
4df0553333 Russian, Belarusian and German translations update 2020-12-23 09:38:27 +03:00
DYefremov
f0d535ba4e yt fix 2020-12-23 09:38:12 +03:00
DYefremov
88ef5563cf ftp client improvements 2020-12-22 14:31:38 +03:00
DYefremov
6431f2ccd8 minor fix 2020-12-22 14:31:25 +03:00
DYefremov
ca9b4a780d storing app window size on close 2020-12-19 12:38:40 +03:00
DYefremov
f74eead20b added alternate layout support 2020-12-18 22:18:13 +03:00
DYefremov
8d4d90fd9f added alternate layout option 2020-12-18 22:16:03 +03:00
DYefremov
4269d16d31 added ftp client to main window 2020-12-17 14:00:14 +03:00
DYefremov
9cf3e97bd3 added ftp client base class 2020-12-17 10:27:01 +03:00
DYefremov
3b85d35b62 added keyboard key 2020-12-17 10:25:30 +03:00
DYefremov
6bddd89206 ftp refactoring [func extension] 2020-12-14 15:12:28 +03:00
DYefremov
e16f2cba82 allowed to add dir during path config 2020-12-14 15:11:44 +03:00
DYefremov
59748aa9ba added encoding detection for *.m3u import 2020-12-07 09:32:00 +03:00
DYefremov
caf4925409 data load rework (#37 fix [can't decode byte]) 2020-12-05 14:13:29 +03:00
DYefremov
2ab540ccfa bump version 2020-12-05 14:12:04 +03:00
DYefremov
4b762802da added playlist extraction via youtube-dl 2020-12-05 14:10:09 +03:00
DYefremov
41a6e54e90 upd. README 2020-12-02 01:06:50 +03:00
DYefremov
7db02f2a9e added types to the service parser 2020-11-28 14:25:08 +03:00
DYefremov
fd40fd8d72 changes for popup menu actions 2020-11-27 21:27:23 +03:00
DYefremov
0b4313e4cf minor changes in control panel gui 2020-11-27 21:25:00 +03:00
DYefremov
a74628ed5c added sorting reset when loading data 2020-11-26 13:54:00 +03:00
DYefremov
443f6bf252 translations correction for app description 2020-11-26 13:53:49 +03:00
DYefremov
bb243ce281 added timeouts for telnet login 2020-11-26 13:53:20 +03:00
DYefremov
44049c380e config.yml update 2020-11-26 13:53:03 +03:00
DYefremov
281fe2a8f4 Russian, Belarusian and German translations update 2020-11-26 13:52:21 +03:00
DYefremov
39cc0ad8b3 some changes in the control panel 2020-11-26 13:51:46 +03:00
DYefremov
a625dc9f8b style correction 2020-11-26 13:51:29 +03:00
DYefremov
53f69b8f67 Russian, Belarusian and German translations update 2020-11-26 13:51:13 +03:00
DYefremov
94dfda0fa2 added basic support for timers via http api 2020-11-26 13:40:36 +03:00
DYefremov
cfe3f4c707 added styles 2020-11-26 13:39:36 +03:00
DYefremov
d18734910d small http api refactoring 2020-11-26 13:36:45 +03:00
DYefremov
d843633043 added epg display in control panel 2020-11-26 13:28:16 +03:00
DYefremov
b513d7a9b0 added prototype of simple control panel (#38) 2020-11-26 13:09:55 +03:00
DYefremov
92b2f840f8 fix update of sat positions after web import 2020-11-25 22:18:22 +03:00
DYefremov
9e4c8f388c added transponder view popup menu 2020-11-25 22:17:04 +03:00
DYefremov
664c52cfe1 minor compatibility fix 2020-11-25 22:15:27 +03:00
DYefremov
cc96cdd8fd added web import for services (#16) 2020-11-25 22:09:13 +03:00
DYefremov
15cca3f5f7 small decoupling for lamedb parsing 2020-11-25 21:27:22 +03:00
DYefremov
0ec2570043 added remote control requests 2020-11-25 21:26:51 +03:00
DYefremov
97cb26cd60 bump version 2020-11-25 21:26:36 +03:00
DYefremov
1da3eacc8c minor fix for the picon parser 2020-10-10 15:21:47 +03:00
DYefremov
cb6f185032 minor fix for yt 2020-10-06 11:27:09 +03:00
DYefremov
a8918bcf1f Russian, Belarusian and German translations update 2020-10-05 22:17:20 +03:00
DYefremov
c358197080 data loading refactoring 2020-10-05 22:16:54 +03:00
DYefremov
474fc3ec58 adaptation of dnd 2020-10-05 22:15:01 +03:00
DYefremov
c17bad215f changed getting of the drag icon 2020-09-30 20:58:30 +03:00
DYefremov
7891aca6e2 upd README 2020-09-28 13:48:04 +03:00
DYefremov
608de65897 Russian, Belarusian and German translations update 2020-09-28 13:46:08 +03:00
DYefremov
cbed3f7cca reworked and improved dnd for lists 2020-09-28 13:45:01 +03:00
DYefremov
08c1dca06d added support for loading and importing data via dnd 2020-09-28 13:30:12 +03:00
DYefremov
1edbd7d771 version update 2020-09-28 13:25:10 +03:00
DYefremov
0c3f6870dd added support for opening archives 2020-09-28 13:22:19 +03:00
DYefremov
f877872059 minor rework of the chooser dialog 2020-09-28 12:59:38 +03:00
DYefremov
335dfc005a displaying sid value in uppercase for tooltips(#34) 2020-09-12 22:52:18 +03:00
DYefremov
46450cf9b6 Display the sid value in tooltips in hex and dec format(#34). 2020-09-12 22:52:07 +03:00
Víctor Pont
9ed82ea129 Spanish translation update (#36) 2020-09-11 16:38:01 +03:00
DYefremov
555699c2a1 renaming bouquet fix [losing custom names](#33) 2020-09-08 12:45:32 +03:00
DYefremov
83b810286a upd README 2020-09-06 13:30:08 +03:00
DYefremov
61a56f1989 copy pl *.mo file 2020-09-02 23:04:08 +03:00
Wieslaw Weglowski
50ce4a688a Polish translation corrections (#31) 2020-09-02 22:53:47 +03:00
DYefremov
871b428b19 Russian, Belarusian and German translations update 2020-09-02 22:53:22 +03:00
DYefremov
3cd864cd84 version update 2020-08-31 22:27:45 +03:00
DYefremov
78c6a3c9fa minor fix for picon assignment 2020-08-31 22:27:20 +03:00
DYefremov
4c0904cf6c added Belarusian translation 2020-08-31 12:24:33 +03:00
DYefremov
7aa688df15 fix playback from the start screen 2020-08-31 12:20:37 +03:00
DYefremov
c91d58e0cf minor changes in sat dialogs 2020-08-27 23:21:19 +03:00
DYefremov
d071bb5d85 telnet password visibility fix 2020-08-24 22:44:30 +03:00
DYefremov
8cee77357c Polish translation update 2020-08-24 22:22:25 +03:00
DYefremov
20f53dee33 minor optimization 2020-08-24 22:22:09 +03:00
Wieslaw Weglowski
c454a33b3c Update demon-editor.po (#30) 2020-08-24 22:21:56 +03:00
DYefremov
5642b8871c upd README 2020-08-19 21:24:34 +03:00
DYefremov
e7480ec622 rework of the picons resizing 2020-08-19 21:20:07 +03:00
DYefremov
ecce001ce4 German and Russian translation update 2020-08-15 16:55:55 +03:00
DYefremov
7bae895458 minor yt fix 2020-08-15 16:55:40 +03:00
DYefremov
5b3bd48746 output fix for picons downloader 2020-08-09 00:14:58 +03:00
DYefremov
4769a814bd update *.spec 2020-08-09 00:05:46 +03:00
DYefremov
b08d4ed7d7 added dark mode support 2020-08-09 00:05:04 +03:00
DYefremov
233eb6bc53 added dark mode option 2020-08-08 14:47:57 +03:00
DYefremov
8b3d24c006 small changes of settings dialog appearance 2020-08-07 15:11:32 +03:00
DYefremov
48184c1fd9 minor fixes 2020-08-07 11:37:59 +03:00
DYefremov
46d91b93bc skipping enigma2 stop during picons upload 2020-08-06 21:19:47 +03:00
DYefremov
69989e784d minor correction of translations 2020-08-04 12:52:01 +03:00
DYefremov
ce1c978222 version update 2020-08-03 22:50:22 +03:00
DYefremov
5bcac35deb added option for experimental features 2020-08-03 22:23:44 +03:00
51 changed files with 10680 additions and 3379 deletions

View File

@@ -22,10 +22,10 @@ a = Analysis([EXE_NAME],
pathex=PATH_EXE, pathex=PATH_EXE,
binaries=None, binaries=None,
datas=ui_files, datas=ui_files,
hiddenimports=[], hiddenimports=['fileinput', 'uuid'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
excludes=[], excludes=['youtube_dl', 'tkinter'],
win_no_prefer_redirects=False, win_no_prefer_redirects=False,
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher) cipher=block_cipher)
@@ -60,6 +60,8 @@ app = BUNDLE(coll,
'CFBundleName': 'DemonEditor', 'CFBundleName': 'DemonEditor',
'CFBundleDisplayName': 'DemonEditor', 'CFBundleDisplayName': 'DemonEditor',
'CFBundleGetInfoString': "Enigma2 channel and satellites editor", 'CFBundleGetInfoString': "Enigma2 channel and satellites editor",
'CFBundleShortVersionString': "1.0.0 Alpha (Build: {})".format(BUILD_DATE), 'LSApplicationCategoryType': 'public.app-category.utilities',
'NSHumanReadableCopyright': u"Copyright © 2020, Dmitriy Yefremov" 'CFBundleShortVersionString': "1.0.3 Beta (Build: {})".format(BUILD_DATE),
'NSHumanReadableCopyright': u"Copyright © 2020, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false'
}) })

View File

@@ -1,20 +1,27 @@
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor # <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![platform](https://img.shields.io/badge/platform-macos-lightgrey)
## Enigma2 channel and satellites list editor for macOS (experimental). ## Enigma2 channel and satellite list editor for macOS (experimental).
### The functionality and performance of this version may be different from the Linux version! 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).
**The functionality and performance of this version may be different from the [Linux version](https://github.com/DYefremov/DemonEditor)!**
### Main features of the program: ![Main app window in macOS Big Sur.](https://user-images.githubusercontent.com/7511379/92320982-9b20c780-f02e-11ea-8a43-fc0c70503573.png)
## Main features of the program
* Editing bouquets, channels, satellites. * Editing bouquets, channels, satellites.
* Import function. * Import function.
* Backup function. * Backup function.
* Extended support of IPTV. * Extended support of IPTV.
* Support of picons. * Support of picons.
* Downloading of picons and updating of satellites (transponders) from the web. * Importing services, downloading picons and updating satellites from the Web.
* Import to bouquet(Neutrino WEBTV) from m3u. * Import to bouquet(Neutrino WEBTV) from m3u.
* Export of bouquets with IPTV services in m3u. * Export of bouquets with IPTV services in m3u.
* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental). * 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](https://www.videolan.org/vlc/)). * Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)).
#### Keyboard shortcuts: * Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
* Simple FTP client (experimental).
#### Keyboard shortcuts
* **&#8984; + X** - only in bouquet list. * **&#8984; + X** - only in bouquet list.
* **&#8984; + C** - only in services list. * **&#8984; + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion! Clipboard is **"rubber"**. There is an accumulation before the insertion!
@@ -34,40 +41,56 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **&#8679; + &#8984; + F** - show/hide filter bar. * **&#8679; + &#8984; + F** - show/hide filter bar.
* **Left/Right** - remove selection. * **Left/Right** - remove selection.
For multiple mouse selection (including Drag and Drop), press and hold the **&#8984;** key! For **multiple** selection with the mouse, press and hold the **&#8984;** key!
### Minimum requirements: ## Minimum requirements
Python >= **3.5**, GTK+ >= **3.16**, pygobject3, adwaita-icon-theme, python3-requests. *Python >= 3.5.2, GTK+ >= 3.16 with PyGObject bindings, python3-requests.*
#### Installation:
## Installation and Launch
To run the program on macOS, you need to install [brew](https://brew.sh/).
Then install the required components via terminal:
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme``` ```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
```pip3 install requests``` ```pip3 install requests```
#### Optional: #### Optional:
```brew install wget imagemagick``` ```brew install wget```
```pip3 install pyobjc``` ```pip3 install pillow, pyobjc```
#### Launching:
To start the program, just download the archive, unpack and run it from the terminal with the command: ```./start.py``` To start the program, just download the [archive](https://github.com/DYefremov/DemonEditor/archive/experimental-mac.zip), unpack and run it from the terminal
### Building standalone application: with the command: ```./start.py```
Install [PyInstaller](https://www.pyinstaller.org/) with the command from the terminal: ## Standalone package
```pip3 install pyinstaller``` You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
and in th root dir run command:
```pyinstaller DemonEditor.spec```
### Standalone package:
You can download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Recommended copy the package to the **Application** directory. Recommended copy the package to the **Application** directory.
Perhaps in the security settings it will be necessary to allow the launch of this application! Perhaps in the security settings it will be necessary to allow the launch of this application!
**The package may not contain all the latest changes. Not all features can be supported and tested!** **The package may not contain all the latest changes. Not all features can be supported and tested!**
### Note:
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY. THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE. AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
The package may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license. The package may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
By downloading and using this package you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this! By downloading and using this package you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!
### Important:
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!
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the #### Building your own package
selected bouquets!** If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten), Install [PyInstaller](https://www.pyinstaller.org/) with the command from the terminal:
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. ```pip3 install pyinstaller```
and in the root dir run command:
```pyinstaller DemonEditor.spec```
## Important
**This version is not fully tested and has experimental status!**
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.
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),
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.
#### Command line arguments:
* **-l** - write logs to file.
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
* **-t on/off** - show/hide simple built-in **telnet** client (experimental). **ANSI escape sequences are not supported!**
## License
Licensed under the [MIT](LICENSE) license.

View File

@@ -1,2 +1,2 @@
theme: jekyll-theme-cayman theme: jekyll-theme-slate
show_downloads: true show_downloads: true

View File

@@ -5,7 +5,7 @@ import time
import urllib import urllib
import xml.etree.ElementTree as ETree import xml.etree.ElementTree as ETree
from enum import Enum from enum import Enum
from ftplib import FTP, error_perm from ftplib import FTP, CRLF, Error, error_perm
from http.client import RemoteDisconnected from http.client import RemoteDisconnected
from telnetlib import Telnet from telnetlib import Telnet
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
@@ -35,25 +35,6 @@ class DownloadType(Enum):
EPG = 5 EPG = 5
class HttpRequestType(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
TEST = None
TOKEN = "session"
PLAY = "mediaplayerplay?file="
PLAYER_LIST = "mediaplayerlist?path=playlist"
PLAYER_PLAY = "mediaplayercmd?command=play"
PLAYER_NEXT = "mediaplayercmd?command=next"
PLAYER_PREV = "mediaplayercmd?command=previous"
PLAYER_STOP = "mediaplayercmd?command=stop"
PLAYER_REMOVE = "mediaplayerremove?file="
GRUB = "grab?format=jpg&"
class TestException(Exception): class TestException(Exception):
pass pass
@@ -62,8 +43,289 @@ class HttpApiException(Exception):
pass pass
def download_data(*, settings, download_type=DownloadType.ALL, callback=print, files_filter=None): class UtfFTP(FTP):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: """ FTP class wrapper. """
def retrlines(self, cmd, callback=None):
""" Small modification of the original method.
It is used to retrieve data in line mode and skip errors related
to reading file names in encoding other than UTF-8 or Latin-1.
Decode errors are ignored [UnicodeDecodeError, etc].
"""
if callback is None:
callback = log
self.sendcmd("TYPE A")
with self.transfercmd(cmd) as conn, conn.makefile("r", encoding=self.encoding, errors="ignore") as fp:
while 1:
line = fp.readline(self.maxline + 1)
if len(line) > self.maxline:
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
log(msg)
raise Error(msg)
if self.debugging > 2:
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
if not line:
break
if line[-2:] == CRLF:
line = line[:-2]
elif line[-1:] == "\n":
line = line[:-1]
callback(line)
return self.voidresp()
# ***************** Download ******************* #
def download_files(self, save_path, file_list, callback=None):
""" Downloads files from the receiver via FTP. """
for file in filter(lambda s: s.endswith(file_list), self.nlst()):
self.download_file(file, save_path, callback)
def download_file(self, name, save_path, callback=None):
with open(save_path + name, "wb") as f:
msg = "Downloading file: {}. Status: {}\n"
try:
resp = str(self.retrbinary("RETR " + name, f.write))
except error_perm as e:
resp = str(e)
msg = msg.format(name, e)
log(msg.rstrip())
else:
msg = msg.format(name, resp)
callback(msg) if callback else log(msg.rstrip())
return resp
def download_dir(self, path, save_path, callback=None):
""" Downloads directory from FTP with all contents.
Creates a leaf directory and all intermediate ones. This is recursive.
"""
os.makedirs(os.path.join(save_path, path), exist_ok=True)
files = []
self.dir(path, files.append)
for f in files:
f_data = f.split()
f_path = os.path.join(path, " ".join(f_data[8:]))
if f_data[0][0] == "d":
try:
os.makedirs(os.path.join(save_path, f_path), exist_ok=True)
except OSError as e:
msg = "Download dir error: {}".format(e).rstrip()
log(msg)
return "500 " + msg
else:
self.download_dir(f_path, save_path, callback)
else:
try:
self.download_file(f_path, save_path, callback)
except OSError as e:
log("Download dir error: {}".format(e).rstrip())
resp = "226 Transfer complete."
msg = "Copy directory {}. Status: {}".format(path, resp)
log(msg)
if callback:
callback(msg)
return resp
def download_xml(self, data_path, xml_path, xml_files, callback):
""" Used for download *.xml files. """
self.cwd(xml_path)
self.download_files(data_path, xml_files, callback)
def download_picons(self, src, dest, callback, files_filter=None):
try:
self.cwd(src)
except error_perm as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), self.nlst()):
self.download_file(file, dest, callback)
# ***************** Uploading ******************* #
def upload_bouquets(self, data_path, remove_unused, callback):
if remove_unused:
self.remove_unused_bouquets(callback)
self.upload_files(data_path, BQ_FILES_LIST, callback)
def upload_files(self, data_path, file_list, callback):
for file_name in os.listdir(data_path):
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
continue
if file_name.endswith(file_list):
self.send_file(file_name, data_path, callback)
def upload_xml(self, data_path, xml_path, xml_files, callback):
""" Used for transfer *.xml files. """
self.cwd(xml_path)
for xml_file in xml_files:
self.send_file(xml_file, data_path, callback)
def upload_picons(self, src, dest, callback, files_filter=None):
try:
self.cwd(dest)
except error_perm as e:
if str(e).startswith("550"):
self.mkd(dest) # if not exist
self.cwd(dest)
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
self.send_file(file_name, src, callback)
def remove_unused_bouquets(self, callback):
bq_files = ("userbouquet.", "bouquets.xml", "ubouquets.xml")
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
self.delete_file(file, callback)
def send_file(self, file_name, path, callback=None):
""" Opens the file in binary mode and transfers into receiver """
file_src = path + file_name
resp = "500"
if not os.path.isfile(file_src):
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
return resp + " File not found."
with open(file_src, "rb") as f:
msg = "Uploading file: {}. Status: {}\n"
try:
resp = str(self.storbinary("STOR " + file_name, f))
except Error as e:
resp = str(e)
msg = msg.format(file_name, resp)
log(msg)
else:
msg = msg.format(file_name, resp)
if callback:
callback(msg)
return resp
def upload_dir(self, path, callback=None):
""" Uploads directory to FTP with all contents.
Creates a leaf directory and all intermediate ones. This is recursive.
"""
resp = "200"
msg = "Uploading directory: {}. Status: {}"
try:
files = os.listdir(path)
except OSError as e:
log(e)
else:
os.chdir(path)
for f in files:
file = r"{}{}".format(path, f)
if os.path.isfile(file):
self.send_file(f, path, callback)
elif os.path.isdir(file):
try:
self.mkd(f)
except Error:
pass # NOP
try:
self.cwd(f)
except Error as e:
resp = str(e)
log(msg.format(f, resp))
else:
self.upload_dir(file + "/")
self.cwd("..")
os.chdir("..")
if callback:
callback(msg.format(path, resp))
return resp
# ****************** Deletion ******************** #
def delete_picons(self, callback, dest=None, files_filter=None):
if dest:
try:
self.cwd(dest)
except Error as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), self.nlst()):
self.delete_file(file, callback)
def delete_file(self, file, callback=log):
msg = "Deleting file: {}. Status: {}\n"
try:
resp = self.delete(file)
except Error as e:
resp = str(e)
msg = msg.format(file, resp)
log(msg)
else:
msg = msg.format(file, resp)
if callback:
callback(msg)
return resp
def delete_dir(self, path, callback=None):
files = []
self.dir(path, files.append)
for f in files:
f_data = f.split()
name = " ".join(f_data[8:])
f_path = path + "/" + name
if f_data[0][0] == "d":
self.delete_dir(f_path, callback)
else:
self.delete_file(f_path, callback)
msg = "Remove directory {}. Status: {}\n"
try:
resp = self.rmd(path)
except Error as e:
msg = msg.format(path, e)
log(msg)
return "500"
else:
msg = msg.format(path, resp)
log(msg.rstrip())
if callback:
callback(msg)
return resp
def rename_file(self, from_name, to_name, callback=None):
msg = "File rename: {}. Status: {}\n"
try:
resp = self.rename(from_name, to_name)
except Error as e:
resp = str(e)
msg = msg.format(from_name, resp)
log(msg)
else:
msg = msg.format(from_name, resp)
if callback:
callback(msg)
return resp
def download_data(*, settings, download_type=DownloadType.ALL, callback=log, files_filter=None):
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8" ftp.encoding = "utf-8"
callback("FTP OK.\n") callback("FTP OK.\n")
save_path = settings.data_local_path save_path = settings.data_local_path
@@ -72,18 +334,17 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print, f
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS: if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(settings.services_path) ftp.cwd(settings.services_path)
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
for file in filter(lambda f: f.endswith(file_list), ftp.nlst()): ftp.download_files(save_path, file_list, callback)
download_file(ftp, file, save_path, callback)
# *.xml and webtv # *.xml and webtv
if download_type in (DownloadType.ALL, DownloadType.SATELLITES): if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
download_xml(ftp, save_path, settings.satellites_xml_path, STC_XML_FILE, callback) ftp.download_xml(save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
if download_type in (DownloadType.ALL, DownloadType.WEBTV): if download_type in (DownloadType.ALL, DownloadType.WEBTV):
download_xml(ftp, save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback) ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.PICONS: if download_type is DownloadType.PICONS:
picons_path = settings.picons_local_path picons_path = settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True) os.makedirs(os.path.dirname(picons_path), exist_ok=True)
download_picons(ftp, settings.picons_path, picons_path, callback, files_filter) ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
# epg.dat # epg.dat
if download_type is DownloadType.EPG: if download_type is DownloadType.EPG:
stb_path = settings.services_path stb_path = settings.services_path
@@ -93,14 +354,13 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print, f
save_path = epg_options.get("epg_dat_path", save_path) save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(stb_path) ftp.cwd(stb_path)
for file in filter(lambda f: f.endswith("epg.dat"), ftp.nlst()): ftp.download_files(save_path, "epg.dat", callback)
download_file(ftp, file, save_path, callback)
callback("\nDone.\n") callback("\nDone.\n")
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False, def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
callback=print, done_callback=None, use_http=False, files_filter=None): callback=log, done_callback=None, use_http=False, files_filter=None):
s_type = settings.setting_type s_type = settings.setting_type
data_path = settings.data_local_path data_path = settings.data_local_path
host = settings.host host = settings.host
@@ -119,6 +379,8 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
message = "All user data will be reloaded!" message = "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES: elif download_type is DownloadType.SATELLITES:
message = "Satellites.xml file will be updated!" message = "Satellites.xml file will be updated!"
elif download_type is DownloadType.PICONS:
message = "Picons will be updated!"
params = urlencode({"text": message, "type": 2, "timeout": 5}) params = urlencode({"text": message, "type": 2, "timeout": 5})
ht.send((url + "message?{}".format(params), "Sending info message... ")) ht.send((url + "message?{}".format(params), "Sending info message... "))
@@ -128,46 +390,50 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ht.send((url + "powerstate?newstate=0", "Toggle Standby ")) ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
time.sleep(2) time.sleep(2)
else: else:
# telnet if download_type is not DownloadType.PICONS:
tn = telnet(host=host, # telnet
user=settings.telnet_user, tn = telnet(host=host,
password=settings.telnet_password, user=settings.telnet_user,
timeout=settings.telnet_timeout) password=settings.telnet_password,
next(tn) timeout=settings.telnet_timeout)
# terminate enigma or neutrino next(tn)
tn.send("init 4") # terminate enigma or neutrino
callback("Telnet initialization ...\n")
tn.send("init 4")
callback("Stopping GUI...\n")
with FTP(host=host, user=settings.user, passwd=settings.password) as ftp: with UtfFTP(host=host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8" ftp.encoding = "utf-8"
callback("FTP OK.\n") callback("FTP OK.\n")
sat_xml_path = settings.satellites_xml_path sat_xml_path = settings.satellites_xml_path
services_path = settings.services_path services_path = settings.services_path
if download_type is DownloadType.SATELLITES: if download_type is DownloadType.SATELLITES:
upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback) ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV: if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback) ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.BOUQUETS: if download_type is DownloadType.BOUQUETS:
ftp.cwd(services_path) ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback) ftp.upload_bouquets(data_path, remove_unused, callback)
if download_type is DownloadType.ALL: if download_type is DownloadType.ALL:
upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback) ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP: if s_type is SettingsType.NEUTRINO_MP:
upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback) ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
ftp.cwd(services_path) ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback) ftp.upload_bouquets(data_path, remove_unused, callback)
upload_files(ftp, data_path, DATA_FILES_LIST, callback) ftp.upload_files(data_path, DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS: if download_type is DownloadType.PICONS:
upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback, files_filter) ftp.upload_picons(settings.picons_local_path, settings.picons_path, callback, files_filter)
if tn and not use_http: if tn and not use_http:
# resume enigma or restart neutrino # resume enigma or restart neutrino
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6") 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: elif ht and use_http:
if download_type is DownloadType.BOUQUETS: if download_type is DownloadType.BOUQUETS:
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets.")) ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
@@ -184,80 +450,13 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ht.close() ht.close()
def upload_bouquets(ftp, data_path, remove_unused, callback):
if remove_unused:
remove_unused_bouquets(ftp, callback)
upload_files(ftp, data_path, BQ_FILES_LIST, callback)
def upload_files(ftp, data_path, file_list, callback):
for file_name in os.listdir(data_path):
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
continue
if file_name.endswith(file_list):
send_file(file_name, data_path, ftp, callback)
def remove_unused_bouquets(ftp, callback):
for file in filter(lambda f: f.endswith(("tv", "radio", "bouquets.xml", "ubouquets.xml")), ftp.nlst()):
callback("Deleting file: {}. Status: {}\n".format(file, ftp.delete(file)))
def upload_xml(ftp, data_path, xml_path, xml_files, callback):
""" Used for transfer *.xml files. """
ftp.cwd(xml_path)
for xml_file in xml_files:
send_file(xml_file, data_path, ftp, callback)
def download_xml(ftp, data_path, xml_path, xml_files, callback):
""" Used for download *.xml files. """
ftp.cwd(xml_path)
list(map(lambda f: download_file(ftp, f, data_path, callback), (f for f in ftp.nlst() if f.endswith(xml_files))))
# ***************** Picons *******************# # ***************** Picons *******************#
def upload_picons(ftp, src, dest, callback, files_filter=None):
try:
ftp.cwd(dest)
except error_perm as e:
if str(e).startswith("550"):
ftp.mkd(dest) # if not exist
ftp.cwd(dest)
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
send_file(file_name, src, ftp, callback)
def download_picons(ftp, src, dest, callback, files_filter=None):
try:
ftp.cwd(src)
except error_perm as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), ftp.nlst()):
download_file(ftp, file, dest, callback)
def delete_picons(ftp, callback, dest=None, files_filter=None):
if dest:
try:
ftp.cwd(dest)
except error_perm as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), ftp.nlst()):
callback("Delete file: {}. Status: {}\n".format(file, ftp.delete(file)))
def remove_picons(*, settings, callback, done_callback=None, files_filter=None): def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8" ftp.encoding = "utf-8"
callback("FTP OK.\n") callback("FTP OK.\n")
delete_picons(ftp, callback, settings.picons_path, files_filter) ftp.delete_picons(callback, settings.picons_path, files_filter)
if done_callback: if done_callback:
done_callback() done_callback()
@@ -266,29 +465,13 @@ def picons_filter_function(files_filter=None):
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF) return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
def download_file(ftp, name, save_path, callback):
with open(save_path + name, "wb") as f:
callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write))))
def send_file(file_name, path, ftp, callback):
""" Opens the file in binary mode and transfers into receiver """
file_src = path + file_name
if not os.path.isfile(file_src):
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
return
with open(file_src, "rb") as f:
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
def http(user, password, url, callback, use_ssl=False): def http(user, password, url, callback, use_ssl=False):
init_auth(user, password, url, use_ssl) init_auth(user, password, url, use_ssl)
data = get_post_data(url, password, url) data = get_post_data(url, password, url)
while True: while True:
url, message = yield url, message = yield
resp = get_response(HttpRequestType.TEST, url, data).get("e2statetext", None) resp = get_response(HttpAPI.Request.TEST, url, data).get("e2statetext", None)
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else "")) callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
@@ -301,11 +484,11 @@ def telnet(host, port=23, user="", password="", timeout=5):
time.sleep(1) time.sleep(1)
command = yield command = yield
if user != "": if user != "":
tn.read_until(b"login: ") tn.read_until(b"login: ", timeout)
tn.write(user.encode("utf-8") + b"\n") tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout) time.sleep(timeout)
if password != "": if password != "":
tn.read_until(b"Password: ") tn.read_until(b"Password: ", timeout)
tn.write(password.encode("utf-8") + b"\n") tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout) time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8")) tn.write("{}\r\n".format(command).encode("utf-8"))
@@ -322,6 +505,58 @@ def telnet(host, port=23, user="", password="", timeout=5):
class HttpAPI: class HttpAPI:
__MAX_WORKERS = 4 __MAX_WORKERS = 4
class Request(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
TEST = None
TOKEN = "session"
# Player
PLAY = "mediaplayerplay?file="
PLAYER_LIST = "mediaplayerlist?path=playlist"
PLAYER_PLAY = "mediaplayercmd?command=play"
PLAYER_NEXT = "mediaplayercmd?command=next"
PLAYER_PREV = "mediaplayercmd?command=previous"
PLAYER_STOP = "mediaplayercmd?command=stop"
PLAYER_REMOVE = "mediaplayerremove?file="
# Remote control
POWER = "powerstate?newstate="
REMOTE = "remotecontrol?command="
VOL = "vol?set=set"
# EPG
EPG = "epgservice?sRef="
# Timer
TIMER = ""
TIMER_LIST = "timerlist"
# Screenshot
GRUB = "grab?format=jpg&"
class Remote(str, Enum):
""" Args for HttpRequestType [REMOTE] class. """
UP = "103"
LEFT = "105"
RIGHT = "106"
DOWN = "108"
MENU = "139"
EXIT = "174"
OK = "352"
RED = "398"
GREEN = "399"
YELLOW = "400"
BLUE = "401"
class Power(str, Enum):
""" Args for HttpRequestType [POWER] class. """
TOGGLE_STANDBY = "0"
DEEP_STANDBY = "1"
REBOOT = "2"
RESTART_GUI = "3"
WAKEUP = "4"
STANDBY = "5"
def __init__(self, settings): def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS) self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
@@ -342,13 +577,19 @@ class HttpAPI:
url = self._base_url + req_type.value url = self._base_url + req_type.value
data = self._data data = self._data
if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM: if req_type is self.Request.ZAP or req_type is self.Request.STREAM:
url += urllib.parse.quote(ref) url += urllib.parse.quote(ref)
elif req_type is HttpRequestType.PLAY or req_type is HttpRequestType.PLAYER_REMOVE: 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 += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
elif req_type is HttpRequestType.GRUB: elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security. data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref) 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 += ref
def done_callback(f): def done_callback(f):
callback(f.result()) callback(f.result())
@@ -363,12 +604,12 @@ class HttpAPI:
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port) 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) self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, self._main_url, use_ssl) init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(self._main_url, HttpRequestType.TOKEN.value) url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
s_id = get_session_id(user, password, url) s_id = get_session_id(user, password, url)
if s_id != "0": if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8") self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(HttpRequestType.INFO, None, self.init_callback) self.send(self.Request.INFO, None, self.init_callback)
def init_callback(self, info): def init_callback(self, info):
if info: if info:
@@ -391,24 +632,30 @@ class HttpAPI:
def get_response(req_type, url, data=None): def get_response(req_type, url, data=None):
try: try:
with urlopen(Request(url, data=data), timeout=10) as f: with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT: if req_type is HttpAPI.Request.STREAM or req_type is HttpAPI.Request.STREAM_CURRENT:
return {"m3u": f.read().decode("utf-8")} return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpRequestType.GRUB: elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()} return {"img_data": f.read()}
elif req_type is HttpRequestType.CURRENT: elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"): 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 return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpRequestType.PLAYER_LIST: elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")] 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: else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()} return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e: except HTTPError as e:
if req_type is HttpRequestType.TEST: if req_type is HttpAPI.Request.TEST:
raise e raise e
return {"error_code": e.code} return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e: except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpRequestType.TEST: if req_type is HttpAPI.Request.TEST:
raise e raise e
except ETree.ParseError as e: except ETree.ParseError as e:
log("Parsing response error: {}".format(e)) log("Parsing response error: {}".format(e))
@@ -435,11 +682,11 @@ def init_auth(user, password, url, use_ssl=False):
def get_session_id(user, password, url): def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8") data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return get_response(HttpRequestType.TOKEN, url, data=data).get("e2sessionid", "0") return get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_post_data(base_url, password, user): def get_post_data(base_url, password, user):
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpRequestType.TOKEN.value)) s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN.value))
data = None data = None
if s_id != "0": if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8") data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
@@ -465,7 +712,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
data = get_post_data(base_url, password, user) data = get_post_data(base_url, password, user)
try: try:
return get_response(HttpRequestType.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "") return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
except (RemoteDisconnected, URLError, HTTPError) as e: except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e) raise TestException(e)

View File

@@ -12,6 +12,7 @@ def get_blacklist(path):
with open(path + __FILE_NAME, "r") as file: with open(path + __FILE_NAME, "r") as file:
# filter empty values and "\n" # filter empty values and "\n"
return {*list(filter(None, (x.strip() for x in file.readlines())))} return {*list(filter(None, (x.strip() for x in file.readlines())))}
return {}
def write_blacklist(path, channels): def write_blacklist(path, channels):

View File

@@ -25,11 +25,16 @@ def write_services(path, services, format_version=4):
def write_to_lamedb(path, services): def write_to_lamedb(path, services):
""" Writing lamedb file ver.4 """ """ Writing lamedb file ver.4 """
with open(path + _FILE_NAME, "w") as file:
file.writelines(get_services_lines(services))
def get_services_lines(services):
""" Returns a list of strings from services for lamedb [v.4]. """
lines = [_HEADER.format(4), "\ntransponders\n"] lines = [_HEADER.format(4), "\ntransponders\n"]
tr_lines = [] tr_lines = []
services_lines = ["end\nservices\n"] services_lines = ["end\nservices\n"]
tr_set = set() tr_set = set()
for srv in services: for srv in services:
data_id = str(srv.data_id).split(_SEP) data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3]) tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
@@ -44,12 +49,12 @@ def write_to_lamedb(path, services):
lines.extend(tr_lines) lines.extend(tr_lines)
lines.extend(services_lines) lines.extend(services_lines)
lines.append("end\n" + _END_LINE) lines.append("end\n" + _END_LINE)
with open(path + _FILE_NAME, "w") as file:
file.writelines(lines) return lines
def write_to_lamedb5(path, services): def write_to_lamedb5(path, services):
""" Writing lamedb5 file """ """ Writing lamedb5 file. """
lines = [_HEADER.format(5) + "\n"] lines = [_HEADER.format(5) + "\n"]
services_lines = [] services_lines = []
tr_set = set() tr_set = set()
@@ -73,7 +78,7 @@ def write_to_lamedb5(path, services):
def parse(path, version=4): def parse(path, version=4):
""" Parsing lamedb """ """ Parsing lamedb. """
if version == 4: if version == 4:
return parse_v4(path) return parse_v4(path)
elif version == 5: elif version == 5:
@@ -82,7 +87,7 @@ def parse(path, version=4):
def parse_v3(services, transponders, path): def parse_v3(services, transponders, path):
""" Parsing version 3 """ """ Parsing version 3. """
for t in transponders: for t in transponders:
tr = transponders[t].lower() tr = transponders[t].lower()
tr_type = tr[0:1] tr_type = tr[0:1]
@@ -108,32 +113,37 @@ def parse_v3(services, transponders, path):
def parse_v4(path): def parse_v4(path):
""" Parsing version 4 """ """ Parsing version 4. """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file: with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try: try:
data = str(file.read()) data = str(file.read())
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e)) log("lamedb parse error: " + str(e))
else: else:
transponders, sep, services = data.partition("transponders") # 1 step return get_services_list(data, path)
pattern = re.compile("/[34]/$")
match = re.search(pattern, transponders)
if not match:
msg = "lamedb parsing error: unsupported format."
log(msg)
raise SyntaxError(msg)
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
if match.group() == "/3/": def get_services_list(data, path=None):
return parse_v3(services.split("\n"), parse_transponders(transponders.split("/")), path) """ Returns a list of services from a string data representation. """
transponders, sep, services = data.partition("transponders") # 1 step
pattern = re.compile("/[34]/$")
match = re.search(pattern, transponders)
if not match:
msg = "lamedb parsing error: unsupported format."
log(msg)
raise SyntaxError(msg)
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path) transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
if match.group() == "/3/":
return parse_v3(services.split("\n"), parse_transponders(transponders.split("/")), path)
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
def parse_v5(path): def parse_v5(path):
""" Parsing version 5 """ """ Parsing version 5. """
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file: with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines() lns = file.readlines()
@@ -141,9 +151,9 @@ def parse_v5(path):
raise SyntaxError("lamedb v.5 parsing error: unsupported format.") raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
trs, srvs = {}, [""] trs, srvs = {}, [""]
for l in lns: for line in lns:
if l.startswith("s:"): if line.startswith("s:"):
srv_data = l.strip("s:").split(",", 2) srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"") srv_data[1] = srv_data[1].strip("\"")
data_len = len(srv_data) data_len = len(srv_data)
if data_len == 3: if data_len == 3:
@@ -151,15 +161,15 @@ def parse_v5(path):
elif data_len == 2: elif data_len == 2:
srv_data.append("p:") srv_data.append("p:")
srvs.extend(srv_data) srvs.extend(srv_data)
elif l.startswith("t:"): elif line.startswith("t:"):
tr, srv = l.split(",") tr, srv = line.split(",")
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1) trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
return parse_services(srvs, trs, path) return parse_services(srvs, trs, path)
def parse_transponders(arg): def parse_transponders(arg):
""" Parsing transponders """ """ Parsing transponders. """
transponders = {} transponders = {}
for ar in arg: for ar in arg:
tr = ar.replace("\n", "").split("\t") tr = ar.replace("\n", "").split("\t")
@@ -170,9 +180,9 @@ def parse_transponders(arg):
def parse_services(services, transponders, path): def parse_services(services, transponders, path):
""" Parsing services """ """ Parsing services. """
services_list = [] services_list = []
blacklist = str(get_blacklist(path)) blacklist = get_blacklist(path) if path else {}
srvs = split(services, 3) srvs = split(services, 3)
if srvs[0][0] == "": # remove first empty element if srvs[0][0] == "": # remove first empty element
srvs.remove(srvs[0]) srvs.remove(srvs[0])
@@ -208,12 +218,13 @@ def parse_services(services, transponders, path):
# For comparison in bouquets. Needed in upper case!!! # For comparison in bouquets. Needed in upper case!!!
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid) fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid) picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid)
s_id = "1:0:{:X}:{}:{}:{}:{}:0:0:0:".format(srv_type, ssid, tid, nid, onid)
all_flags = srv[2].split(",") all_flags = srv[2].split(",")
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
flags = list(filter(lambda x: x.startswith("f:"), all_flags)) flags = list(filter(lambda x: x.startswith("f:"), all_flags))
hide = HIDE_ICON if flags and Flag.is_hide(int(flags[0][2:])) else None hide = HIDE_ICON if flags and Flag.is_hide(int(flags[0][2:])) else None
locked = LOCKED_ICON if fav_id in blacklist else None locked = LOCKED_ICON if s_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags)) package = list(filter(lambda x: x.startswith("p:"), all_flags))
package = package[0][2:] if package else "" package = package[0][2:] if package else ""

View File

@@ -21,15 +21,27 @@ class StreamType(Enum):
E_SERVICE_URI = "8193" E_SERVICE_URI = "8193"
def parse_m3u(path, s_type): def parse_m3u(path, s_type, detect_encoding=True):
with open(path) as file: with open(path, "rb") as file:
data = file.read()
encoding = "utf-8"
if detect_encoding:
try:
import chardet
except ModuleNotFoundError:
pass
else:
enc = chardet.detect(data)
encoding = enc.get("encoding", "utf-8")
aggr = [None] * 10 aggr = [None] * 10
services = [] services = []
groups = set() groups = set()
counter = 0 counter = 0
name = None name = None
for line in file.readlines(): for line in str(data, encoding=encoding, errors="ignore").splitlines():
if line.startswith("#EXTINF"): if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip() name = line[1 + line.index(","):].strip()
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2: elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:

View File

@@ -612,6 +612,30 @@ class Settings:
# *********** Appearance *********** # # *********** Appearance *********** #
@property
def dark_mode(self):
return self._settings.get("dark_mode", False)
@dark_mode.setter
def dark_mode(self, value):
self._settings["dark_mode"] = value
@property
def alternate_layout(self):
return self._settings.get("alternate_layout", IS_DARWIN)
@alternate_layout.setter
def alternate_layout(self, value):
self._settings["alternate_layout"] = value
@property
def bq_details_first(self):
return self._settings.get("bq_details_first", False)
@bq_details_first.setter
def bq_details_first(self, value):
self._settings["bq_details_first"] = value
@property @property
def is_themes_support(self): def is_themes_support(self):
return self._settings.get("is_themes_support", False) return self._settings.get("is_themes_support", False)
@@ -678,6 +702,17 @@ class Settings:
def debug_mode(self, value): def debug_mode(self, value):
self._settings["debug_mode"] = value self._settings["debug_mode"] = value
# **************** Experimental **************** #
@property
def is_enable_experimental(self):
""" Allows experimental functionality. """
return self._settings.get("enable_experimental", False)
@is_enable_experimental.setter
def is_enable_experimental(self, value):
self._settings["enable_experimental"] = value
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@@ -2,11 +2,10 @@ import glob
import os import os
import re import re
import shutil import shutil
from collections import namedtuple from collections import namedtuple
from html.parser import HTMLParser from html.parser import HTMLParser
from app.commons import run_task from app.commons import run_task, log
from app.settings import SettingsType from app.settings import SettingsType
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}" _ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
@@ -33,9 +32,9 @@ class PiconsParser(HTMLParser):
self.picons = [] self.picons = []
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):
if tag == 'td': if tag == "td":
self._is_td = True self._is_td = True
if tag == 'th': if tag == "th":
self._is_th = True self._is_th = True
if tag == "img": if tag == "img":
self._current_row.append(attrs[0][1]) self._current_row.append(attrs[0][1])
@@ -46,16 +45,16 @@ class PiconsParser(HTMLParser):
self._current_cell.append(data.strip()) self._current_cell.append(data.strip())
def handle_endtag(self, tag): def handle_endtag(self, tag):
if tag == 'td': if tag == "td":
self._is_td = False self._is_td = False
elif tag == 'th': elif tag == "th":
self._is_th = False self._is_th = False
if tag in ('td', 'th'): if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell).strip() final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell) self._current_row.append(final_cell)
self._current_cell = [] self._current_cell = []
elif tag == 'tr': elif tag == "tr":
row = self._current_row row = self._current_row
ln = len(row) ln = len(row)
@@ -80,6 +79,10 @@ class PiconsParser(HTMLParser):
@staticmethod @staticmethod
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2): def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
if not os.path.isfile(open_path):
log("PiconsParser error [parse]. No such file or directory: {}".format(open_path))
return
with open(open_path, encoding="utf-8", errors="replace") as f: with open(open_path, encoding="utf-8", errors="replace") as f:
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W") neg_pos = pos.endswith("W")
@@ -105,8 +108,7 @@ class PiconsParser(HTMLParser):
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name) shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
except (TypeError, ValueError) as e: except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e) msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
# log(msg) log(msg)
print(msg)
@staticmethod @staticmethod
def format(ssid, on_id, namespace, picon_ids, s_type): def format(ssid, on_id, namespace, picon_ids, s_type):

View File

@@ -1,15 +1,20 @@
""" Module for download satellites from internet ("flysat.com") """ Module for downloading satellites, transponders ans services from the web.
for replace or update current satellites.xml file.
Sources: www.flysat.com, www.lyngsat.com.
Replaces or updates the current satellites.xml file.
""" """
import re import re
import requests
from enum import Enum from enum import Enum
from html.parser import HTMLParser from html.parser import HTMLParser
import requests
from app.commons import log from app.commons import log
from app.eparser import Satellite, Transponder, is_transponder_valid from app.eparser import Satellite, Transponder, is_transponder_valid
from app.eparser.ecommons import PLS_MODE 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 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0"}
class SatelliteSource(Enum): class SatelliteSource(Enum):
@@ -22,11 +27,55 @@ class SatelliteSource(Enum):
return src.value return src.value
class Cell:
""" Cell representation for table parsers. """
__slots__ = ["_text", "_url", "_img"]
def __init__(self, text=None, link=None, img=None):
self._text = text
self._url = link
self._img = img
def __repr__(self):
return "Cell({}, {}, {})".format(self._text, self._url, self._img)
def __str__(self):
return "<Cell(text={}, link={}, img={})>".format(self._text, self._url, self._img)
def __iter__(self):
return (x for x in (self._text, self._url, self._img))
def __len__(self):
return 3
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = value
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value
@property
def img(self):
return self._img
@img.setter
def img(self, value):
self._img = value
class SatellitesParser(HTMLParser): class SatellitesParser(HTMLParser):
""" Parser for satellite html page. """ """ Parser for satellite html page. """
_HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/59.02"}
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '): def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
HTMLParser.__init__(self) HTMLParser.__init__(self)
@@ -42,9 +91,9 @@ class SatellitesParser(HTMLParser):
self._source = source self._source = source
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):
if tag == 'td': if tag == "td":
self._is_td = True self._is_td = True
if tag == 'tr': if tag == "tr":
self._is_th = True self._is_th = True
if tag == "a": if tag == "a":
self._current_row.append(attrs[0][1]) self._current_row.append(attrs[0][1])
@@ -55,16 +104,16 @@ class SatellitesParser(HTMLParser):
self._current_cell.append(data.strip()) self._current_cell.append(data.strip())
def handle_endtag(self, tag): def handle_endtag(self, tag):
if tag == 'td': if tag == "td":
self._is_td = False self._is_td = False
elif tag == 'tr': elif tag == "tr":
self._is_th = False self._is_th = False
if tag in ('td', 'th'): if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell).strip() final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell) self._current_row.append(final_cell)
self._current_cell = [] self._current_cell = []
elif tag == 'tr': elif tag == "tr":
row = self._current_row row = self._current_row
self._rows.append(row) self._rows.append(row)
self._current_row = [] self._current_row = []
@@ -80,7 +129,7 @@ class SatellitesParser(HTMLParser):
for src in SatelliteSource.get_sources(self._source): for src in SatelliteSource.get_sources(self._source):
try: try:
request = requests.get(url=src, headers=self._HEADERS) request = requests.get(url=src, headers=_HEADERS)
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
log(repr(e)) log(repr(e))
return [] return []
@@ -98,17 +147,24 @@ class SatellitesParser(HTMLParser):
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows))) return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
elif self._source is SatelliteSource.LYNGSAT: elif self._source is SatelliteSource.LYNGSAT:
extra_pattern = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html") extra_pattern = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html")
base_url = "https://www.lyngsat.com/" base_url = "https://www.lyngsat.com/"
sats = [] sats = []
names = set()
current_pos = "0" current_pos = "0"
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows): for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
r_len = len(row) r_len = len(row)
if r_len == 7: if r_len == 7:
current_pos = self.parse_position(row[2]) current_pos = self.parse_position(row[2])
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ") name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, row[5], base_url + row[1], False)) # [all in one] satellites if name not in names:
sats.append((row[4], current_pos, row[5], base_url + row[3], False)) # [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 if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row)) data = list(filter(None, row))
urls = set() urls = set()
@@ -146,7 +202,7 @@ class SatellitesParser(HTMLParser):
""" Getting transponders(sorted by frequency). """ """ Getting transponders(sorted by frequency). """
self._rows.clear() self._rows.clear()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=self._HEADERS) request = requests.get(url=url, headers=_HEADERS)
reason = request.reason reason = request.reason
trs = [] trs = []
if reason == "OK": if reason == "OK":
@@ -247,5 +303,198 @@ class SatellitesParser(HTMLParser):
trs.append(tr) trs.append(tr)
class ServicesParser(HTMLParser):
""" Services parser for LYNGSAT source. """
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' '):
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._TR_PAT = re.compile(r"(DVB-S[2]?)/?(.*PSK)?\s+SR\s+(\d+)\s+FEC\s+(\d/\d).*ONID/TID:\s+(\d+)/(\d+)\s+.*")
self._PTR_PAT = re.compile(r".*?(\d+\.\d°[EW]):\s+(\d+)\s+([RLHV]).*")
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
self._S2_TR = "{}:{}:{}:{}"
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._current_row = []
self._current_cell_text = []
self._current_cell = Cell()
self._rows = []
self._source = source
def handle_starttag(self, tag, attrs):
if tag == "td":
self._is_td = True
elif tag == "tr":
self._is_th = True
elif tag == "a" and not self._current_cell.url:
self._current_cell.url = attrs[0][1]
elif tag == "img":
img_link = attrs[0][1]
if img_link.startswith("/logo/"):
self._current_cell.img = img_link
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell_text.append(data.strip())
def handle_endtag(self, tag):
if tag == "td":
self._is_td = False
elif tag == "tr":
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
self._current_row.append(self._current_cell)
self._current_cell_text = []
self._current_cell = Cell()
elif tag == "tr":
row = self._current_row
self._rows.append(row)
self._current_row = []
def error(self, message):
log("ServicesParser error: {}".format(message))
def init_data(self, url):
""" Initializes data for the given URL. """
if self._source is not SatelliteSource.LYNGSAT:
raise ValueError("Unsupported source: {}!".format(self._source.name))
self._rows.clear()
request = requests.get(url=url, headers=_HEADERS)
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
raise ValueError(reason)
def get_transponders_links(self, sat_url):
""" Returns transponder links. """
try:
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)]
return []
def get_transponder_services(self, tr_url, sat_position=None, use_pids=False):
""" Returns services for given transponder.
@param tr_url: transponder URL.
@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.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"
tr_found = False
pos_found = False
tr = None
# Transponder
for r in filter(lambda x: x and len(x) == 2, self._rows):
if not pos_found:
pos_tr = re.match(self._PTR_PAT, r[1].text)
if pos_tr:
if not sat_position:
pos = int(SatellitesParser.get_position(
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
freq = int(pos_tr.group(2))
pol = get_key_by_value(POLARIZATION, pos_tr.group(3))
pos_found = True
if pos_found and not tr_found:
td = re.match(self._TR_PAT, r[1].text) or re.match(self._TR_PAT, r[0].text)
if td:
sys, mod, sr, _fec, nid, tid = td.group(1), td.group(2), td.group(3), td.group(4), td.group(
5), td.group(6)
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)
tr_found = True
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))
return []
# Services
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
sid, name, cas, pkg, s_type, v_pid, a_pid = r[0].text, r[2].text, r[4].text, r[5].text, r[
6].text.strip(), r[7].text, r[8].text.split()
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)))
srv = Service(flags_cas=flags,
transponder_type="s",
coded=None,
service=name,
locked=None,
hide=None,
package=pkg,
service_type=_s_type,
picon=r[1].img,
picon_id=picon_id,
ssid=sid,
freq=freq,
rate=sr,
pol=pol,
fec=fec,
system=sys,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=tr)
services.append(srv)
except ValueError as e:
log("ServicesParser error [get transponder services]: {}".format(e))
return services
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@@ -43,7 +43,10 @@ class YouTube:
self._callback = callback self._callback = callback
if self._settings.enable_yt_dl: if self._settings.enable_yt_dl:
self._yt_dl = YouTubeDL.get_instance(self._settings, callback=self._callback) try:
self._yt_dl = YouTubeDL.get_instance(self._settings, callback=self._callback)
except YouTubeException:
pass # NOP
@classmethod @classmethod
def get_instance(cls, settings, callback=log): def get_instance(cls, settings, callback=log):
@@ -126,6 +129,21 @@ class YouTube:
return None, rsn return None, rsn
def get_yt_playlist(self, list_id, url=None):
""" Returns tuple from the playlist header and list of tuples (title, video id). """
if self._settings.enable_yt_dl and url:
try:
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
info = self._yt_dl.get_info(url, skip_errors=False)
if "url" in info:
info = self._yt_dl.get_info(info.get("url"), skip_errors=False)
return info.get("title", ""), [(e.get("title", ""), e.get("id", "")) for e in info.get("entries", [])]
finally:
# Restoring default options
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
return PlayListParser.get_yt_playlist(list_id)
class PlayListParser(HTMLParser): class PlayListParser(HTMLParser):
""" Very simple parser to handle YouTube playlist pages. """ """ Very simple parser to handle YouTube playlist pages. """
@@ -136,6 +154,7 @@ class PlayListParser(HTMLParser):
self._header = "" self._header = ""
self._playlist = [] self._playlist = []
self._is_script = False self._is_script = False
self._scr_start = ('var ytInitialData = ', 'window["ytInitialData"] = ')
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):
if tag == "script": if tag == "script":
@@ -144,8 +163,11 @@ class PlayListParser(HTMLParser):
def handle_data(self, data): def handle_data(self, data):
if self._is_script: if self._is_script:
data = data.lstrip() data = data.lstrip()
if data.startswith('window["ytInitialData"] = '): if data.startswith(self._scr_start):
data = data.split(";")[0].lstrip('window["ytInitialData"] = ') data = data.split(";")[0]
for s in self._scr_start:
data = data.lstrip(s)
try: try:
resp = json.loads(data) resp = json.loads(data)
except JSONDecodeError as e: except JSONDecodeError as e:
@@ -161,7 +183,7 @@ class PlayListParser(HTMLParser):
ct = resp.get("contents", None) ct = resp.get("contents", None)
if ct: if ct:
for d in [(d.get("title", {}).get("simpleText", ""), for d in [(d.get("title", {}).get("runs", [{}])[0].get("text", ""),
d.get("videoId", "")) for d in flat("playlistVideoRenderer", ct)]: d.get("videoId", "")) for d in flat("playlistVideoRenderer", ct)]:
self._playlist.append(d) self._playlist.append(d)
self._is_script = False self._is_script = False
@@ -202,6 +224,7 @@ class YouTubeDL:
_DownloadError = None _DownloadError = None
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest" _LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL]. _OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
"extract_flat": False, # Do not resolve URLs, return the immediate result.
"quiet": True, # Do not print messages to stdout. "quiet": True, # Do not print messages to stdout.
"simulate": True, # Do not download the video files. "simulate": True, # Do not download the video files.
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to. "cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
@@ -245,7 +268,7 @@ class YouTubeDL:
if hasattr(youtube_dl.version, "__version__"): if hasattr(youtube_dl.version, "__version__"):
l_ver = self.get_last_release_id() l_ver = self.get_last_release_id()
cur_ver = youtube_dl.version.__version__ cur_ver = youtube_dl.version.__version__
if youtube_dl.version.__version__ < l_ver: if l_ver and youtube_dl.version.__version__ < l_ver:
msg = "youtube-dl has new release!\nCurrent: {}. Last: {}.".format(cur_ver, l_ver) msg = "youtube-dl has new release!\nCurrent: {}. Last: {}.".format(cur_ver, l_ver)
show_notification(msg) show_notification(msg)
log(msg) log(msg)
@@ -262,8 +285,11 @@ class YouTubeDL:
def get_last_release_id(): def get_last_release_id():
""" Getting last release id. """ """ Getting last release id. """
url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest" url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
with urlopen(url, timeout=10) as resp: try:
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0") with urlopen(url, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
except URLError as e:
log("YouTubeDLHelper error [get last release id]: {}".format(e))
def get_latest_release(self): def get_latest_release(self):
try: try:
@@ -310,8 +336,17 @@ class YouTubeDL:
self._callback("Update process. Please wait.", False) self._callback("Update process. Please wait.", False)
return {}, "" return {}, ""
info = self.get_info(url, skip_errors)
fmts = info.get("formats", None)
if fmts:
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
fm.get("format_id", "") in self._supported}, info.get("title", "")
return {}, info.get("title", "")
def get_info(self, url, skip_errors=False):
try: try:
info = self._dl.extract_info(url, download=False) return self._dl.extract_info(url, download=False)
except URLError as e: except URLError as e:
log(str(e)) log(str(e))
raise YouTubeException(e) raise YouTubeException(e)
@@ -319,13 +354,13 @@ class YouTubeDL:
log(str(e)) log(str(e))
if not skip_errors: if not skip_errors:
raise YouTubeException(e) raise YouTubeException(e)
else:
fmts = info.get("formats", None)
if fmts:
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
fm.get("format_id", "") in self._supported}, info.get("title", "")
return {}, info.get("title", "") def update_options(self, options):
self._dl.params.update(options)
@property
def options(self):
return self._dl.params
def flat(key, d): def flat(key, d):

View File

@@ -37,6 +37,10 @@
</item> </item>
</section> </section>
</submenu> </submenu>
<item>
<attribute name="label" translatable="yes">Import from Web</attribute>
<attribute name="action">app.on_import_from_web</attribute>
</item>
<item> <item>
<attribute name="label" translatable="yes">New empty configuration</attribute> <attribute name="label" translatable="yes">New empty configuration</attribute>
<attribute name="action">app.on_new_configuration</attribute> <attribute name="action">app.on_new_configuration</attribute>
@@ -47,6 +51,10 @@
<attribute name="label" translatable="yes">Open</attribute> <attribute name="label" translatable="yes">Open</attribute>
<attribute name="action">app.on_data_open</attribute> <attribute name="action">app.on_data_open</attribute>
</item> </item>
<item>
<attribute name="label" translatable="yes">Extract...</attribute>
<attribute name="action">app.on_archive_open</attribute>
</item>
<item> <item>
<attribute name="label" translatable="yes">Save</attribute> <attribute name="label" translatable="yes">Save</attribute>
<attribute name="action">app.on_data_save</attribute> <attribute name="action">app.on_data_save</attribute>

View File

@@ -59,14 +59,13 @@ class BackupDialog:
def show(self): def show(self):
self._dialog_window.show() self._dialog_window.show()
@run_idle
def init_data(self): def init_data(self):
try: if os.path.isdir(self._backup_path):
files = os.listdir(self._backup_path) for file in filter(lambda x: x.endswith(".zip"), os.listdir(self._backup_path)):
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
for file in filter(lambda x: x.endswith(".zip"), files):
self._model.append((file.rstrip(".zip"), False)) self._model.append((file.rstrip(".zip"), False))
else:
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
def on_restore_bouquets(self, item): def on_restore_bouquets(self, item):
self.restore(RestoreType.BOUQUETS) self.restore(RestoreType.BOUQUETS)
@@ -129,6 +128,8 @@ class BackupDialog:
append_text_to_tview(name + "\n", self._text_view) append_text_to_tview(name + "\n", self._text_view)
except FileNotFoundError as e: except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR) self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self._text_view.get_buffer().set_text("")
def restore(self, restore_type): def restore(self, restore_type):
model, paths = self._main_view.get_selection().get_selected_rows() model, paths = self._main_view.get_selection().get_selected_rows()

1849
app/ui/control.glade Normal file

File diff suppressed because it is too large Load Diff

680
app/ui/control.py Normal file
View File

@@ -0,0 +1,680 @@
""" Receiver control module via HTTP API. """
import os
from datetime import datetime
from enum import Enum
from urllib.parse import quote
from gi.repository import GLib
from .dialogs import get_dialogs_string, show_dialog, DialogType
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI
class ControlBox(Gtk.HBox):
_TIME_STR = "%Y-%m-%d %H:%M"
class Tool(Enum):
""" The currently displayed tool. """
REMOTE = "control"
EPG = "epg"
TIMERS = "timers"
TIMER = "timer"
class EpgRow(Gtk.ListBoxRow):
def __init__(self, event: dict, **properties):
super().__init__(**properties)
self._event_data = event
h_box = Gtk.HBox()
h_box.set_orientation(Gtk.Orientation.VERTICAL)
self._title = event.get("e2eventtitle", "")
title_label = Gtk.Label(self._title)
self._desc = event.get("e2eventdescription", "")
description = Gtk.Label()
description.set_markup("<i>{}</i>".format(self._desc))
description.set_line_wrap(True)
description.set_max_width_chars(25)
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
time_label = Gtk.Label()
time_label.set_margin_top(5)
self._time_header = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
time_label.set_markup("<b>{}</b>".format(self._time_header))
h_box.add(time_label)
h_box.add(title_label)
h_box.add(description)
sep = Gtk.Separator()
sep.set_margin_top(5)
h_box.add(sep)
h_box.set_spacing(5)
self.add(h_box)
self.show_all()
@property
def event_data(self):
return self._event_data
@property
def title(self):
return self._title
@property
def desc(self):
return self._desc
@property
def time_header(self):
return self._time_header
class TimerRow(Gtk.ListBoxRow):
_UI_PATH = UI_RESOURCES_PATH + "timer_row.glade"
def __init__(self, timer, **properties):
super().__init__(**properties)
self._timer = timer
builder = Gtk.Builder()
builder.add_from_string(get_dialogs_string(self._UI_PATH))
row_box = builder.get_object("timer_row_box")
name_label = builder.get_object("timer_name_label")
description_label = builder.get_object("timer_description_label")
service_name_label = builder.get_object("timer_service_name_label")
time_label = builder.get_object("timer_time_label")
name_label.set_text(timer.get("e2name", "") or "")
description_label.set_text(timer.get("e2description", "") or "")
service_name_label.set_text(timer.get("e2servicename", "") or "")
# Time
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
time_label.set_text("{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M")))
self.add(row_box)
self.show()
@property
def timer(self):
return self._timer
class TimerAction(Enum):
ADD = 0
EVENT = 1
CHANGE = 2
def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._http_api = http_api
self._settings = settings
self._update_epg = False
self._app = app
self._last_tool = self.Tool.REMOTE
self._timer_action = self.TimerAction.ADD
self._current_timer = {}
handlers = {"on_visible_tool": self.on_visible_tool,
"on_volume_changed": self.on_volume_changed,
"on_epg_press": self.on_epg_press,
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_timers_press": self.on_timers_press,
"on_timers_drag_data_received": self.on_timers_drag_data_received}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "control.glade")
builder.connect_signals(handlers)
self.add(builder.get_object("main_box_frame"))
self._stack = builder.get_object("stack")
self._screenshot_image = builder.get_object("screenshot_image")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
self._screenshot_check_button = builder.get_object("screenshot_check_button")
self._screenshot_check_button.bind_property("active", self._screenshot_image, "visible")
self._snr_value_label = builder.get_object("snr_value_label")
self._ber_value_label = builder.get_object("ber_value_label")
self._agc_value_label = builder.get_object("agc_value_label")
self._volume_button = builder.get_object("volume_button")
self._epg_list_box = builder.get_object("epg_list_box")
self._epg_list_box.set_filter_func(self.epg_filter_function)
self._epg_filter_entry = builder.get_object("epg_filter_entry")
self._timers_list_box = builder.get_object("timers_list_box")
self._app._control_revealer.bind_property("visible", self, "visible")
# Timers
self._timer_remove_button = builder.get_object("timer_remove_button")
self._timer_remove_button.bind_property("visible", builder.get_object("timer_edit_button"), "visible")
# Timer
self._timer_name_entry = builder.get_object("timer_name_entry")
self._timer_desc_entry = builder.get_object("timer_desc_entry")
self._timer_service_entry = builder.get_object("timer_service_entry")
self._timer_service_ref_entry = builder.get_object("timer_service_ref_entry")
self._timer_event_id_entry = builder.get_object("timer_event_id_entry")
self._timer_begins_entry = builder.get_object("timer_begins_entry")
self._timer_ends_entry = builder.get_object("timer_ends_entry")
self._timer_begins_calendar = builder.get_object("timer_begins_calendar")
self._timer_begins_hr_button = builder.get_object("timer_begins_hr_button")
self._timer_begins_min_button = builder.get_object("timer_begins_min_button")
self._timer_ends_calendar = builder.get_object("timer_ends_calendar")
self._timer_ends_hr_button = builder.get_object("timer_ends_hr_button")
self._timer_ends_min_button = builder.get_object("timer_ends_min_button")
self._timer_enabled_switch = builder.get_object("timer_enabled_switch")
self._timer_action_combo_box = builder.get_object("timer_action_combo_box")
self._timer_after_combo_box = builder.get_object("timer_after_combo_box")
self._timer_mo_check_button = builder.get_object("timer_mo_check_button")
self._timer_tu_check_button = builder.get_object("timer_tu_check_button")
self._timer_we_check_button = builder.get_object("timer_we_check_button")
self._timer_th_check_button = builder.get_object("timer_th_check_button")
self._timer_fr_check_button = builder.get_object("timer_fr_check_button")
self._timer_sa_check_button = builder.get_object("timer_sa_check_button")
self._timer_su_check_button = builder.get_object("timer_su_check_button")
self._timer_location_switch = builder.get_object("timer_location_switch")
self._timer_location_entry = builder.get_object("timer_location_entry")
self._timer_location_switch.bind_property("active", self._timer_location_entry, "sensitive")
# Disable DnD for timer entries.
self._timer_name_entry.drag_dest_unset()
self._timer_desc_entry.drag_dest_unset()
self._timer_service_entry.drag_dest_unset()
# DnD initialization for the timer list.
self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._timers_list_box.drag_dest_add_text_targets()
builder.get_object("stack_switcher").set_visible(settings.is_enable_experimental)
builder.get_object("epg_box").set_visible(settings.is_enable_experimental)
builder.get_object("timers_box").set_visible(settings.is_enable_experimental)
self.init_actions(app)
self.connect("hide", self.on_hide)
self.show()
def init_actions(self, app):
# Remote controller actions
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
app.set_action("on_ok", lambda a, v: self.on_remote_action(HttpAPI.Remote.OK))
app.set_action("on_menu", lambda a, v: self.on_remote_action(HttpAPI.Remote.MENU))
app.set_action("on_exit", lambda a, v: self.on_remote_action(HttpAPI.Remote.EXIT))
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
app.set_action("on_green", lambda a, v: self.on_remote_action(HttpAPI.Remote.GREEN))
app.set_action("on_yellow", lambda a, v: self.on_remote_action(HttpAPI.Remote.YELLOW))
app.set_action("on_blue", lambda a, v: self.on_remote_action(HttpAPI.Remote.BLUE))
# Power
app.set_action("on_standby", lambda a, v: self.on_power_action(HttpAPI.Power.STANDBY))
app.set_action("on_wake_up", lambda a, v: self.on_power_action(HttpAPI.Power.WAKEUP))
app.set_action("on_reboot", lambda a, v: self.on_power_action(HttpAPI.Power.REBOOT))
app.set_action("on_restart_gui", lambda a, v: self.on_power_action(HttpAPI.Power.RESTART_GUI))
app.set_action("on_shutdown", lambda a, v: self.on_power_action(HttpAPI.Power.DEEP_STANDBY))
# Screenshots
app.set_action("on_screenshot_all", self.on_screenshot_all)
app.set_action("on_screenshot_video", self.on_screenshot_video)
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
# Timers
app.set_action("on_timer_add", self.on_timer_add)
app.set_action("on_timer_add_from_event", self.on_timer_add_from_event)
app.set_action("on_timer_remove", self.on_timer_remove)
app.set_action("on_timer_edit", self.on_timer_edit)
app.set_action("on_timer_save", self.on_timer_save)
app.set_action("on_timer_cancel", self.on_timer_cancel)
app.set_action("on_timer_begins_set", self.on_timer_begins_set)
app.set_action("on_timer_ends_set", self.on_timer_ends_set)
@property
def update_epg(self):
return self._update_epg
def on_visible_tool(self, stack, param):
tool = self.Tool(stack.get_visible_child_name())
self._update_epg = tool is self.Tool.EPG
if tool is self.Tool.TIMERS:
self.update_timer_list()
if tool is not self.Tool.TIMER:
self._last_tool = tool
def on_hide(self, item):
self._update_epg = False
# ***************** Remote controller ********************* #
def on_remote(self, action, state=False):
""" Shows/Hides [R key] remote controller. """
action.set_state(state)
self._remote_revealer.set_visible(state)
self._remote_revealer.set_reveal_child(state)
if state:
self._http_api.send(HttpAPI.Request.VOL, "state", self.update_volume)
def on_remote_action(self, action):
self._http_api.send(HttpAPI.Request.REMOTE, action, self.on_response)
@run_with_delay(0.5)
def on_volume_changed(self, button, value):
self._http_api.send(HttpAPI.Request.VOL, "{:.0f}".format(value), self.on_response)
def update_volume(self, vol):
if "error_code" in vol:
return
GLib.idle_add(self._volume_button.set_value, int(vol.get("e2current", "0")))
def on_response(self, resp):
if "error_code" in resp:
return
if self._screenshot_check_button.get_active():
ref = "mode=all" if self._http_api.is_owif else "d="
self._http_api.send(HttpAPI.Request.GRUB, ref, self.update_screenshot)
@run_task
def update_screenshot(self, data):
if "error_code" in data:
return
data = data.get("img_data", None)
if data:
from gi.repository import GdkPixbuf
loader = GdkPixbuf.PixbufLoader.new_with_type("jpeg")
loader.set_size(280, 165)
try:
loader.write(data)
pix = loader.get_pixbuf()
except GLib.Error:
pass # NOP
else:
GLib.idle_add(self._screenshot_image.set_from_pixbuf, pix)
finally:
loader.close()
def on_screenshot_all(self, action, value=None):
self._http_api.send(HttpAPI.Request.GRUB, "mode=all" if self._http_api.is_owif else "d=",
self.on_screenshot)
def on_screenshot_video(self, action, value=None):
self._http_api.send(HttpAPI.Request.GRUB, "mode=video" if self._http_api.is_owif else "v=",
self.on_screenshot)
def on_screenshot_osd(self, action, value=None):
self._http_api.send(HttpAPI.Request.GRUB, "mode=osd" if self._http_api.is_owif else "o=",
self.on_screenshot)
@run_task
def on_screenshot(self, data):
if "error_code" in data:
return
img = data.get("img_data", None)
if img:
is_darwin = self._settings.is_darwin
GLib.idle_add(self._screenshot_button_box.set_sensitive, is_darwin)
path = os.path.expanduser("~/Desktop") if is_darwin else None
try:
import tempfile
import subprocess
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=not is_darwin) as tf:
tf.write(img)
cmd = ["open" if is_darwin else "xdg-open", tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._screenshot_button_box.set_sensitive, True)
def on_power_action(self, action):
self._http_api.send(HttpAPI.Request.POWER, action, lambda resp: log("Power status changed..."))
def update_signal(self, sig):
self._snr_value_label.set_text(sig.get("e2snrdb", "0 dB").strip())
self._ber_value_label.set_text(str(sig.get("e2ber", None) or "0").strip())
self._agc_value_label.set_text(sig.get("e2acg", "0 %").strip())
# ************************ EPG **************************** #
def on_service_changed(self, ref):
self._app._wait_dialog.show()
self._http_api.send(HttpAPI.Request.EPG, ref, self.update_epg_data)
@run_idle
def update_epg_data(self, epg):
list(map(self._epg_list_box.remove, (r for r in self._epg_list_box)))
list(map(lambda e: self._epg_list_box.add(self.EpgRow(e)), epg.get("event_list", [])))
self._app._wait_dialog.hide()
def on_epg_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
row = list_box.get_selected_row()
if row:
self.set_timer_from_event_data(row.event_data)
def on_epg_filter_changed(self, entry):
self._epg_list_box.invalidate_filter()
def epg_filter_function(self, row: EpgRow):
txt = self._epg_filter_entry.get_text().upper()
return any((not txt, txt in row.time_header.upper(), txt in row.title.upper(), txt in row.desc.upper()))
def on_timer_add_from_event(self, action, value=None):
rows = self._epg_list_box.get_selected_rows()
if not rows:
self._app.show_error_dialog("No selected item!")
return
refs = []
for row in rows:
event = row.event_data
ref = "timeraddbyeventid?sRef={}&eventid={}&justplay=0".format(event.get("e2eventservicereference", ""),
event.get("e2eventid", ""))
refs.append(ref)
gen = self.write_timers_list(refs)
GLib.idle_add(lambda: next(gen, False))
def write_timers_list(self, refs):
self._app._wait_dialog.show()
tasks = list(refs)
for ref in refs:
self._http_api.send(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
yield True
while tasks:
yield True
self._stack.set_visible_child_name(self.Tool.TIMERS.value)
# *********************** Timers *************************** #
def on_timers_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
self.on_timer_edit()
def update_timer_list(self):
self._app._wait_dialog.show()
self._http_api.send(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
@run_idle
def update_timers_data(self, timers):
list(map(self._timers_list_box.remove, (r for r in self._timers_list_box)))
list(map(lambda t: self._timers_list_box.add(self.TimerRow(t)), timers.get("timer_list", [])))
self._timer_remove_button.set_visible(len(self._timers_list_box))
self._app._wait_dialog.hide()
def on_timer_add(self, action=None, value=None):
self._timer_action = self.TimerAction.ADD
date = datetime.now()
self.set_begins_date(date)
self.set_ends_date(date)
self._timer_event_id_entry.set_text("")
self._timer_location_switch.set_active(False)
self.set_repetition_flags(0)
self._stack.set_visible_child_name(self.Tool.TIMER.value)
def on_timer_remove(self, action, value=None):
rows = self._timers_list_box.get_selected_rows()
if not rows or show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
refs = {}
for row in rows:
timer = row.timer
ref = "timerdelete?sRef={}&begin={}&end={}".format(timer.get("e2servicereference", ""),
timer.get("e2timebegin", ""),
timer.get("e2timeend", ""))
refs[ref] = row
self._app._wait_dialog.show("Deleting data...")
gen = self.remove_timers(refs)
GLib.idle_add(lambda: next(gen, False))
def remove_timers(self, refs):
tasks = list(refs)
removed = set()
for ref in refs:
yield from self.remove_timer(ref, removed, tasks)
while tasks:
yield True
list(map(self._timers_list_box.remove, (refs[ref] for ref in refs if ref in removed)))
self._app._wait_dialog.hide()
self._timer_remove_button.set_visible(len(self._timers_list_box))
yield True
def remove_timer(self, ref, removed, tasks=None):
def callback(resp):
if resp.get("e2state", "") == "True":
log(resp.get("e2statetext", ""))
removed.add(ref)
else:
log(resp.get("e2statetext", None) or "Timer deletion error.")
if tasks:
tasks.pop()
self._http_api.send(HttpAPI.Request.TIMER, ref, callback)
yield True
def on_timer_edit(self, action=None, value=None):
row = self._timers_list_box.get_selected_row()
if row:
self._timer_action = self.TimerAction.CHANGE
timer = row.timer
self._current_timer = timer
self._timer_name_entry.set_text(timer.get("e2name", ""))
self._timer_desc_entry.set_text(timer.get("e2description", "") or "")
self._timer_service_entry.set_text(timer.get("e2servicename", "") or "")
self._timer_service_ref_entry.set_text(timer.get("e2servicereference", ""))
self._timer_event_id_entry.set_text(timer.get("e2eit", ""))
self._timer_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
self._timer_action_combo_box.set_active_id(timer.get("e2justplay", "0"))
self._timer_after_combo_box.set_active_id(timer.get("e2afterevent", "0"))
self.set_time_data(int(timer.get("e2timebegin", "0")), int(timer.get("e2timeend", "0")))
location = timer.get("e2location", "")
self._timer_location_entry.set_text("" if location == "None" else location)
# Days
self.set_repetition_flags(int(timer.get("e2repeated", "0")))
self._stack.set_visible_child_name(self.Tool.TIMER.value)
def on_timer_save(self, action, value=None):
args = []
t_data = self.get_timer_data()
s_ref = t_data.get("sRef", "")
if self._timer_action is self.TimerAction.EVENT:
args.append("timeraddbyeventid?sRef={}".format(s_ref))
args.append("eventid={}".format(t_data.get("eit", "0")))
args.append("justplay={}".format(t_data.get("justplay", "")))
args.append("tags={}".format(""))
else:
if self._timer_action is self.TimerAction.ADD:
args.append("timeradd?sRef={}".format(s_ref))
args.append("deleteOldOnSave={}".format(0))
elif self._timer_action is self.TimerAction.CHANGE:
args.append("timerchange?sRef={}".format(s_ref))
args.append("channelOld={}".format(s_ref))
args.append("beginOld={}".format(self._current_timer.get("e2timebegin", "0")))
args.append("endOld={}".format(self._current_timer.get("e2timeend", "0")))
args.append("deleteOldOnSave={}".format(1))
args.append("begin={}".format(t_data.get("begin", "")))
args.append("end={}".format(t_data.get("end", "")))
args.append("name={}".format(quote(t_data.get("name", ""))))
args.append("description={}".format(quote(t_data.get("description", ""))))
args.append("tags={}".format(""))
args.append("eit={}".format("0"))
args.append("disabled={}".format(t_data.get("disabled", "1")))
args.append("justplay={}".format(t_data.get("justplay", "1")))
args.append("afterevent={}".format(t_data.get("afterevent", "0")))
args.append("repeated={}".format(self.get_repetition_flags()))
if self._timer_location_switch.get_active():
args.append("dirname={}".format(self._timer_location_entry.get_text()))
self._http_api.send(HttpAPI.Request.TIMER, "&".join(args), self.timer_add_edit_callback)
@run_idle
def timer_add_edit_callback(self, resp):
if "error_code" in resp:
msg = "Error getting timer status.\n{}".format(resp.get("error_code"))
self._app.show_error_dialog(msg)
log(msg)
return
state = resp.get("e2state", None)
if state == "False":
msg = resp.get("e2statetext", "")
self._app.show_error_dialog(msg)
log(msg)
if state == "True":
log(resp.get("e2statetext", ""))
self._stack.set_visible_child_name(self._last_tool.value)
else:
log("Error getting timer status. No response!")
def on_timer_cancel(self, action, value=None):
self._stack.set_visible_child_name(self._last_tool.value)
def on_timer_begins_set(self, action, value=None):
self.set_begins_date(self.get_begins_date())
def on_timer_ends_set(self, action, value=None):
self.set_ends_date(self.get_ends_date())
def get_begins_date(self):
date = self._timer_begins_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_begins_hr_button.get_value()),
minute=int(self._timer_begins_min_button.get_value()))
def set_begins_date(self, date):
hour = date.hour
minute = date.minute
self._timer_begins_hr_button.set_value(hour)
self._timer_begins_min_button.set_value(minute)
self._timer_begins_calendar.select_day(date.day)
self._timer_begins_calendar.select_month(date.month - 1, date.year)
self._timer_begins_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
def get_ends_date(self):
date = self._timer_ends_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_ends_hr_button.get_value()),
minute=int(self._timer_ends_min_button.get_value()))
def set_ends_date(self, date):
hour = date.hour
minute = date.minute
self._timer_ends_hr_button.set_value(hour)
self._timer_ends_min_button.set_value(minute)
self._timer_ends_calendar.select_day(date.day)
self._timer_ends_calendar.select_month(date.month - 1, date.year)
self._timer_ends_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
def set_timer_from_event_data(self, timer):
self._stack.set_visible_child_name(self.Tool.TIMER.value)
self._timer_action = self.TimerAction.EVENT
self._timer_name_entry.set_text(timer.get("e2eventtitle", ""))
self._timer_desc_entry.set_text(timer.get("e2eventdescription", ""))
self._timer_service_entry.set_text(timer.get("e2eventservicename", ""))
self._timer_service_ref_entry.set_text(timer.get("e2eventservicereference", ""))
self._timer_event_id_entry.set_text(timer.get("e2eventid", ""))
self._timer_action_combo_box.set_active_id("1")
self._timer_after_combo_box.set_active_id("3")
start_time = int(timer.get("e2eventstart", "0"))
self.set_time_data(start_time, start_time + int(timer.get("e2eventduration", "0")))
def set_time_data(self, start_time, end_time):
""" Sets values for time widgets. """
ev_time_start = datetime.fromtimestamp(start_time) or datetime.now()
ev_time_end = datetime.fromtimestamp(end_time) or datetime.now()
self._timer_begins_entry.set_text(ev_time_start.strftime(self._TIME_STR))
self._timer_ends_entry.set_text(ev_time_end.strftime(self._TIME_STR))
self._timer_begins_calendar.select_day(ev_time_start.day)
self._timer_begins_calendar.select_month(ev_time_start.month - 1, ev_time_start.year)
self._timer_ends_calendar.select_day(ev_time_end.day)
self._timer_ends_calendar.select_month(ev_time_end.month - 1, ev_time_end.year)
self._timer_begins_hr_button.set_value(ev_time_start.hour)
self._timer_begins_min_button.set_value(ev_time_start.minute)
self._timer_ends_hr_button.set_value(ev_time_end.hour)
self._timer_ends_min_button.set_value(ev_time_end.minute)
def get_timer_data(self):
""" Returns timer data as a dict. """
return {"sRef": self._timer_service_ref_entry.get_text(),
"begin": int(datetime.strptime(self._timer_begins_entry.get_text(), self._TIME_STR).timestamp()),
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), self._TIME_STR).timestamp()),
"name": self._timer_name_entry.get_text(),
"description": self._timer_desc_entry.get_text(),
"dirname": "",
"eit": self._timer_event_id_entry.get_text(),
"disabled": int(not self._timer_enabled_switch.get_active()),
"justplay": self._timer_action_combo_box.get_active_id(),
"afterevent": self._timer_after_combo_box.get_active_id(),
"repeated": self.get_repetition_flags()}
def get_repetition_flags(self):
""" Returns flags for repetition. """
day_flags = 0
for i, box in enumerate((self._timer_mo_check_button,
self._timer_tu_check_button,
self._timer_we_check_button,
self._timer_th_check_button,
self._timer_fr_check_button,
self._timer_sa_check_button,
self._timer_su_check_button)):
if box.get_active():
day_flags = day_flags | (1 << i)
return day_flags
def set_repetition_flags(self, flags):
for i, box in enumerate((self._timer_mo_check_button,
self._timer_tu_check_button,
self._timer_we_check_button,
self._timer_th_check_button,
self._timer_fr_check_button,
self._timer_sa_check_button,
self._timer_su_check_button)):
box.set_active(flags & 1 == 1)
flags = flags >> 1
# ***************** Drag-and-drop ********************* #
def on_timers_drag_data_received(self, box, context, x, y, data, info, time):
txt = data.get_text()
if txt:
itr_str, sep, source = txt.partition(self._app.DRAG_SEP)
if not source:
return
itrs = itr_str.split(",")
if len(itrs) > 1:
self._app.show_error_dialog("Please, select only one item!")
return
fav_id = None
if source == self._app.FAV_MODEL_NAME:
model = self._app.fav_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.FAV_ID)
elif source == self._app.SERVICE_MODEL_NAME:
model = self._app.services_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.SRV_FAV_ID)
service = self._app.current_services.get(fav_id, None)
if service:
self._timer_name_entry.set_text(service.service)
self._timer_service_entry.set_text(service.service)
self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":"))
self.on_timer_add()
context.finish(True, False, time)

View File

@@ -40,10 +40,10 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property> <property name="icon_name">system-help</property>
<property name="type_hint">normal</property> <property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property> <property name="program_name">DemonEditor</property>
<property name="version">1.0.0 Alpha</property> <property name="version">1.0.3 Beta</property>
<property name="copyright">2018-2020 Dmitriy Yefremov <property name="copyright">2018-2020 Dmitriy Yefremov
</property> </property>
<property name="comments" translatable="yes">Enigma2 channel and satellites list editor for MacOS. <property name="comments" translatable="yes">Enigma2 channel and satellite list editor for MacOS.
(Experimental)</property> (Experimental)</property>
<property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-mac</property> <property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-mac</property>
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий. <property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
@@ -51,7 +51,7 @@ Author: Dmitriy Yefremov
<property name="authors">Dmitriy Yefremov <property name="authors">Dmitriy Yefremov
</property> </property>
<property name="translator_credits" translatable="yes">translator-credits</property> <property name="translator_credits" translatable="yes">translator-credits</property>
<property name="artists">Program logo: &lt;a href="http://ihad.tv"&gt; mfgeg&lt;/a&gt;</property> <property name="artists">Program logo: &lt;a href="http://ihad.tv"&gt;mfgeg&lt;/a&gt;</property>
<property name="logo_icon_name">demon-editor</property> <property name="logo_icon_name">demon-editor</property>
<property name="wrap_license">True</property> <property name="wrap_license">True</property>
<property name="license_type">mit-x11</property> <property name="license_type">mit-x11</property>
@@ -89,7 +89,7 @@ Author: Dmitriy Yefremov
<property name="window_position">center</property> <property name="window_position">center</property>
<property name="default_width">320</property> <property name="default_width">320</property>
<property name="destroy_with_parent">True</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_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>

View File

@@ -2,6 +2,7 @@
import gettext import gettext
from enum import Enum from enum import Enum
from functools import lru_cache from functools import lru_cache
from pathlib import Path
from app.commons import run_idle from app.commons import run_idle
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
@@ -72,12 +73,13 @@ class WaitDialog:
self._dialog.destroy() self._dialog.destroy()
def show_dialog(dialog_type: DialogType, transient, text=None, settings=None, action_type=None, file_filter=None): def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
""" Shows dialogs by name """ title=None, create_dir=False):
""" Shows dialogs by name. """
if dialog_type in (DialogType.INFO, DialogType.ERROR): if dialog_type in (DialogType.INFO, DialogType.ERROR):
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text) return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
elif dialog_type is DialogType.CHOOSER and settings: elif dialog_type is DialogType.CHOOSER and settings:
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter) return get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons, title, create_dir)
elif dialog_type is DialogType.INPUT: elif dialog_type is DialogType.INPUT:
return get_input_dialog(transient, text) return get_input_dialog(transient, text)
elif dialog_type is DialogType.QUESTION: elif dialog_type is DialogType.QUESTION:
@@ -87,7 +89,7 @@ def show_dialog(dialog_type: DialogType, transient, text=None, settings=None, ac
return get_about_dialog(transient) return get_about_dialog(transient)
def get_chooser_dialog(transient, settings, name, patterns): def get_chooser_dialog(transient, settings, name, patterns, title=None):
file_filter = Gtk.FileFilter() file_filter = Gtk.FileFilter()
file_filter.set_name(name) file_filter.set_name(name)
for p in patterns: for p in patterns:
@@ -97,30 +99,28 @@ def get_chooser_dialog(transient, settings, name, patterns):
transient=transient, transient=transient,
settings=settings, settings=settings,
action_type=Gtk.FileChooserAction.OPEN, action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter) file_filter=file_filter,
title=title)
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter): def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
dialog = Gtk.FileChooserNative() action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
dialog.set_title(get_message(text) if text else "") dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
dialog.set_transient_for(transient) dialog.set_create_folders(dirs)
dialog.set_action(action_type if action_type is not None else Gtk.FileChooserAction.SELECT_FOLDER)
dialog.set_create_folders(False)
dialog.set_modal(True) dialog.set_modal(True)
if file_filter is not None: if file_filter is not None:
dialog.add_filter(file_filter) dialog.add_filter(file_filter)
path = settings.data_local_path dialog.set_current_folder(settings.data_local_path)
dialog.set_current_folder(path)
response = dialog.run() response = dialog.run()
if response == Gtk.ResponseType.ACCEPT: if response == Gtk.ResponseType.ACCEPT:
if dialog.get_filename(): path = Path(dialog.get_filename() or dialog.get_current_folder())
path = dialog.get_filename() if path.is_dir():
if action_type is not Gtk.FileChooserAction.OPEN: response = "{}/".format(path.resolve())
path = path + "/" elif path.is_file():
response = path response = str(path.resolve())
dialog.destroy() dialog.destroy()
return response return response

657
app/ui/ftp.glade Normal file
View File

@@ -0,0 +1,657 @@
<?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 for GNU/Linux. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="bookmarks_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name url -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkImage" id="file_create_folder_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-new</property>
</object>
<object class="GtkListStore" id="file_list_store">
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name size -->
<column type="gchararray"/>
<!-- column-name date -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name extra -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkImage" id="ftp_create_folder_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-new</property>
</object>
<object class="GtkListStore" id="ftp_list_store">
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name size -->
<column type="gchararray"/>
<!-- column-name date -->
<column type="gchararray"/>
<!-- column-name attr -->
<column type="gchararray"/>
<!-- column-name extra -->
<column type="gchararray"/>
</columns>
</object>
<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_bottom">1</property>
<property name="label_xalign">0</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>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="wide_handle">True</property>
<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">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="ftp_info_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<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>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ftp_info_label">
<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="yalign">1</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="GtkBox" id="ftp_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="bookmark_button">
<property name="can_focus">False</property>
<property name="model">bookmarks_list_store</property>
<property name="id_column">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="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>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="pc_info_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">32</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="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>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove</property>
</object>
<object class="GtkImage" id="remove_image_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove</property>
</object>
<object class="GtkImage" id="rename_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">gtk-edit</property>
</object>
<object class="GtkMenu" id="ftp_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="ftp_create_folder_menu_item">
<property name="label" translatable="yes">Create folder</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">ftp_create_folder_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
<accelerator key="F7" signal="activate"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_edit_menu_item">
<property name="label" translatable="yes">Edit</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">rename_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_edit" object="ftp_name_column_renderer" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
<accelerator key="F2" signal="activate"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_remove_menu_item">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
<object class="GtkImage" id="rename_image_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">gtk-edit</property>
</object>
<object class="GtkMenu" id="file_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="file_create_folder_menu_item">
<property name="label" translatable="yes">Create folder</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">file_create_folder_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
<accelerator key="F7" signal="activate"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="file_edit_menu_item">
<property name="label" translatable="yes">Edit</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">rename_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_edit" object="file_name_column_renderer" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
<accelerator key="F2" signal="activate"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="file_remove_menu_item">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
</interface>

574
app/ui/ftp.py Normal file
View File

@@ -0,0 +1,574 @@
""" Simple FTP client module. """
import subprocess
from collections import namedtuple
from datetime import datetime
from enum import IntEnum
from ftplib import all_errors
from pathlib import Path
from shutil import rmtree
from urllib.parse import urlparse, unquote
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.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
class FtpClientBox(Gtk.HBox):
""" Simple FTP client base class. """
ROOT = ".."
FOLDER = "<Folder>"
LINK = "<Link>"
MAX_SIZE = 10485760 # 10 MB file limit
URI_SEP = "::::"
class Column(IntEnum):
ICON = 0
NAME = 1
SIZE = 2
DATE = 3
ATTR = 4
EXTRA = 5
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_spacing(2)
self.set_orientation(Gtk.Orientation.VERTICAL)
self._app = app
self._settings = settings
self._ftp = None
self._select_enabled = True
handlers = {"on_connect": self.on_connect,
"on_disconnect": self.on_disconnect,
"on_ftp_row_activated": self.on_ftp_row_activated,
"on_file_row_activated": self.on_file_row_activated,
"on_ftp_edit": self.on_ftp_edit,
"on_ftp_edited": self.on_ftp_edited,
"on_file_edit": self.on_file_edit,
"on_file_edited": self.on_file_edited,
"on_file_remove": self.on_file_remove,
"on_ftp_remove": self.on_ftp_file_remove,
"on_file_create_folder": self.on_file_create_folder,
"on_ftp_create_folder": self.on_ftp_create_folder,
"on_view_drag_begin": self.on_view_drag_begin,
"on_ftp_drag_data_get": self.on_ftp_drag_data_get,
"on_ftp_drag_data_received": self.on_ftp_drag_data_received,
"on_file_drag_data_get": self.on_file_drag_data_get,
"on_file_drag_data_received": self.on_file_drag_data_received,
"on_view_drag_end": self.on_view_drag_end,
"on_view_popup_menu": on_popup_menu,
"on_view_key_press": self.on_view_key_press,
"on_view_press": self.on_view_press,
"on_view_release": self.on_view_release}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "ftp.glade")
builder.connect_signals(handlers)
self.add(builder.get_object("main_frame"))
self._ftp_info_label = builder.get_object("ftp_info_label")
self._ftp_view = builder.get_object("ftp_view")
self._ftp_model = builder.get_object("ftp_list_store")
self._ftp_name_renderer = builder.get_object("ftp_name_column_renderer")
self._file_view = builder.get_object("file_view")
self._file_model = builder.get_object("file_list_store")
self._file_name_renderer = builder.get_object("file_name_column_renderer")
# Buttons
self._connect_button = builder.get_object("connect_button")
disconnect_button = builder.get_object("disconnect_button")
disconnect_button.bind_property("visible", builder.get_object("ftp_create_folder_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_edit_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
# Force Ctrl
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
self._file_view.connect("key-press-event", self._app.force_ctrl)
# Icons
theme = Gtk.IconTheme.get_default()
folder_icon = "folder-symbolic" if settings.is_darwin else "folder"
self._folder_icon = theme.load_icon(folder_icon, 16, 0) if theme.lookup_icon(folder_icon, 16, 0) else None
self._link_icon = theme.load_icon("emblem-symbolic-link", 16, 0) if theme.lookup_icon("emblem-symbolic-link",
16, 0) else None
# Initialization
self.init_drag_and_drop()
self.init_ftp()
self.init_file_data()
self.show()
@run_task
def init_ftp(self):
GLib.idle_add(self._ftp_model.clear)
try:
if self._ftp:
self._ftp.close()
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
self._ftp.encoding = "utf-8"
self.update_ftp_info(self._ftp.getwelcome())
except all_errors as e:
self.update_ftp_info(str(e))
self.on_disconnect()
else:
self.init_ftp_data()
@run_task
def init_ftp_data(self, path=None):
if not self._ftp:
return
if path:
try:
self._ftp.cwd(path)
except all_errors as e:
self.update_ftp_info(str(e))
files = []
try:
self._ftp.dir(files.append)
except all_errors as e:
log(e)
self.update_ftp_info(str(e))
self.on_disconnect()
else:
self.append_ftp_data(files)
GLib.idle_add(self._connect_button.set_visible, False)
@run_task
def init_file_data(self, path=None):
self.append_file_data(Path(path if path else self._settings.data_local_path))
@run_idle
def append_file_data(self, path: Path):
self._file_model.clear()
self._file_model.append(File(None, self.ROOT, None, None, str(path), "0"))
try:
dirs = [p for p in path.iterdir()]
except OSError as e:
log(e)
else:
for p in dirs:
is_dir = p.is_dir()
st = p.stat()
size = str(st.st_size)
date = datetime.fromtimestamp(st.st_mtime).strftime("%d-%m-%y %H:%M")
icon = None
if is_dir:
r_size = self.FOLDER
icon = self._folder_icon
elif p.is_symlink():
r_size = self.LINK
icon = self._link_icon
else:
r_size = self.get_size_from_bytes(size)
self._file_model.append(File(icon, p.name, r_size, date, str(p.resolve()), size))
@run_idle
def append_ftp_data(self, files):
self._ftp_model.clear()
self._ftp_model.append(File(None, self.ROOT, None, None, self._ftp.pwd(), "0"))
for f in files:
f_data = f.split()
f_type = f_data[0][0]
is_dir = f_type == "d"
is_link = f_type == "l"
size = f_data[4]
icon = None
if is_dir:
r_size = self.FOLDER
icon = self._folder_icon
elif is_link:
r_size = self.LINK
icon = self._link_icon
else:
r_size = self.get_size_from_bytes(size)
date = "{}, {} {}".format(f_data[5], f_data[6], f_data[7])
self._ftp_model.append(File(icon, " ".join(f_data[8:]), r_size, date, f_data[0], size))
def on_connect(self, item=None):
self.init_ftp()
def on_disconnect(self, item=None):
if self._ftp:
self._ftp.close()
self._connect_button.set_visible(True)
GLib.idle_add(self._ftp_model.clear)
def on_ftp_row_activated(self, view, path, column):
row = self._ftp_model[path][:]
f_path = row[self.Column.NAME]
size = row[self.Column.SIZE]
if size == self.FOLDER or f_path == self.ROOT:
self.init_ftp_data(f_path)
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!")
else:
self.open_ftp_file(f_path)
def on_file_row_activated(self, view, path, column):
row = self._file_model[path][:]
path = Path(row[self.Column.ATTR])
if row[self.Column.SIZE] == self.FOLDER:
self.init_file_data(path)
elif row[self.Column.NAME] == self.ROOT:
self.init_file_data(path.parent)
else:
self.open_file(row[self.Column.ATTR])
@run_task
def open_file(self, path):
GLib.idle_add(self._file_view.set_sensitive, False)
try:
cmd = ["open" if self._settings.is_darwin else "xdg-open", path]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._file_view.set_sensitive, True)
@run_task
def open_ftp_file(self, f_path):
is_darwin = self._settings.is_darwin
GLib.idle_add(self._ftp_view.set_sensitive, False)
try:
import tempfile
import os
path = os.path.expanduser("~/Desktop") if is_darwin else None
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=not is_darwin) as tf:
msg = "Downloading file: {}. Status: {}"
try:
status = self._ftp.retrbinary("RETR " + f_path, tf.write)
self.update_ftp_info(msg.format(f_path, status))
except all_errors as e:
self.update_ftp_info(msg.format(f_path, e))
tf.flush()
cmd = ["open" if is_darwin else "xdg-open", tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._ftp_view.set_sensitive, True)
def on_ftp_edit(self, renderer):
model, paths = self._ftp_view.get_selection().get_selected_rows()
if not paths:
return
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
return
renderer.set_property("editable", True)
self._ftp_view.set_cursor(paths, self._ftp_view.get_column(0), True)
def on_ftp_edited(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._ftp_model[path]
old_name = row[self.Column.NAME]
if old_name == new_value:
return
resp = self._ftp.rename_file(old_name, new_value)
self.update_ftp_info("{} Status: {}".format(old_name, resp))
if resp[0] == "2":
row[self.Column.NAME] = new_value
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!")
return
renderer.set_property("editable", True)
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
def on_file_edited(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._file_model[path]
old_name = row[self.Column.NAME]
if old_name == new_value:
return
path = Path(row[self.Column.ATTR])
if path.exists():
try:
new_path = path.rename("{}/{}".format(path.parent, new_value))
except ValueError as e:
log(e)
self._app.show_error_dialog(str(e))
else:
if new_path.name == new_value:
row[self.Column.NAME] = new_value
row[self.Column.ATTR] = str(new_path.resolve())
def on_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
model, paths = self._file_view.get_selection().get_selected_rows()
to_delete = []
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
f_path = Path(model[path][self.Column.ATTR])
try:
rmtree(f_path, ignore_errors=True) if f_path.is_dir() else f_path.unlink()
except OSError as e:
log(e)
else:
to_delete.append(model.get_iter(path))
list(map(model.remove, to_delete))
def on_ftp_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
model, paths = self._ftp_view.get_selection().get_selected_rows()
to_delete = []
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
row = model[path][:]
name = row[self.Column.NAME]
if row[self.Column.SIZE] == self.FOLDER:
resp = self._ftp.delete_dir(name, self.update_ftp_info)
else:
resp = self._ftp.delete_file(name, self.update_ftp_info)
if resp[0] == "2":
to_delete.append(model.get_iter(path))
list(map(model.remove, to_delete))
def on_file_create_folder(self, renderer):
itr = self._file_model.get_iter_first()
if not itr:
return
name = self.get_new_folder_name(self._file_model)
cur_path = self._file_model.get_value(itr, self.Column.ATTR)
path = Path("{}/{}".format(cur_path, name))
try:
path.mkdir()
except OSError as e:
log(e)
self._app.show_error_dialog(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)
self._file_view.set_cursor(self._file_model.get_path(itr), self._file_view.get_column(0), True)
def on_ftp_create_folder(self, renderer):
itr = self._ftp_model.get_iter_first()
if not itr:
return
cur_path = self._ftp_model.get_value(itr, self.Column.ATTR)
name = self.get_new_folder_name(self._ftp_model)
try:
folder = "{}/{}".format(cur_path, name)
resp = self._ftp.mkd(folder)
except all_errors as e:
self.update_ftp_info(str(e))
log(e)
else:
if resp == "{}/{}".format(cur_path, name):
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
renderer.set_property("editable", True)
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)
def get_new_folder_name(self, model):
""" Returns the default name for the newly created folder. """
name = "new folder"
names = {r[self.Column.NAME] for r in model}
count = 0
while name in names:
count += 1
name = "{}{}".format(name, count)
return name
# ***************** Drag-and-drop ********************* #
def init_drag_and_drop(self):
self._ftp_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._ftp_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._ftp_view.drag_source_add_uri_targets()
self._ftp_view.drag_dest_add_uri_targets()
self._file_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._file_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._file_view.drag_source_add_uri_targets()
self._file_view.drag_dest_add_uri_targets()
self._ftp_view.get_selection().set_select_function(lambda *args: self._select_enabled)
self._file_view.get_selection().set_select_function(lambda *args: self._select_enabled)
def on_view_drag_begin(self, view, context):
model, paths = view.get_selection().get_selected_rows()
if len(paths) < 1:
return
pix = self._app.get_drag_icon_pixbuf(model, paths, self.Column.NAME, self.Column.SIZE)
Gtk.drag_set_icon_pixbuf(context, pix, 0, 0)
return True
def on_ftp_drag_data_get(self, view, context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = []
for r in [model[p][:] for p in paths]:
if r[self.Column.SIZE] != self.LINK and r[self.Column.NAME] != self.ROOT:
uris.append(Path("/{}:{}".format(r[self.Column.NAME], r[self.Column.ATTR])).as_uri())
data.set_uris([sep.join(uris)])
@run_task
def on_ftp_drag_data_received(self, view, context, x, y, data: Gtk.SelectionData, info, time):
if not self._ftp:
return
resp = "2"
try:
GLib.idle_add(self._app._wait_dialog.show)
uris = data.get_uris()
if self._settings.is_darwin and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
for uri in uris:
uri = urlparse(unquote(uri)).path
path = Path(uri)
if path.is_dir():
try:
self._ftp.mkd(path.name)
except all_errors as e:
pass # NOP
self._ftp.cwd(path.name)
resp = self._ftp.upload_dir(str(path.resolve()) + "/", self.update_ftp_info)
else:
resp = self._ftp.send_file(path.name, str(path.parent) + "/", callback=self.update_ftp_info)
finally:
GLib.idle_add(self._app._wait_dialog.hide)
if resp and resp[0] == "2":
itr = self._ftp_model.get_iter_first()
if itr:
self.init_ftp_data(self._ftp_model.get_value(itr, self.Column.ATTR))
Gtk.drag_finish(context, True, False, time)
return True
def on_file_drag_data_get(self, view, context, data: Gtk.SelectionData, info, time):
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
data.set_uris(uris)
@run_task
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + "/"
try:
GLib.idle_add(self._app._wait_dialog.show)
uris = data.get_uris()
if self._settings.is_darwin and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
for uri in uris:
name, sep, attr = unquote(Path(uri).name).partition(":")
if not attr:
return True
if attr[0] == "d":
self._ftp.download_dir(name, cur_path, self.update_ftp_info)
else:
self._ftp.download_file(name, cur_path, self.update_ftp_info)
except OSError as e:
log(e)
finally:
GLib.idle_add(self._app._wait_dialog.hide)
self.init_file_data(cur_path)
Gtk.drag_finish(context, True, False, time)
return True
def on_view_drag_end(self, view, context):
self._select_enabled = True
view.get_selection().unselect_all()
@run_idle
def update_ftp_info(self, message):
message = message.strip()
self._ftp_info_label.set_text(message)
self._ftp_info_label.set_tooltip_text(message)
def on_view_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.F7:
if self._ftp_view.is_focus():
self.on_ftp_create_folder(self._ftp_name_renderer)
elif self._file_view.is_focus():
self.on_file_create_folder(self._file_name_renderer)
elif key is KeyboardKey.F2 or ctrl and KeyboardKey.R:
if self._ftp_view.is_focus():
self.on_ftp_edit(self._ftp_name_renderer)
elif self._file_view.is_focus():
self.on_file_edit(self._file_name_renderer)
elif key is KeyboardKey.DELETE:
if self._ftp_view.is_focus():
self.on_ftp_file_remove()
elif self._file_view.is_focus():
self.on_file_remove()
def on_view_press(self, view, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
target = view.get_path_at_pos(event.x, event.y)
mask = not (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK))
if target and mask and view.get_selection().path_is_selected(target[0]):
self._select_enabled = False
def on_view_release(self, view, event):
""" Handles a mouse click (release) to view. """
# Enable selection.
self._select_enabled = True
def get_size_from_bytes(self, size):
""" Simple convert function from bytes to other units like K, M or G. """
try:
b = float(size)
except ValueError:
return size
else:
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
if b < kb:
return str(b)
elif kb <= b < mb:
return "{0:.1f} K".format(b / kb)
elif mb <= b < gb:
return "{0:.1f} M".format(b / mb)
elif gb <= b:
return "{0:.1f} G".format(b / gb)
if __name__ == '__main__':
pass

View File

@@ -1,7 +1,7 @@
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
from app.commons import run_idle from app.commons import run_idle, log
from app.eparser import get_bouquets, get_services from app.eparser import get_bouquets, get_services
from app.eparser.ecommons import BqType, BqServiceType, Bouquet from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.enigma.bouquets import get_bouquet from app.eparser.enigma.bouquets import get_bouquet
@@ -123,6 +123,7 @@ class ImportDialog:
self._services_model.clear() self._services_model.clear()
try: try:
if not self._bouquets: if not self._bouquets:
log("Import [init data]: getting bouquets...")
self._bouquets = get_bouquets(path, self._profile) self._bouquets = get_bouquets(path, self._profile)
for bqs in self._bouquets: for bqs in self._bouquets:
for bq in bqs.bouquets: for bq in bqs.bouquets:
@@ -133,6 +134,7 @@ class ImportDialog:
for srv in services: for srv in services:
self._services[srv.fav_id] = srv self._services[srv.fav_id] = srv
except FileNotFoundError as e: except FileNotFoundError as e:
log("Import error [init data]: {}".format(e))
self.show_info_message(str(e), Gtk.MessageType.ERROR) self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_import(self, item): def on_import(self, item):
@@ -143,9 +145,17 @@ class ImportDialog:
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL: if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return return
self.import_data()
@run_idle
def import_data(self):
""" Importing data into models. """
if not self._bouquets:
return
log("Importing data...")
services = set() services = set()
to_delete = set() to_delete = set()
for row in self._main_model: for row in self._main_model:
bq = (row[0], row[1]) bq = (row[0], row[1])
if row[-1]: if row[-1]:
@@ -155,19 +165,16 @@ class ImportDialog:
services.add(srv) services.add(srv)
else: else:
to_delete.add(bq) to_delete.add(bq)
bqs_to_delete = [] bqs_to_delete = []
for bqs in self._bouquets: for bqs in self._bouquets:
for bq in bqs.bouquets: for bq in bqs.bouquets:
if (bq.name, bq.type) in to_delete: if (bq.name, bq.type) in to_delete:
bqs_to_delete.append(bq) bqs_to_delete.append(bq)
for bqs in self._bouquets: for bqs in self._bouquets:
bq = bqs.bouquets bq = bqs.bouquets
for b in bqs_to_delete: for b in bqs_to_delete:
with suppress(ValueError): with suppress(ValueError):
bq.remove(b) bq.remove(b)
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services))) self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services)))
self._dialog_window.destroy() self._dialog_window.destroy()

View File

@@ -11,7 +11,7 @@ from app.commons import run_idle, run_task
from app.eparser.ecommons import BqServiceType, Service from app.eparser.ecommons import BqServiceType, Service
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
from app.settings import SettingsType from app.settings import SettingsType
from app.tools.yt import PlayListParser, YouTubeException, YouTube from app.tools.yt import YouTubeException, YouTube
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
from .main_helper import get_base_model, get_iptv_url, on_popup_menu from .main_helper import get_base_model, get_iptv_url, on_popup_menu
from .uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey, from .uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey,
@@ -666,7 +666,9 @@ class YtListImportDialog:
def update_refs_list(self): def update_refs_list(self):
if self._yt_list_id: if self._yt_list_id:
try: try:
self._yt_list_title, links = PlayListParser.get_yt_playlist(self._yt_list_id) if not self._yt:
self._yt = YouTube.get_instance(self._settings)
self._yt_list_title, links = self._yt.get_yt_playlist(self._yt_list_id, self._url_entry.get_text())
except Exception as e: except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR) self.show_info_message(str(e), Gtk.MessageType.ERROR)
return return

Binary file not shown.

View File

@@ -4,12 +4,13 @@ from contextlib import suppress
from datetime import datetime from datetime import datetime
from functools import lru_cache from functools import lru_cache
from itertools import chain from itertools import chain
from urllib.parse import urlparse, unquote
from gi.repository import GLib, Gio from gi.repository import GLib, Gio
from app.commons import run_idle, log, run_task, run_with_delay, init_logger from app.commons import run_idle, log, run_task, run_with_delay, init_logger
from app.connections import (HttpAPI, HttpRequestType, download_data, DownloadType, upload_data, test_http, from app.connections import (HttpAPI, download_data, DownloadType, upload_data, test_http, TestException,
TestException, HttpApiException, STC_XML_FILE) HttpApiException, STC_XML_FILE)
from app.eparser import get_blacklist, write_blacklist, parse_m3u from app.eparser import get_blacklist, write_blacklist, parse_m3u
from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service
from app.eparser.ecommons import CAS, Flag, BouquetService from app.eparser.ecommons import CAS, Flag, BouquetService
@@ -30,7 +31,7 @@ from .main_helper import (insert_marker, move_items, rename, ViewTarget, set_fla
remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons, remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons,
get_selection, get_model_data, remove_all_unused_picons, get_picon_pixbuf, get_base_itrs) get_selection, get_model_data, remove_all_unused_picons, get_picon_pixbuf, get_base_itrs)
from .picons_manager import PiconsDialog from .picons_manager import PiconsDialog
from .satellites_dialog import show_satellites_dialog from .satellites_dialog import show_satellites_dialog, ServicesUpdateDialog
from .search import SearchProvider from .search import SearchProvider
from .service_details_dialog import ServiceDetailsDialog, Action from .service_details_dialog import ServiceDetailsDialog, Action
from .settings_dialog import show_settings_dialog from .settings_dialog import show_settings_dialog
@@ -42,6 +43,7 @@ class Application(Gtk.Application):
SERVICE_MODEL_NAME = "services_list_store" SERVICE_MODEL_NAME = "services_list_store"
FAV_MODEL_NAME = "fav_list_store" FAV_MODEL_NAME = "fav_list_store"
BQ_MODEL_NAME = "bouquets_tree_store" BQ_MODEL_NAME = "bouquets_tree_store"
DRAG_SEP = "::::"
DEL_FACTOR = 50 # Batch size to delete in one pass. DEL_FACTOR = 50 # Batch size to delete in one pass.
FAV_FACTOR = DEL_FACTOR * 2 FAV_FACTOR = DEL_FACTOR * 2
@@ -79,7 +81,6 @@ class Application(Gtk.Application):
self.add_main_option("debug", ord("d"), GLib.OptionFlags.NONE, GLib.OptionArg.STRING, "", None) self.add_main_option("debug", ord("d"), GLib.OptionFlags.NONE, GLib.OptionArg.STRING, "", None)
self._handlers = {"on_close_app": self.on_close_app, self._handlers = {"on_close_app": self.on_close_app,
"on_resize": self.on_resize,
"on_about_app": self.on_about_app, "on_about_app": self.on_about_app,
"on_settings": self.on_settings, "on_settings": self.on_settings,
"on_profile_changed": self.on_profile_changed, "on_profile_changed": self.on_profile_changed,
@@ -90,6 +91,7 @@ class Application(Gtk.Application):
"on_tree_view_key_release": self.on_tree_view_key_release, "on_tree_view_key_release": self.on_tree_view_key_release,
"on_bouquets_selection": self.on_bouquets_selection, "on_bouquets_selection": self.on_bouquets_selection,
"on_satellite_editor_show": self.on_satellite_editor_show, "on_satellite_editor_show": self.on_satellite_editor_show,
"on_fav_selection": self.on_fav_selection,
"on_services_selection": self.on_services_selection, "on_services_selection": self.on_services_selection,
"on_fav_cut": self.on_fav_cut, "on_fav_cut": self.on_fav_cut,
"on_bouquets_cut": self.on_bouquets_cut, "on_bouquets_cut": self.on_bouquets_cut,
@@ -109,12 +111,14 @@ class Application(Gtk.Application):
"on_fav_view_query_tooltip": self.on_fav_view_query_tooltip, "on_fav_view_query_tooltip": self.on_fav_view_query_tooltip,
"on_services_view_query_tooltip": self.on_services_view_query_tooltip, "on_services_view_query_tooltip": self.on_services_view_query_tooltip,
"on_view_drag_begin": self.on_view_drag_begin, "on_view_drag_begin": self.on_view_drag_begin,
"on_view_drag_end": self.on_view_drag_end,
"on_view_drag_data_get": self.on_view_drag_data_get, "on_view_drag_data_get": self.on_view_drag_data_get,
"on_services_view_drag_drop": self.on_services_view_drag_drop, "on_services_view_drag_drop": self.on_services_view_drag_drop,
"on_services_view_drag_data_received": self.on_services_view_drag_data_received, "on_services_view_drag_data_received": self.on_services_view_drag_data_received,
"on_view_drag_data_received": self.on_view_drag_data_received, "on_view_drag_data_received": self.on_view_drag_data_received,
"on_bq_view_drag_data_received": self.on_bq_view_drag_data_received, "on_bq_view_drag_data_received": self.on_bq_view_drag_data_received,
"on_view_press": self.on_view_press, "on_view_press": self.on_view_press,
"on_view_release": self.on_view_release,
"on_view_popup_menu": self.on_view_popup_menu, "on_view_popup_menu": self.on_view_popup_menu,
"on_view_focus": self.on_view_focus, "on_view_focus": self.on_view_focus,
"on_model_changed": self.on_model_changed, "on_model_changed": self.on_model_changed,
@@ -152,6 +156,7 @@ class Application(Gtk.Application):
"on_full_screen": self.on_full_screen, "on_full_screen": self.on_full_screen,
"on_drawing_area_realize": self.on_drawing_area_realize, "on_drawing_area_realize": self.on_drawing_area_realize,
"on_player_drawing_area_draw": self.on_player_drawing_area_draw, "on_player_drawing_area_draw": self.on_player_drawing_area_draw,
"on_ftp_realize": self.on_ftp_realize,
"on_record": self.on_record, "on_record": self.on_record,
"on_remove_all_unavailable": self.on_remove_all_unavailable, "on_remove_all_unavailable": self.on_remove_all_unavailable,
"on_new_bouquet": self.on_new_bouquet, "on_new_bouquet": self.on_new_bouquet,
@@ -178,6 +183,7 @@ class Application(Gtk.Application):
self._blacklist = set() self._blacklist = set()
self._current_bq_name = None self._current_bq_name = None
self._bq_selected = "" # Current selected bouquet self._bq_selected = "" # Current selected bouquet
self._select_enabled = True # Multiple selection
# Current satellite positions in the services list # Current satellite positions in the services list
self._sat_positions = [] self._sat_positions = []
self._marker_types = {BqServiceType.MARKER.name, BqServiceType.SPACE.name} self._marker_types = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
@@ -191,6 +197,8 @@ class Application(Gtk.Application):
self._http_api = None self._http_api = None
self._fav_click_mode = None self._fav_click_mode = None
self._links_transmitter = None self._links_transmitter = None
self._control_box = None
self._ftp_client = None
# Colors # Colors
self._use_colors = False self._use_colors = False
self._NEW_COLOR = None # Color for new services in the main list self._NEW_COLOR = None # Color for new services in the main list
@@ -225,6 +233,7 @@ class Application(Gtk.Application):
self._app_info_box.bind_property("visible", builder.get_object("toolbar_extra_box"), "visible", 4) self._app_info_box.bind_property("visible", builder.get_object("toolbar_extra_box"), "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("toolbar_tools_box"), "visible", 4) self._app_info_box.bind_property("visible", builder.get_object("toolbar_tools_box"), "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("save_tool_button"), "visible", 4) self._app_info_box.bind_property("visible", builder.get_object("save_tool_button"), "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("add_bouquet_tool_button"), "visible", 4)
# Status bar # Status bar
self._profile_combo_box = builder.get_object("profile_combo_box") self._profile_combo_box = builder.get_object("profile_combo_box")
self._receiver_info_box = builder.get_object("receiver_info_box") self._receiver_info_box = builder.get_object("receiver_info_box")
@@ -245,10 +254,16 @@ class Application(Gtk.Application):
self._signal_level_bar.bind_property("visible", builder.get_object("record_button"), "visible") self._signal_level_bar.bind_property("visible", builder.get_object("record_button"), "visible")
self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4) self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4)
self._receiver_info_box.bind_property("visible", self._signal_box, "visible") self._receiver_info_box.bind_property("visible", self._signal_box, "visible")
# Screenshots # Control
self._screenshots_button = builder.get_object("screenshots_button") self._control_button = builder.get_object("control_button")
self._receiver_info_box.bind_property("visible", self._screenshots_button, "visible") self._receiver_info_box.bind_property("visible", self._control_button, "visible")
# Force ctrl press event for view. Multiple selections in lists only with Space key(as in file managers)!!! self._control_revealer = builder.get_object("control_revealer")
# FTP client
self._ftp_button = builder.get_object("ftp_button")
self._ftp_revealer = builder.get_object("ftp_revealer")
self._ftp_button.bind_property("active", self._ftp_revealer, "visible")
self._ftp_button.set_visible(self._settings.is_enable_experimental)
# Force Ctrl press event for view. Multiple selections in lists only with Space key(as in file managers)!!!
self._services_view.connect("key-press-event", self.force_ctrl) self._services_view.connect("key-press-event", self.force_ctrl)
self._fav_view.connect("key-press-event", self.force_ctrl) self._fav_view.connect("key-press-event", self.force_ctrl)
# Clipboard # Clipboard
@@ -301,10 +316,50 @@ class Application(Gtk.Application):
self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS, self._LOCK_HIDE_ELEMENTS) self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS, self._LOCK_HIDE_ELEMENTS)
self._tool_elements = {k: builder.get_object(k) for k in set(chain.from_iterable(d_elements))} self._tool_elements = {k: builder.get_object(k) for k in set(chain.from_iterable(d_elements))}
# Style # Style
self._style_provider = Gtk.CssProvider() style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css") style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._status_bar_box.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider, self._status_bar_box.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER) Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.init_layout(builder)
def init_layout(self, builder):
""" Initializes an alternate layout, if enabled. """
if self._settings.is_darwin and self._settings.alternate_layout:
top_box = builder.get_object("top_box")
top_toolbar = builder.get_object("top_toolbar")
top_toolbar.set_margin_left(0)
top_toolbar.set_margin_right(10)
extra_box = builder.get_object("toolbar_extra_tools_box")
extra_box.set_margin_left(10)
extra_box.set_margin_right(0)
extra_box.reorder_child(self._ftp_button, 0)
extra_box.reorder_child(builder.get_object("add_bouquet_tool_button"), 2)
top_box.set_child_packing(extra_box, False, True, 0, Gtk.PackType.START)
top_box.set_child_packing(top_toolbar, False, True, 0, Gtk.PackType.END)
top_box.reorder_child(extra_box, 0)
top_box.reorder_child(top_toolbar, 1)
center_box = builder.get_object("center_box")
center_box.reorder_child(self._ftp_revealer, 0)
center_box.reorder_child(self._control_revealer, 1)
center_box.reorder_child(builder.get_object("main_box"), 2)
main_paned = builder.get_object("main_data_paned")
services_box = main_paned.get_child1()
fav_paned = main_paned.get_child2()
main_paned.remove(services_box)
if not self._settings.bq_details_first:
bouquets_box = fav_paned.get_child2()
fav_paned.remove(bouquets_box)
fav_paned.pack2(services_box, True, False)
main_paned.pack1(bouquets_box, True, False)
else:
main_paned.remove(fav_paned)
main_paned.pack1(fav_paned, True, True)
main_paned.pack2(services_box)
def do_startup(self): def do_startup(self):
Gtk.Application.do_startup(self) Gtk.Application.do_startup(self)
@@ -351,21 +406,14 @@ class Application(Gtk.Application):
iptv_handlers = ("on_iptv", "on_import_yt_list", "on_import_m3u", "on_export_to_m3u", iptv_handlers = ("on_iptv", "on_import_yt_list", "on_import_m3u", "on_export_to_m3u",
"on_epg_list_configuration", "on_iptv_list_configuration", "on_remove_all_unavailable") "on_epg_list_configuration", "on_iptv_list_configuration", "on_remove_all_unavailable")
def set_action(n, fun, enabled=True): list(map(lambda x: self.set_action(x, self._handlers.get(x)), main_handlers))
ac = Gio.SimpleAction.new(n, None)
ac.connect("activate", fun)
ac.set_enabled(enabled)
self.add_action(ac)
return ac
list(map(lambda x: set_action(x, self._handlers.get(x)), main_handlers))
# Import # Import
action = set_action("on_import_bouquet", self._handlers.get("on_import_bouquet"), False) action = self.set_action("on_import_bouquet", self._handlers.get("on_import_bouquet"), False)
self._tool_elements.get("bouquet_import_popup_item").bind_property("sensitive", action, "enabled") self._tool_elements.get("bouquet_import_popup_item").bind_property("sensitive", action, "enabled")
# IPTV # IPTV
iptv_elem = self._tool_elements.get("fav_iptv_popup_item") iptv_elem = self._tool_elements.get("fav_iptv_popup_item")
for h in iptv_handlers: for h in iptv_handlers:
action = set_action(h, self._handlers.get(h), False) action = self.set_action(h, self._handlers.get(h), False)
iptv_elem.bind_property("sensitive", action, "enabled") iptv_elem.bind_property("sensitive", action, "enabled")
# Search, Filter # Search, Filter
search_action = Gio.SimpleAction.new_stateful("search", None, GLib.Variant.new_boolean(False)) search_action = Gio.SimpleAction.new_stateful("search", None, GLib.Variant.new_boolean(False))
@@ -379,21 +427,31 @@ class Application(Gtk.Application):
self._app_info_box.bind_property("visible", filter_action, "enabled", 4) self._app_info_box.bind_property("visible", filter_action, "enabled", 4)
self._main_window.add_action(filter_action) self._main_window.add_action(filter_action)
# Lock, Hide # Lock, Hide
self._app_info_box.bind_property("visible", set_action("on_hide", self.on_hide, False), "enabled", 4) self._app_info_box.bind_property("visible", self.set_action("on_hide", self.on_hide, False), "enabled", 4)
self._app_info_box.bind_property("visible", set_action("on_locked", self.on_locked, False), "enabled", 4) self._app_info_box.bind_property("visible", self.set_action("on_locked", self.on_locked, False), "enabled", 4)
# Open and download/upload data # Open and download/upload data
set_action("open_data", lambda a, v: self.open_data()) self.set_action("open_data", lambda a, v: self.open_data())
set_action("on_download_data", self.on_download_data) self.set_action("on_download_data", self.on_download_data)
set_action("upload_all", lambda a, v: self.on_upload_data(DownloadType.ALL)) self.set_action("upload_all", lambda a, v: self.on_upload_data(DownloadType.ALL))
set_action("upload_bouquets", lambda a, v: self.on_upload_data(DownloadType.BOUQUETS)) self.set_action("upload_bouquets", lambda a, v: self.on_upload_data(DownloadType.BOUQUETS))
self.set_action("on_archive_open", self.on_archive_open)
self.set_action("on_import_from_web", self.on_import_from_web)
# Edit # Edit
set_action("on_edit", self.on_edit) self.set_action("on_edit", self.on_edit)
# Save # Save
self._app_info_box.bind_property("visible", set_action("on_data_save", self.on_data_save, False), "enabled", 4) self._app_info_box.bind_property("visible", self.set_action("on_data_save", self.on_data_save, False),
# Screenshots "enabled", 4)
set_action("on_screenshot_all", self.on_screenshot_all) # Control
set_action("on_screenshot_video", self.on_screenshot_video) remote_action = Gio.SimpleAction.new_stateful("on_remote", None, GLib.Variant.new_boolean(False))
set_action("on_screenshot_osd", self.on_screenshot_osd) remote_action.connect("change-state", self.on_control)
self.add_action(remote_action)
def set_action(self, name, fun, enabled=True):
ac = Gio.SimpleAction.new(name, None)
ac.connect("activate", fun)
ac.set_enabled(enabled)
self.add_action(ac)
return ac
def set_accels(self): def set_accels(self):
""" Setting accelerators for the actions. """ """ Setting accelerators for the actions. """
@@ -475,14 +533,14 @@ class Application(Gtk.Application):
self.set_profile(profile) self.set_profile(profile)
def init_drag_and_drop(self): def init_drag_and_drop(self):
""" Enable drag-and-drop """ """ Enable drag-and-drop. """
target = [] target = []
bq_target = [] bq_target = []
self._services_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY) self._services_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
self._services_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) self._services_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._fav_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, self._fav_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target,
Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE | Gdk.DragAction.COPY)
self._fav_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) self._fav_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._bouquets_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, bq_target, self._bouquets_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, bq_target,
Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
@@ -503,6 +561,17 @@ class Application(Gtk.Application):
self._bouquets_view.drag_source_add_text_targets() self._bouquets_view.drag_source_add_text_targets()
self._bouquets_view.drag_dest_add_uri_targets() self._bouquets_view.drag_dest_add_uri_targets()
self._app_info_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
if self._settings.is_darwin:
self._app_info_box.drag_dest_add_uri_targets()
else:
self._app_info_box.drag_dest_add_text_targets()
self._services_view.drag_dest_add_text_targets()
# For multiple selection.
self._services_view.get_selection().set_select_function(lambda *args: self._select_enabled)
self._fav_view.get_selection().set_select_function(lambda *args: self._select_enabled)
self._bouquets_view.get_selection().set_select_function(lambda *args: self._select_enabled)
def init_colors(self, update=False): def init_colors(self, update=False):
""" Initialisation of background colors for the services. """ Initialisation of background colors for the services.
@@ -545,6 +614,10 @@ class Application(Gtk.Application):
event.state |= MOD_MASK event.state |= MOD_MASK
def on_close_app(self, *args): def on_close_app(self, *args):
""" Performing operations before closing the application. """
# Saving the current size of the application window.
self._settings.add("window_size", self._main_window.get_size())
if self._recorder: if self._recorder:
if self._recorder.is_record(): if self._recorder.is_record():
msg = "{}\n\n\t{}".format(get_message("Recording in progress!"), get_message("Are you sure?")) msg = "{}\n\n\t{}".format(get_message("Recording in progress!"), get_message("Are you sure?"))
@@ -559,10 +632,6 @@ class Application(Gtk.Application):
else: else:
GLib.idle_add(self.quit) GLib.idle_add(self.quit)
def on_resize(self, window):
""" Stores new size properties for app window after resize """
self._settings.add("window_size", window.get_size())
@run_idle @run_idle
def on_about_app(self, action, value=None): def on_about_app(self, action, value=None):
show_dialog(DialogType.ABOUT, self._main_window) show_dialog(DialogType.ABOUT, self._main_window)
@@ -763,6 +832,8 @@ class Application(Gtk.Application):
self._bouquets.pop("{}:{}".format(b_row[Column.BQ_NAME], b_row[Column.BQ_TYPE]), None) self._bouquets.pop("{}:{}".format(b_row[Column.BQ_NAME], b_row[Column.BQ_TYPE]), None)
self._bouquets_model.remove(itr) self._bouquets_model.remove(itr)
self._bq_selected = ""
self._bq_name_label.set_text(self._bq_selected)
self._wait_dialog.hide() self._wait_dialog.hide()
yield True yield True
@@ -909,7 +980,6 @@ class Application(Gtk.Application):
self._wait_dialog.show(get_message("Sorting data...")) self._wait_dialog.show(get_message("Sorting data..."))
GLib.idle_add(self.sort_fav, c_num, bq, paths, order, 0 if c_num == Column.FAV_NUM else "") GLib.idle_add(self.sort_fav, c_num, bq, paths, order, 0 if c_num == Column.FAV_NUM else "")
@run_idle
def sort_fav(self, c_num, bq, paths, rev=False, nv=""): def sort_fav(self, c_num, bq, paths, rev=False, nv=""):
""" Sorting function for the bouquet details list. """ Sorting function for the bouquet details list.
@@ -977,30 +1047,116 @@ class Application(Gtk.Application):
pol = ", {}: {},".format(get_message("Pol"), srv.pol) if srv.pol else "," pol = ", {}: {},".format(get_message("Pol"), srv.pol) if srv.pol else ","
fec = "{}: {}".format("FEC", srv.fec) if srv.fec else "," fec = "{}: {}".format("FEC", srv.fec) if srv.fec else ","
ht = "{}{}: {}\n{}: {}\n{}: {}\n{}: {}{} {}, {}: {}\n{}" ht = "{}{}: {}\n{}: {}\n{}: {}\n{}: {}{} {}, {}\n{}"
return ht.format(header, return ht.format(header,
get_message("Package"), srv.package, get_message("Package"), srv.package,
get_message("System"), srv.system, get_message("System"), srv.system,
get_message("Freq"), srv.freq, get_message("Freq"), srv.freq,
get_message("Rate"), srv.rate, pol, fec, "SID", srv.ssid, get_message("Rate"), srv.rate, pol, fec, self.get_ssid_info(srv),
ref) ref)
def get_hint_for_srv_list(self, srv): def get_hint_for_srv_list(self, srv):
""" Returns short info about service as formatted string for using as hint. """ """ Returns short info about service as formatted string for using as hint. """
return "{}{}".format(*self.get_hint_header_info(srv)) header, ref = self.get_hint_header_info(srv)
return "{}{}\n{}".format(header, self.get_ssid_info(srv), ref)
def get_hint_header_info(self, srv): def get_hint_header_info(self, srv):
header = "{}: {}\n{}: {}\n".format(get_message("Name"), srv.service, get_message("Type"), srv.service_type) header = "{}: {}\n{}: {}\n".format(get_message("Name"), srv.service, get_message("Type"), srv.service_type)
ref = "{}: {}".format(get_message("Service reference"), srv.picon_id.rstrip(".png")) ref = "{}: {}".format(get_message("Service reference"), srv.picon_id.rstrip(".png"))
return header, ref return header, ref
def get_ssid_info(self, srv):
""" Returns SID representation in hex and dec formats. """
sid = srv.ssid or "0"
try:
dec = "{0:04d}".format(int(sid, 16))
except ValueError as e:
log("SID value conversion error: {}".format(e))
else:
return "SID: 0x{} ({})".format(sid.upper(), dec)
return "SID: 0x{}".format(sid.upper())
# ***************** Drag-and-drop *********************# # ***************** Drag-and-drop *********************#
def on_view_drag_begin(self, view, context): def on_view_drag_begin(self, view, context):
""" Selects a row under the cursor in the view at the dragging beginning. """ """ Sets its own icon for dragging.
path, column = view.get_cursor()
if path: We have to use "connect_after" (after="yes" in xml) to override what the default handler did.
view.get_selection().select_path(path) https://lazka.github.io/pgi-docs/Gtk-3.0/classes/Widget.html#Gtk.Widget.signals.drag_begin
"""
model, paths = view.get_selection().get_selected_rows()
if len(paths) < 1:
return
name, model = get_model_data(view)
name_column, type_column = Column.SRV_SERVICE, Column.SRV_TYPE
if name == self.FAV_MODEL_NAME:
name_column, type_column = Column.FAV_SERVICE, Column.FAV_TYPE
elif name == self.BQ_MODEL_NAME:
name_column, type_column = Column.BQ_NAME, Column.BQ_TYPE
# https://stackoverflow.com/a/52248549
Gtk.drag_set_icon_pixbuf(context, self.get_drag_icon_pixbuf(model, paths, name_column, type_column), 0, 0)
return True
def on_view_drag_end(self, view, context):
self._select_enabled = True
view.get_selection().unselect_all()
def get_drag_icon_pixbuf(self, model, paths, text_column, type_column):
""" Creates and returns Pixbuf for a dragging icon. """
import cairo
window = Gtk.OffscreenWindow()
window.get_style_context().add_class(Gtk.STYLE_CLASS_DND)
frame = Gtk.Frame()
list_box = Gtk.ListBox()
list_box.set_selection_mode(Gtk.SelectionMode.NONE)
padding = 10
for index, row in enumerate([model[p] for p in paths]):
if index == 25:
list_box.add(Gtk.Arrow(Gtk.ArrowType.DOWN))
break
h_box = Gtk.HBox()
h_box.set_spacing(10)
h_box.get_style_context().add_class(Gtk.STYLE_CLASS_LIST_ROW)
label = Gtk.Label(row[text_column])
label.set_alignment(0, 0)
label.set_padding(padding, 2)
h_box.add(label)
label = Gtk.Label(row[type_column])
label.set_halign(Gtk.Align.END)
label.set_padding(padding, 2)
h_box.add(label)
list_box.add(h_box)
if len(paths) > 1:
list_box.add(Gtk.Separator())
h_box = Gtk.HBox()
h_box.set_spacing(2)
img = Gtk.Image.new_from_icon_name("document-properties", 0)
h_box.add(img)
h_box.add(Gtk.Label(len(paths)))
h_box.set_halign(Gtk.Align.START)
h_box.set_margin_left(10)
h_box.set_margin_bottom(5)
h_box.set_margin_top(2)
list_box.add(h_box)
frame.add(list_box)
frame.show_all()
window.add(frame)
window.show()
alloc = frame.get_allocation()
w, h = alloc.width, alloc.height
surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
frame.draw(cairo.Context(surf))
pix = Gdk.pixbuf_get_from_surface(surf, 0, 0, w, h)
window.destroy()
return pix
def on_view_drag_data_get(self, view, drag_context, data, info, time): def on_view_drag_data_get(self, view, drag_context, data, info, time):
selection = self.get_selection(view) selection = self.get_selection(view)
@@ -1022,17 +1178,23 @@ class Application(Gtk.Application):
def on_view_drag_data_received(self, view, drag_context, x, y, data, info, time): def on_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
txt = data.get_text() txt = data.get_text()
uris = data.get_uris() uris = data.get_uris()
name, model = get_model_data(view)
if txt: if txt:
self.receive_selection(view=view, drop_info=view.get_dest_row_at_pos(x, y), data=txt) if txt.startswith("file://") and name == self.SERVICE_MODEL_NAME:
self.on_import_data(urlparse(unquote(txt)).path.strip())
elif name == self.FAV_MODEL_NAME:
self.receive_selection(view=view, drop_info=view.get_dest_row_at_pos(x, y), data=txt)
if uris: if uris:
from urllib.parse import unquote, urlparse
src, sep, dest = uris[0].partition("::::") src, sep, dest = uris[0].partition("::::")
picon_path = urlparse(unquote(src)).path src_path = urlparse(unquote(src)).path
dest_path = urlparse(unquote(dest)).path + "/" if dest:
self.picons_buffer = self.on_assign_picon(view, picon_path, dest_path) dest_path = urlparse(unquote(dest)).path + "/"
drag_context.finish(True, None, time) self.picons_buffer = self.on_assign_picon(view, src_path, dest_path)
elif name == self.SERVICE_MODEL_NAME:
self.on_import_data(src_path)
drag_context.finish(True, False, time)
def on_bq_view_drag_data_received(self, view, drag_context, x, y, data, info, time): def on_bq_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
model_name, model = get_model_data(view) model_name, model = get_model_data(view)
@@ -1048,7 +1210,11 @@ class Application(Gtk.Application):
if not data: if not data:
return return
itr_str, sep, source = data.partition("::::") if data.startswith("file://"):
self.on_import_bouquet(None, file_path=urlparse(unquote(data)).path.strip())
return
itr_str, sep, source = data.partition(self.DRAG_SEP)
if source != self.BQ_MODEL_NAME: if source != self.BQ_MODEL_NAME:
return return
@@ -1068,7 +1234,8 @@ class Application(Gtk.Application):
break break
if all((not is_darwin, p_itr, model.get_path(p_itr)[0] == p_path)): if all((not is_darwin, p_itr, model.get_path(p_itr)[0] == p_path)):
top_iter = model.move_before(itr, top_iter) model.move_after(itr, top_iter)
top_iter = itr
else: else:
model.insert(parent_itr, model.get_path(top_iter)[1], model[itr][:]) model.insert(parent_itr, model.get_path(top_iter)[1], model[itr][:])
to_del.append(itr) to_del.append(itr)
@@ -1093,7 +1260,7 @@ class Application(Gtk.Application):
def receive_selection(self, *, view, drop_info, data): def receive_selection(self, *, view, drop_info, data):
""" Update fav view after data received """ """ Update fav view after data received """
try: try:
itr_str, sep, source = data.partition("::::") itr_str, sep, source = data.partition(self.DRAG_SEP)
if source == self.BQ_MODEL_NAME: if source == self.BQ_MODEL_NAME:
return return
@@ -1138,10 +1305,22 @@ class Application(Gtk.Application):
self.show_error_dialog(str(e)) self.show_error_dialog(str(e))
def on_view_press(self, view, event): def on_view_press(self, view, event):
""" Handles a mouse click (press) to view. """
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY: if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
target = view.get_path_at_pos(event.x, event.y)
# Idea taken from here: https://kevinmehall.net/2010/pygtk_multi_select_drag_drop
mask = not (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK))
if target and mask and view.get_selection().path_is_selected(target[0]):
self._select_enabled = False
name, model = get_model_data(view) name, model = get_model_data(view)
self.delete_views_selection(name) self.delete_views_selection(name)
def on_view_release(self, view, event):
""" Handles a mouse click (release) to view. """
# Enable selection.
self._select_enabled = True
def delete_views_selection(self, name): def delete_views_selection(self, name):
if name == self.SERVICE_MODEL_NAME: if name == self.SERVICE_MODEL_NAME:
self.delete_selection(self._fav_view) self.delete_selection(self._fav_view)
@@ -1164,7 +1343,7 @@ class Application(Gtk.Application):
self.delete_selection(self._services_view, self._fav_view) self.delete_selection(self._services_view, self._fav_view)
self.on_view_focus(self._bouquets_view) self.on_view_focus(self._bouquets_view)
menu.popup(None, None, None, None, event.button, event.time) menu.popup_at_pointer(None)
return True return True
def on_satellite_editor_show(self, action, value=None): def on_satellite_editor_show(self, action, value=None):
@@ -1225,21 +1404,86 @@ class Application(Gtk.Application):
self.show_error_dialog(str(e)) self.show_error_dialog(str(e))
def on_data_open(self, action=None, value=None): def on_data_open(self, action=None, value=None):
response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings) """ Opening data via "File/Open". """
response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings, title="Open folder")
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self.open_data(response)
def on_archive_open(self, action=None, value=None):
""" Opening the data archive via "File/Open archive". """
file_filter = Gtk.FileFilter()
file_filter.set_name("*.zip, *.gz")
file_filter.add_mime_type("application/zip")
file_filter.add_mime_type("application/gzip")
response = show_dialog(DialogType.CHOOSER, self._main_window,
action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter,
settings=self._settings,
title="Open archive")
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return return
self.open_data(response) self.open_data(response)
def open_data(self, data_path=None, callback=None): def open_data(self, data_path=None, callback=None):
""" Opening data and fill views. """ """ Opening data and fill views. """
gen = self.update_data(data_path, callback) if data_path and os.path.isfile(data_path):
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) self.open_compressed_data(data_path)
else:
gen = self.update_data(data_path, callback)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def open_compressed_data(self, data_path):
""" Opening archived data. """
arch_path = self.get_archive_path(data_path)
if arch_path:
gen = self.update_data("{}{}".format(arch_path.name, os.sep), callback=arch_path.cleanup)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def get_archive_path(self, data_path):
""" Returns the temp dir path for the extracted data, or None if the archive format is not supported. """
import zipfile
import tarfile
import tempfile
tmp_path = tempfile.TemporaryDirectory()
tmp_path_name = tmp_path.name
if zipfile.is_zipfile(data_path):
with zipfile.ZipFile(data_path) as zip_file:
for zip_info in zip_file.infolist():
if not zip_info.filename.endswith(os.sep):
try:
f_name = zip_info.filename.encode("cp437").decode("utf-8")
except (UnicodeEncodeError, UnicodeDecodeError) as e:
log("Filename [{}] error in zip archive: {}".format(zip_info.filename, e))
else:
zip_info.filename = os.path.basename(f_name)
zip_file.extract(zip_info, path=tmp_path_name)
elif tarfile.is_tarfile(data_path):
with tarfile.open(data_path) as tar:
for mb in tar.getmembers():
if mb.isfile():
mb.name = os.path.basename(mb.name)
tar.extract(mb, path=tmp_path_name)
else:
tmp_path.cleanup()
log("Error getting the path for the archive. Unsupported file format: {}".format(data_path))
self.show_error_dialog("Unsupported format!")
return
return tmp_path
def update_data(self, data_path, callback=None): def update_data(self, data_path, callback=None):
self._profile_combo_box.set_sensitive(False) self._profile_combo_box.set_sensitive(False)
self._wait_dialog.show() self._wait_dialog.show()
yield from self.clear_current_data() yield from self.clear_current_data()
# Reset of sorting
self._services_view.get_model().reset_default_sort_func()
self.reset_view_sort_indication(self._services_view)
self.reset_view_sort_indication(self._fav_view)
try: try:
current_profile = self._profile_combo_box.get_active_text() current_profile = self._profile_combo_box.get_active_text()
@@ -1515,8 +1759,17 @@ class Application(Gtk.Application):
self._bouquets_model.append(None, ["Providers", None, None, BqType.BOUQUET.value]) self._bouquets_model.append(None, ["Providers", None, None, BqType.BOUQUET.value])
self._bouquets_model.append(None, ["FAV", None, None, BqType.TV.value]) self._bouquets_model.append(None, ["FAV", None, None, BqType.TV.value])
self._bouquets_model.append(None, ["WEBTV", None, None, BqType.WEBTV.value]) self._bouquets_model.append(None, ["WEBTV", None, None, BqType.WEBTV.value])
self._data_hash = self.get_data_hash()
yield True yield True
def on_fav_selection(self, model, path, column):
if self._control_box and self._control_box.update_epg:
ref = self.get_service_ref(path)
if not ref:
return
self._control_box.on_service_changed(ref)
def on_services_selection(self, model, path, column): def on_services_selection(self, model, path, column):
self.update_service_bar(model, path) self.update_service_bar(model, path)
@@ -1546,7 +1799,7 @@ class Application(Gtk.Application):
if len(path) > 1: if len(path) > 1:
gen = self.update_bouquet_services(model, path) gen = self.update_bouquet_services(model, path)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) GLib.idle_add(lambda: next(gen, False))
def update_bouquet_services(self, model, path, bq_key=None): def update_bouquet_services(self, model, path, bq_key=None):
""" Updates list of bouquet services """ """ Updates list of bouquet services """
@@ -1558,12 +1811,12 @@ class Application(Gtk.Application):
services = self._bouquets.get(key, []) services = self._bouquets.get(key, [])
ex_services = self._extra_bouquets.get(key, None) ex_services = self._extra_bouquets.get(key, None)
factor = self.FAV_FACTOR * 20 if len(services) > self.FAV_FACTOR * 20:
if len(services) > factor or len(self._fav_model) > factor:
self._bouquets_view.set_sensitive(False) self._bouquets_view.set_sensitive(False)
yield True
self._fav_view.set_model(None)
self._fav_model.clear() self._fav_model.clear()
yield True
num = 0 num = 0
for srv_id in services: for srv_id in services:
@@ -1582,11 +1835,11 @@ class Application(Gtk.Application):
self._fav_model.append((0 if is_marker else num, srv.coded, ex_srv_name if ex_srv_name else srv.service, self._fav_model.append((0 if is_marker else num, srv.coded, ex_srv_name if ex_srv_name else srv.service,
srv.locked, srv.hide, srv_type, srv.pos, srv.fav_id, srv.locked, srv.hide, srv_type, srv.pos, srv.fav_id,
self._picons.get(srv.picon_id, None), None, background)) self._picons.get(srv.picon_id, None), None, background))
if num % self.FAV_FACTOR == 0:
yield True
GLib.idle_add(self._bouquets_view.set_sensitive, True) yield True
GLib.idle_add(self._bouquets_view.grab_focus) self._fav_view.set_model(self._fav_model)
self._bouquets_view.set_sensitive(True)
self._bouquets_view.grab_focus()
yield True yield True
def check_bouquet_selection(self): def check_bouquet_selection(self):
@@ -1651,11 +1904,15 @@ class Application(Gtk.Application):
if active in self._settings.profiles: if active in self._settings.profiles:
self.set_profile(active) self.set_profile(active)
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
if self._ftp_button.get_active() and self._ftp_client:
self._ftp_client.init_ftp()
if self._app_info_box.get_visible(): if self._app_info_box.get_visible():
return return
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
if changed: if changed:
self.open_data() self.open_data()
@@ -1934,10 +2191,17 @@ class Application(Gtk.Application):
self.show_error_dialog("No m3u file is selected!") self.show_error_dialog("No m3u file is selected!")
return return
channels = parse_m3u(response, self._s_type) self._wait_dialog.show()
self.get_m3u(response)
if channels and self._bq_selected: @run_task
self.append_imported_services(channels) def get_m3u(self, path):
try:
channels = parse_m3u(path, self._s_type)
if channels and self._bq_selected:
GLib.idle_add(self.append_imported_services, channels)
finally:
GLib.idle_add(self._wait_dialog.hide)
def append_imported_services(self, services): def append_imported_services(self, services):
bq_services = self._bouquets.get(self._bq_selected) bq_services = self._bouquets.get(self._bq_selected)
@@ -1973,6 +2237,16 @@ class Application(Gtk.Application):
else: else:
show_dialog(DialogType.INFO, self._main_window, "Done!") show_dialog(DialogType.INFO, self._main_window, "Done!")
def on_import_data(self, path):
msg = "Combine with the current data?"
if len(self._services_model) > 0 and show_dialog(DialogType.QUESTION, self._main_window,
msg) == Gtk.ResponseType.OK:
self.import_data(path, force=True)
else:
if os.path.isdir(path) and not path.endswith(os.sep):
path += os.sep
self.open_data(path)
def on_import_bouquet(self, action, value=None, file_path=None): def on_import_bouquet(self, action, value=None, file_path=None):
model, paths = self._bouquets_view.get_selection().get_selected_rows() model, paths = self._bouquets_view.get_selection().get_selected_rows()
if not paths: if not paths:
@@ -1987,19 +2261,62 @@ class Application(Gtk.Application):
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return return
self.import_data(response)
def import_data(self, path, force=None, callback=None):
if os.path.isdir(path) and not path.endswith(os.sep):
path += os.sep
elif os.path.isfile(path):
arch_path = self.get_archive_path(path)
if not arch_path:
return
path = arch_path.name + os.sep
callback = arch_path.cleanup
def append(b, s): def append(b, s):
gen = self.append_imported_data(b, s) gen = self.append_imported_data(b, s, callback)
GLib.idle_add(lambda: next(gen, False)) GLib.idle_add(lambda: next(gen, False))
ImportDialog(self._main_window, response, self._settings, self._services.keys(), append).show() dialog = ImportDialog(self._main_window, path, self._settings, self._services.keys(), append)
dialog.import_data() if force else dialog.show()
def append_imported_data(self, bouquets, services): def append_imported_data(self, bouquets, services, callback=None):
try: try:
self._wait_dialog.show() self._wait_dialog.show()
yield from self.append_data(bouquets, services) yield from self.append_data(bouquets, services)
finally: finally:
log("Importing data done!")
if callback:
callback()
self._wait_dialog.hide() self._wait_dialog.hide()
def on_import_from_web(self, action, value=None):
if self._s_type is not SettingsType.ENIGMA_2:
self.show_error_dialog("Not allowed in this context!")
return
ServicesUpdateDialog(self._main_window, self._settings, self.on_import_data_from_web).show()
@run_idle
def on_import_data_from_web(self, services):
msg = "Combine with the current data?"
if len(self._services_model) > 0 and show_dialog(DialogType.QUESTION, self._main_window,
msg) == Gtk.ResponseType.OK:
gen = self.append_imported_data([], services)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
else:
gen = self.import_data_from_web(services)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def import_data_from_web(self, services):
self._wait_dialog.show()
if self._app_info_box.get_visible():
yield from self.create_new_configuration(self._s_type)
yield from self.append_services(services)
self.update_sat_positions()
yield True
self._wait_dialog.hide()
# ***************** Backup ********************# # ***************** Backup ********************#
def on_backup_tool_show(self, action, value=None): def on_backup_tool_show(self, action, value=None):
@@ -2056,8 +2373,7 @@ class Application(Gtk.Application):
self.set_playback_elms_active() self.set_playback_elms_active()
else: else:
if not self._player_box.get_visible(): if not self._player_box.get_visible():
w, h = self._main_window.get_size() self.set_player_area_size(self._player_box)
self._player_box.set_size_request(w * 0.6, -1)
self._current_mrl = url self._current_mrl = url
self._player_box.set_visible(True) self._player_box.set_visible(True)
@@ -2127,14 +2443,13 @@ class Application(Gtk.Application):
self._fav_view.do_grab_focus(self._fav_view) self._fav_view.do_grab_focus(self._fav_view)
def get_time_str(self, duration): def get_time_str(self, duration):
""" returns a string representation of time from duration in milliseconds """ """ Returns a string representation of time from duration in milliseconds """
m, s = divmod(duration // 1000, 60) m, s = divmod(duration // 1000, 60)
h, m = divmod(m, 60) h, m = divmod(m, 60)
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s) return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
def on_drawing_area_realize(self, widget): def on_drawing_area_realize(self, widget):
w, h = self._main_window.get_size() self.set_player_area_size(widget)
widget.set_size_request(w * 0.6, -1)
if not self._player: if not self._player:
try: try:
@@ -2154,6 +2469,10 @@ class Application(Gtk.Application):
finally: finally:
self.set_playback_elms_active() self.set_playback_elms_active()
def set_player_area_size(self, widget):
w, h = self._main_window.get_size()
widget.set_size_request(w * 0.6, -1)
def on_player_drawing_area_draw(self, widget, cr): def on_player_drawing_area_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """ Used for black background drawing in the player drawing area.
@@ -2204,7 +2523,7 @@ class Application(Gtk.Application):
if is_record: if is_record:
self._recorder.stop() self._recorder.stop()
else: else:
self._http_api.send(HttpRequestType.STREAM_CURRENT, None, self.record) self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.record)
def record(self, data): def record(self, data):
url = self.get_url_from_m3u(data) url = self.get_url_from_m3u(data)
@@ -2269,11 +2588,16 @@ class Application(Gtk.Application):
if self._player and self._player.is_playing(): if self._player and self._player.is_playing():
self._player.stop() self._player.stop()
self._http_api.send(HttpRequestType.STREAM, ref, self.watch) self._http_api.send(HttpAPI.Request.STREAM, ref, self.watch)
def on_watch(self, item=None): def on_watch(self, item=None):
""" Switch to the channel and watch in the player """ """ Switch to the channel and watch in the player """
self._http_api.send(HttpRequestType.STREAM_CURRENT, None, self.watch) if self._app_info_box.get_visible() and self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.set_player_area_size(self._player_box)
self._player_box.set_visible(True)
GLib.idle_add(self._app_info_box.set_visible, False)
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.watch)
def watch(self, data): def watch(self, data):
url = self.get_url_from_m3u(data) url = self.get_url_from_m3u(data)
@@ -2333,7 +2657,7 @@ class Application(Gtk.Application):
self.show_error_dialog("No connection to the receiver!") self.show_error_dialog("No connection to the receiver!")
self.set_playback_elms_active() self.set_playback_elms_active()
self._http_api.send(HttpRequestType.ZAP, ref, zap) self._http_api.send(HttpAPI.Request.ZAP, ref, zap)
def get_service_ref(self, path): def get_service_ref(self, path):
row = self._fav_model[path][:] row = self._fav_model[path][:]
@@ -2355,7 +2679,7 @@ class Application(Gtk.Application):
GLib.idle_add(self._receiver_info_box.set_visible, False) GLib.idle_add(self._receiver_info_box.set_visible, False)
return False return False
self._http_api.send(HttpRequestType.INFO, None, self.update_receiver_info) self._http_api.send(HttpAPI.Request.INFO, None, self.update_receiver_info)
return True return True
def update_receiver_info(self, info): def update_receiver_info(self, info):
@@ -2383,10 +2707,13 @@ class Application(Gtk.Application):
def update_service_info(self): def update_service_info(self):
if self._http_api: if self._http_api:
self._http_api.send(HttpRequestType.SIGNAL, None, self.update_signal) self._http_api.send(HttpAPI.Request.SIGNAL, None, self.update_signal)
self._http_api.send(HttpRequestType.CURRENT, None, self.update_status) self._http_api.send(HttpAPI.Request.CURRENT, None, self.update_status)
def update_signal(self, sig): def update_signal(self, sig):
if self._control_box:
self._control_box.update_signal(sig)
self.set_signal(sig.get("e2snr", "0 %") if sig else "0 %") self.set_signal(sig.get("e2snr", "0 %") if sig else "0 %")
@lru_cache(maxsize=2) @lru_cache(maxsize=2)
@@ -2415,40 +2742,35 @@ class Application(Gtk.Application):
self._service_epg_label.set_text(dsc) self._service_epg_label.set_text(dsc)
self._service_epg_label.set_tooltip_text(evn.get("e2eventdescription", "")) self._service_epg_label.set_tooltip_text(evn.get("e2eventdescription", ""))
# ******************** Screenshots ************************# # ******************* Control *********************** #
def on_screenshot_all(self, action, value=None): def on_control(self, action, state=False):
self._http_api.send(HttpRequestType.GRUB, "mode=all" if self._http_api.is_owif else "d=", self.on_screenshot) """ Shows/Hides [R key] remote controller. """
action.set_state(state)
self._control_revealer.set_visible(state)
self._control_revealer.set_reveal_child(state)
def on_screenshot_video(self, action, value=None): if not self._control_box:
self._http_api.send(HttpRequestType.GRUB, "mode=video" if self._http_api.is_owif else "v=", self.on_screenshot) from app.ui.control import ControlBox
self._control_box = ControlBox(self, self._http_api, self._settings)
self._control_revealer.add(self._control_box)
def on_screenshot_osd(self, action, value=None): if state:
self._http_api.send(HttpRequestType.GRUB, "mode=osd" if self._http_api.is_owif else "o=", self.on_screenshot) self._http_api.send(HttpAPI.Request.VOL, "state", self._control_box.update_volume)
@run_task def on_http_status_visible(self, img):
def on_screenshot(self, data): self._control_button.set_active(False)
if "error_code" in data:
return
img = data.get("img_data", None) # ****************** FTP client ********************* #
if img:
is_darwin = self._settings.is_darwin
GLib.idle_add(self._screenshots_button.set_sensitive, is_darwin)
path = os.path.expanduser("~/Desktop") if is_darwin else None
try: def on_ftp_realize(self, revealer):
import tempfile if not self._ftp_client:
import subprocess from app.ui.ftp import FtpClientBox
revealer.set_visible(True)
self._ftp_client = FtpClientBox(self, self._settings)
revealer.add(self._ftp_client)
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=not is_darwin) as tf: # ***************** Filter and search ********************* #
tf.write(img)
cmd = ["open" if is_darwin else "xdg-open", tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._screenshots_button.set_sensitive, True)
# ***************** Filter and search *********************#
def on_filter_toggled(self, action, value): def on_filter_toggled(self, action, value):
if self._app_info_box.get_visible(): if self._app_info_box.get_visible():
@@ -2608,7 +2930,7 @@ class Application(Gtk.Application):
dialog.show() dialog.show()
def on_bouquets_edit(self, view): def on_bouquets_edit(self, view):
""" Rename bouquets """ """ Renaming bouquets. """
if not self._bq_selected: if not self._bq_selected:
self.show_error_dialog("This item is not allowed to edit!") self.show_error_dialog("This item is not allowed to edit!")
return return
@@ -2628,10 +2950,15 @@ class Application(Gtk.Application):
return return
model.set_value(itr, 0, response) model.set_value(itr, 0, response)
self._bouquets[bq] = self._bouquets.pop("{}:{}".format(bq_name, bq_type)) old_bq_name = "{}:{}".format(bq_name, bq_type)
self._bouquets[bq] = self._bouquets.pop(old_bq_name)
self._current_bq_name = response self._current_bq_name = response
self._bq_name_label.set_text(self._current_bq_name) self._bq_name_label.set_text(self._current_bq_name)
self._bq_selected = "{}:{}".format(response, bq_type) self._bq_selected = bq
# services with extra names for the bouquet
ext_bq = self._extra_bouquets.get(old_bq_name, None)
if ext_bq:
self._extra_bouquets[bq] = ext_bq
def on_rename(self, view): def on_rename(self, view):
name, model = get_model_data(view) name, model = get_model_data(view)

View File

@@ -46,54 +46,57 @@ def move_items(key, view: Gtk.TreeView):
""" Move items in the tree view """ """ Move items in the tree view """
selection = view.get_selection() selection = view.get_selection()
model, paths = selection.get_selected_rows() model, paths = selection.get_selected_rows()
if not paths:
return
if paths: is_tree_store = type(model) is Gtk.TreeStore
mod_length = len(model) mod_length = len(model)
if mod_length == len(paths): if not is_tree_store and mod_length == len(paths):
return return
cursor_path = view.get_cursor()[0]
max_path = Gtk.TreePath.new_from_indices((mod_length,)) cursor_path = view.get_cursor()[0]
min_path = Gtk.TreePath.new_from_indices((0,)) max_path = Gtk.TreePath.new_from_indices((mod_length,))
min_path = Gtk.TreePath.new_from_indices((0,))
if is_tree_store:
is_tree_store = False is_tree_store = False
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths))
if parent_paths:
paths = parent_paths
min_path = model.get_path(model.get_iter_first())
view.collapse_all()
if mod_length == len(paths):
return
else:
if not is_some_level(paths):
return
parent_itr = model.iter_parent(model.get_iter(paths[0]))
parent_index = model.get_path(parent_itr)
children_num = model.iter_n_children(parent_itr)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
children_num -= 1
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0))
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num))
is_tree_store = True
if type(model) is Gtk.TreeStore: if key is KeyboardKey.UP:
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths)) top_path = Gtk.TreePath(paths[0])
if parent_paths: set_cursor(top_path, paths, selection, view)
paths = parent_paths top_path.prev()
min_path = model.get_path(model.get_iter_first()) move_up(top_path, model, paths)
view.collapse_all() elif key is KeyboardKey.DOWN:
if mod_length == len(paths): down_path = Gtk.TreePath(paths[-1])
return set_cursor(down_path, paths, selection, view)
else: down_path.next()
if not is_some_level(paths): if down_path < max_path:
return move_down(down_path, model, paths)
parent_itr = model.iter_parent(model.get_iter(paths[0])) else:
parent_index = model.get_path(parent_itr) max_path.prev()
children_num = model.iter_n_children(parent_itr) move_down(max_path, model, paths)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP): elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP):
children_num -= 1 move_up(min_path if is_tree_store else cursor_path, model, paths)
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0)) elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num)) move_down(max_path if is_tree_store else cursor_path, model, paths)
is_tree_store = True
if key is KeyboardKey.UP:
top_path = Gtk.TreePath(paths[0])
set_cursor(top_path, paths, selection, view)
top_path.prev()
move_up(top_path, model, paths)
elif key is KeyboardKey.DOWN:
down_path = Gtk.TreePath(paths[-1])
set_cursor(down_path, paths, selection, view)
down_path.next()
if down_path < max_path:
move_down(down_path, model, paths)
else:
max_path.prev()
move_down(max_path, model, paths)
elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP):
move_up(min_path if is_tree_store else cursor_path, model, paths)
elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
move_down(max_path if is_tree_store else cursor_path, model, paths)
def move_up(top_path, model, paths): def move_up(top_path, model, paths):
@@ -161,7 +164,8 @@ def rename(view, parent_window, target, fav_view=None, service_view=None, servic
return return
srv_name = response srv_name = response
model.set_value(itr, Column.FAV_SERVICE, response) if not model.get_value(itr, Column.FAV_BACKGROUND):
model.set_value(itr, Column.FAV_SERVICE, response)
if service_view is not None: if service_view is not None:
for row in get_base_model(service_view.get_model()): for row in get_base_model(service_view.get_model()):
@@ -401,15 +405,19 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picons_path = dst_path or settings.picons_local_path picons_path = dst_path or settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True) os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id picon_file = picons_path + picon_id
shutil.copy(src_path, picon_file) try:
picons_files.append(picon_file) shutil.copy(src_path, picon_file)
picon = get_picon_pixbuf(picon_file) except shutil.SameFileError:
picons[picon_id] = picon pass # NOP
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
else: else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos) picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
return picons_files return picons_files
@@ -583,8 +591,10 @@ def get_bouquets_names(model):
# ***************** Others *********************# # ***************** Others *********************#
def update_entry_data(entry, dialog, settings): def update_entry_data(entry, dialog, settings):
""" Updates value in text entry from chooser dialog """ """ Updates value in text entry from chooser dialog. """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings) response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings,
action_type=Gtk.FileChooserAction.CREATE_FOLDER if settings.is_darwin else None,
create_dir=True)
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
entry.set_text(response) entry.set_text(response)
return response return response

File diff suppressed because it is too large Load Diff

View File

@@ -1192,7 +1192,7 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="picons_dir_entry"> <object class="GtkEntry" id="picons_dir_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="secondary_icon_name">folder-open</property> <property name="secondary_icon_name">folder-open-symbolic</property>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<signal name="icon-press" handler="on_picons_dir_open" swapped="no"/> <signal name="icon-press" handler="on_picons_dir_open" swapped="no"/>
</object> </object>

View File

@@ -565,10 +565,10 @@ class PiconsDialog:
return return
self.process_provider(Provider(*prv)) self.process_provider(Provider(*prv))
if self._resize_no_radio_button.get_active(): if not self._resize_no_radio_button.get_active():
self.resize(self._picons_dir_entry.get_text()) self.resize(self._picons_dir_entry.get_text())
else:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO) self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
finally: finally:
GLib.idle_add(self._cancel_button.hide) GLib.idle_add(self._cancel_button.hide)
self._terminate = False self._terminate = False
@@ -591,23 +591,31 @@ class PiconsDialog:
if condition == GLib.IO_IN: if condition == GLib.IO_IN:
char = fd.read(1) char = fd.read(1)
self.append_output(char) self.append_output(char)
return True return char
return False return False
@run_idle @run_idle
def append_output(self, char): def append_output(self, char):
append_text_to_tview(char, self._text_view) append_text_to_tview(char, self._text_view)
@run_task
def resize(self, path): def resize(self, path):
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO) self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
exe = "{}mogrify".format("./" if GTK_PATH else "")
is_220_132 = self._resize_220_132_radio_button.get_active()
command = "{} -resize {}! *.png".format(exe, "220x132" if is_220_132 else "100x60").split()
try: try:
self._current_process = subprocess.Popen(command, universal_newlines=True, cwd=path) from pathlib import Path
self._current_process.wait() from PIL import Image
except FileNotFoundError as e: except ImportError as e:
self.show_info_message("Conversion error. " + str(e), Gtk.MessageType.ERROR) self.show_info_message("{} {}".format(get_message("Conversion error."), e), Gtk.MessageType.ERROR)
else:
res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)
for img_file in Path(path).glob("*.png"):
img = Image.open(img_file)
img = img.resize(res, Image.ANTIALIAS)
img.save(img_file, "PNG", optimize=True)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def on_cancel(self, item=None): def on_cancel(self, item=None):
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:

View File

@@ -790,6 +790,7 @@ Author: Dmitriy Yefremov
<property name="use-header-bar">{use_header}</property> <property name="use-header-bar">{use_header}</property>
<property name="title" translatable="yes">Satellite</property> <property name="title" translatable="yes">Satellite</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property> <property name="destroy_with_parent">True</property>
@@ -797,25 +798,6 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property> <property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<child type="action">
<object class="GtkButton" id="sat_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="sat_ok_button">
<property name="label">gtk-ok</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkBox" id="satelitte_dialog_vbox"> <object class="GtkBox" id="satelitte_dialog_vbox">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -825,6 +807,35 @@ Author: Dmitriy Yefremov
<object class="GtkButtonBox" id="dialog-action_area3"> <object class="GtkButtonBox" id="dialog-action_area3">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="layout_style">end</property> <property name="layout_style">end</property>
<child>
<object class="GtkButton" id="sat_cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="sat_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -876,7 +887,7 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="sat_name_entry"> <object class="GtkEntry" id="sat_name_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">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>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property> <property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property> <property name="secondary_icon_sensitive">False</property>
@@ -978,25 +989,6 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property> <property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<child type="action">
<object class="GtkButton" id="tr_cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="tr_ok_button">
<property name="label">gtk-ok</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkBox" id="tr_dialog_vbox"> <object class="GtkBox" id="tr_dialog_vbox">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -1006,6 +998,35 @@ Author: Dmitriy Yefremov
<object class="GtkButtonBox" id="tr_dialog-action_area"> <object class="GtkButtonBox" id="tr_dialog-action_area">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="layout_style">end</property> <property name="layout_style">end</property>
<child>
<object class="GtkButton" id="tr_cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="tr_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="width_request">90</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1109,7 +1130,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">12</property> <property name="width_chars">12</property>
<property name="max_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>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property> <property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property> <property name="secondary_icon_sensitive">False</property>
@@ -1128,7 +1149,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">12</property> <property name="width_chars">12</property>
<property name="max_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>
<property name="placeholder_text" translatable="yes">27500000</property> <property name="placeholder_text" translatable="yes">27500000</property>
<property name="input_purpose">digits</property> <property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/> <signal name="changed" handler="on_entry_changed" swapped="no"/>
@@ -1284,7 +1305,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">5</property> <property name="width_chars">5</property>
<property name="max_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>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">0 - 262142</property> <property name="placeholder_text" translatable="yes">0 - 262142</property>
<property name="input_purpose">digits</property> <property name="input_purpose">digits</property>
@@ -1301,7 +1322,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">5</property> <property name="width_chars">5</property>
<property name="max_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>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">0 - 255</property> <property name="placeholder_text" translatable="yes">0 - 255</property>
<property name="input_purpose">digits</property> <property name="input_purpose">digits</property>
@@ -1385,6 +1406,32 @@ Author: Dmitriy Yefremov
</row> </row>
</data> </data>
</object> </object>
<object class="GtkListStore" id="update_service_store">
<columns>
<!-- column-name picon -->
<column type="GdkPixbuf"/>
<!-- column-name service -->
<column type="gchararray"/>
<!-- column-name package -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name sid -->
<column type="gchararray"/>
<!-- column-name cas -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="update_transponder_store">
<columns>
<!-- column-name transponder -->
<column type="gchararray"/>
<!-- column-name link -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkWindow" id="satellites_update_window"> <object class="GtkWindow" id="satellites_update_window">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Satellites update</property> <property name="title" translatable="yes">Satellites update</property>
@@ -1395,6 +1442,9 @@ Author: Dmitriy Yefremov
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<signal name="delete-event" handler="on_quit" swapped="no"/> <signal name="delete-event" handler="on_quit" swapped="no"/>
<child>
<placeholder/>
</child>
<child> <child>
<object class="GtkBox" id="satellites_update_main_box"> <object class="GtkBox" id="satellites_update_main_box">
<property name="visible">True</property> <property name="visible">True</property>
@@ -1434,9 +1484,9 @@ Author: Dmitriy Yefremov
<property name="valign">center</property> <property name="valign">center</property>
<property name="layout_style">expand</property> <property name="layout_style">expand</property>
<child> <child>
<object class="GtkButton" id="satellites_update_cancel_button"> <object class="GtkButton" id="cancel_data_button">
<property name="label" translatable="yes">Cancel</property> <property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property> <property name="visible">False</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Cancel</property> <property name="tooltip_text" translatable="yes">Cancel</property>
@@ -1452,7 +1502,7 @@ Author: Dmitriy Yefremov
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkButton" id="receive_sat_list_tool_button"> <object class="GtkButton" id="receive_data_button">
<property name="label" translatable="yes">Receive</property> <property name="label" translatable="yes">Receive</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
@@ -1460,7 +1510,7 @@ Author: Dmitriy Yefremov
<property name="tooltip_text" translatable="yes">Receive</property> <property name="tooltip_text" translatable="yes">Receive</property>
<property name="image">sat_receive_image</property> <property name="image">sat_receive_image</property>
<property name="always_show_image">True</property> <property name="always_show_image">True</property>
<signal name="clicked" handler="on_receive_satellites_list" swapped="no"/> <signal name="clicked" handler="on_receive_data" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1772,99 +1822,297 @@ Author: Dmitriy Yefremov
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow" id="sat_update_scrolled_window"> <object class="GtkPaned" id="sat_update_main_paned">
<property name="width_request">480</property>
<property name="height_request">320</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="margin_top">2</property> <property name="orientation">vertical</property>
<property name="shadow_type">in</property> <property name="wide_handle">True</property>
<child> <child>
<object class="GtkTreeView" id="sat_update_tree_view"> <object class="GtkScrolledWindow" id="sat_update_scrolled_window">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="model">update_sat_list_model_sort</property> <property name="margin_top">2</property>
<signal name="button-press-event" handler="on_popup_menu" object="satellites_update_popup_menu" swapped="no"/> <property name="shadow_type">in</property>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="sat_update_treeview_selection">
<property name="mode">multiple</property>
</object>
</child>
<child> <child>
<object class="GtkTreeViewColumn" id="upd_satellite_column"> <object class="GtkTreeView" id="sat_update_tree_view">
<property name="title" translatable="yes">Satellite</property> <property name="visible">True</property>
<property name="expand">True</property> <property name="can_focus">True</property>
<property name="reorderable">True</property> <property name="model">update_sat_list_model_sort</property>
<property name="sort_column_id">0</property> <property name="search_column">0</property>
<child> <property name="activate_on_single_click">True</property>
<object class="GtkCellRendererText" id="upd_satellite_cellrenderertext"/> <child internal-child="selection">
<attributes> <object class="GtkTreeSelection">
<attribute name="text">0</attribute> <property name="mode">multiple</property>
</attributes> </object>
</child> </child>
</object> <child>
</child> <object class="GtkTreeViewColumn" id="upd_satellite_column">
<child> <property name="title" translatable="yes">Satellite</property>
<object class="GtkTreeViewColumn" id="upd_position_column"> <property name="expand">True</property>
<property name="title" translatable="yes">Position</property> <property name="clickable">True</property>
<property name="reorderable">True</property> <property name="alignment">0.5</property>
<property name="sort_column_id">1</property> <property name="reorderable">True</property>
<child> <property name="sort_column_id">0</property>
<object class="GtkCellRendererText" id="upd_position_cellrenderertext"/> <child>
<attributes> <object class="GtkCellRendererText" id="upd_satellite_cellrenderertext">
<attribute name="text">1</attribute> <property name="xalign">0.0099999997764825821</property>
</attributes> </object>
</child> <attributes>
</object> <attribute name="text">0</attribute>
</child> </attributes>
<child> </child>
<object class="GtkTreeViewColumn" id="upd_type_column"> </object>
<property name="title" translatable="yes">Type</property> </child>
<property name="reorderable">True</property> <child>
<property name="sort_column_id">2</property> <object class="GtkTreeViewColumn" id="upd_position_column">
<child> <property name="title" translatable="yes">Position</property>
<object class="GtkCellRendererText" id="upd_type_cellrenderertext"/> <property name="clickable">True</property>
<attributes> <property name="alignment">0.5</property>
<attribute name="text">2</attribute> <property name="reorderable">True</property>
</attributes> <property name="sort_column_id">1</property>
</child> <child>
</object> <object class="GtkCellRendererText" id="upd_position_cellrenderertext">
</child> <property name="xalign">0.49000000953674316</property>
<child> </object>
<object class="GtkTreeViewColumn" id="upd_url_column"> <attributes>
<property name="visible">False</property> <attribute name="text">1</attribute>
<property name="title" translatable="yes">Url</property> </attributes>
<child> </child>
<object class="GtkCellRendererText" id="upd_url_cellrenderertext"/> </object>
<attributes> </child>
<attribute name="text">3</attribute> <child>
</attributes> <object class="GtkTreeViewColumn" id="upd_type_column">
</child> <property name="title" translatable="yes">Type</property>
</object> <property name="clickable">True</property>
</child> <property name="alignment">0.5</property>
<child> <property name="reorderable">True</property>
<object class="GtkTreeViewColumn" id="upd_selected_treeviewcolumn"> <property name="sort_column_id">2</property>
<property name="title" translatable="yes">Selected</property> <child>
<property name="reorderable">True</property> <object class="GtkCellRendererText" id="upd_type_cellrenderertext">
<property name="sort_column_id">4</property> <property name="xalign">0.49000000953674316</property>
<child> </object>
<object class="GtkCellRendererToggle" id="upd_selected_cellrenderertoggle"> <attributes>
<signal name="toggled" handler="on_selected_toggled" swapped="no"/> <attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_url_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Url</property>
<child>
<object class="GtkCellRendererText" id="upd_url_cellrenderertext"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="clickable">True</property>
<property name="reorderable">True</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererToggle" id="upd_selected_cellrenderer">
<signal name="toggled" handler="on_satellite_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">4</attribute>
</attributes>
</child>
</object> </object>
<attributes>
<attribute name="active">4</attribute>
</attributes>
</child> </child>
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkPaned" id="sat_update_tr_paned">
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkScrolledWindow" id="sat_update_tr_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="sat_update_tr_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_transponder_store</property>
<property name="search_column">0</property>
<property name="activate_on_single_click">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_tr_column">
<property name="title" translatable="yes">Transponder</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="upd_tr_renderer">
<property name="xalign">0.0099999997764825821</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_link_column">
<property name="visible">False</property>
<property name="title" translatable="yes">link</property>
<child>
<object class="GtkCellRendererText" id="upd_tr_link_renderer"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="tr_view_selected_column">
<property name="title" translatable="yes">Selected</property>
<child>
<object class="GtkCellRendererToggle" id="upd_tr_select_renderer">
<signal name="toggled" handler="on_transponder_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="sat_update_srv_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="sat_update_srv_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_service_store</property>
<property name="search_column">1</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_servcie_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererPixbuf" id="srv_picon_renderer">
<property name="xpad">2</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="srv_service_renderer">
<property name="xalign">0.0099999997764825821</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_package_column">
<property name="title" translatable="yes">Package</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_package_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_type_column">
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_type_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_sid_column">
<property name="title" translatable="yes">SID</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_sid_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="srv_view_cas_column">
<property name="title" translatable="yes">CAS</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="srv_cas_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">4</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -1875,7 +2123,6 @@ Author: Dmitriy Yefremov
<property name="margin_right">1</property> <property name="margin_right">1</property>
<property name="margin_top">2</property> <property name="margin_top">2</property>
<property name="margin_bottom">2</property> <property name="margin_bottom">2</property>
<property name="resize_toplevel">True</property>
<child> <child>
<object class="GtkScrolledWindow" id="text_view_scrolled_window"> <object class="GtkScrolledWindow" id="text_view_scrolled_window">
<property name="height_request">120</property> <property name="height_request">120</property>

View File

@@ -1,15 +1,15 @@
import concurrent.futures
import re import re
import time import time
import concurrent.futures
from math import fabs from math import fabs
from gi.repository import GLib from gi.repository import GLib
from app.commons import run_idle, run_task from app.commons import run_idle, run_task, log
from app.eparser import get_satellites, write_satellites, Satellite, Transponder from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import PLS_MODE, get_key_by_value from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog 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 .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
from .search import SearchProvider 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, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
@@ -280,7 +280,7 @@ class SatellitesDialog:
@run_idle @run_idle
def on_update(self, item): def on_update(self, item):
SatellitesUpdateDialog(self._window, self._sat_view.get_model()).show() SatellitesUpdateDialog(self._window, self._settings, self._sat_view.get_model()).show()
@staticmethod @staticmethod
def parse_data(model, path, itr, sats): def parse_data(model, path, itr, sats):
@@ -327,7 +327,7 @@ class TransponderDialog:
self._pls_code_entry = builder.get_object("pls_code_entry") self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry") self._is_id_entry = builder.get_object("is_id_entry")
# pattern for frequency and rate entries (only digits) # pattern for frequency and rate entries (only digits)
self._pattern = re.compile("\D") self._pattern = re.compile(r"\D")
# style # style
self._style_provider = Gtk.CssProvider() self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css") self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -429,16 +429,17 @@ class SatelliteDialog:
return Satellite(name=name, flags="0", position=pos, transponders=None) return Satellite(name=name, flags="0", position=pos, transponders=None)
# ***************** Satellite update dialog *******************# # ********************** Update dialogs ************************ #
class SatellitesUpdateDialog: class UpdateDialog:
""" Dialog for update satellites over internet """ """ Base dialog for update satellites, transponders and services from the web."""
def __init__(self, transient, main_model): def __init__(self, transient, settings, title=None):
handlers = {"on_update_satellites_list": self.on_update_satellites_list, handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_satellites_list": self.on_receive_satellites_list, "on_receive_data": self.on_receive_data,
"on_cancel_receive": self.on_cancel_receive, "on_cancel_receive": self.on_cancel_receive,
"on_selected_toggled": self.on_selected_toggled, "on_satellite_toggled": self.on_satellite_toggled,
"on_transponder_toggled": self.on_transponder_toggled,
"on_info_bar_close": self.on_info_bar_close, "on_info_bar_close": self.on_info_bar_close,
"on_filter_toggled": self.on_filter_toggled, "on_filter_toggled": self.on_filter_toggled,
"on_find_toggled": self.on_find_toggled, "on_find_toggled": self.on_find_toggled,
@@ -451,6 +452,11 @@ class SatellitesUpdateDialog:
"on_search_up": self.on_search_up, "on_search_up": self.on_search_up,
"on_quit": self.on_quit} "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()
builder = Gtk.Builder() builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN) builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade", builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
@@ -458,20 +464,26 @@ class SatellitesUpdateDialog:
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store", "update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu", "pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image", "remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
"sat_update_filter_image", "sat_update_search_image", "sat_update_image")) "sat_update_filter_image", "sat_update_search_image", "sat_update_image",
"update_transponder_store", "update_service_store"))
builder.connect_signals(handlers) builder.connect_signals(handlers)
self._window = builder.get_object("satellites_update_window") self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient) self._window.set_transient_for(transient)
self._main_model = main_model if title:
# self._dialog.get_content_area().set_border_width(0) self._window.set_title(title)
self._transponder_paned = builder.get_object("sat_update_tr_paned")
self._sat_view = builder.get_object("sat_update_tree_view") 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")
self._source_box = builder.get_object("source_combo_box") self._source_box = builder.get_object("source_combo_box")
self._sat_update_expander = builder.get_object("sat_update_expander") self._sat_update_expander = builder.get_object("sat_update_expander")
self._text_view = builder.get_object("text_view") self._text_view = builder.get_object("text_view")
self._receive_button = builder.get_object("receive_sat_list_tool_button") self._receive_button = builder.get_object("receive_data_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar") 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._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)
# Filter # Filter
self._filter_bar = builder.get_object("sat_update_filter_bar") self._filter_bar = builder.get_object("sat_update_filter_bar")
self._from_pos_button = builder.get_object("from_pos_button") self._from_pos_button = builder.get_object("from_pos_button")
@@ -487,21 +499,31 @@ class SatellitesUpdateDialog:
builder.get_object("sat_update_search_down_button"), builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button")) builder.get_object("sat_update_search_up_button"))
self._download_task = False window_size = self._settings.get(self._size_name)
self._parser = None if window_size:
self._window.resize(*window_size)
def show(self): def show(self):
self._window.show() self._window.show()
@property
def is_download(self):
return self._download_task
@is_download.setter
def is_download(self, value):
self._download_task = value
self._receive_button.set_visible(not value)
@run_idle @run_idle
def on_update_satellites_list(self, item): def on_update_satellites_list(self, item):
if self._download_task: if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!") show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return return
model = get_base_model(self._sat_view.get_model()) model = get_base_model(self._sat_view.get_model())
model.clear() model.clear()
self._download_task = True self.is_download = True
src = self._source_box.get_active() src = self._source_box.get_active()
if not self._parser: if not self._parser:
self._parser = SatellitesParser() self._parser = SatellitesParser()
@@ -513,7 +535,7 @@ class SatellitesUpdateDialog:
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT) sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
if sats: if sats:
callback(sats) callback(sats)
self._download_task = False self.is_download = False
@run_idle @run_idle
def append_satellites(self, sats): def append_satellites(self, sats):
@@ -522,70 +544,16 @@ class SatellitesUpdateDialog:
model.append(sat) model.append(sat)
@run_idle @run_idle
def on_receive_satellites_list(self, item): def on_receive_data(self, item):
if self._download_task: if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!") show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return return
self.receive_satellites()
@run_task
def receive_satellites(self):
self._download_task = True
self.update_expander()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
self._download_task = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self._download_task = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed : {:0.0f}s, {} satellites received.".format(start - time.time(), len(sats)))
appender.close()
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
self._download_task = False
@run_idle @run_idle
def update_expander(self): def update_expander(self):
self._sat_update_expander.set_expanded(True) self._sat_update_expander.set_expanded(True)
self._text_view.get_buffer().set_text("", 0) self._text_view.get_buffer().set_text("", 0)
@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_output(self): def append_output(self):
@run_idle @run_idle
def append(t): def append(t):
@@ -598,11 +566,15 @@ class SatellitesUpdateDialog:
def on_cancel_receive(self, item=None): def on_cancel_receive(self, item=None):
self._download_task = False self._download_task = False
def on_selected_toggled(self, toggle, path): def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model() model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active()) self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model) self.update_receive_button_state(self._filter_model)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
model.set_value(model.get_iter(path), 2, not toggle.get_active())
@run_idle @run_idle
def update_receive_button_state(self, model): def update_receive_button_state(self, model):
self._receive_button.set_sensitive((any(r[4] for r in model))) self._receive_button.set_sensitive((any(r[4] for r in model)))
@@ -627,7 +599,7 @@ class SatellitesUpdateDialog:
self._filter_positions = self.get_positions() self._filter_positions = self.get_positions()
self._filter_model.refilter() self._filter_model.refilter()
def filter_function(self, model, iter, data): def filter_function(self, model, itr, data):
if self._filter_model is None or self._filter_model == "None": if self._filter_model is None or self._filter_model == "None":
return True return True
@@ -638,7 +610,7 @@ class SatellitesUpdateDialog:
if from_pos > to_pos: if from_pos > to_pos:
from_pos, to_pos = to_pos, from_pos from_pos, to_pos = to_pos, from_pos
return from_pos <= float(self._parser.get_position(model.get(iter, 1)[0])) <= to_pos return from_pos <= float(self._parser.get_position(model.get(itr, 1)[0])) <= to_pos
def get_positions(self): def get_positions(self):
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1) from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
@@ -671,10 +643,298 @@ class SatellitesUpdateDialog:
self._filter_model.get_model().set_value(itr, 4, select) self._filter_model.get_model().set_value(itr, 4, select)
def on_quit(self, window, event): def on_quit(self, window, event):
self._download_task = False self._settings.add(self._size_name, window.get_size())
self.is_download = False
# ***************** Commons *******************# class SatellitesUpdateDialog(UpdateDialog):
""" Dialog for update satellites from the web. """
def __init__(self, transient, settings, main_model):
super().__init__(transient=transient, settings=settings)
self._main_model = main_model
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self.is_download = True
self.update_expander()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
self.is_download = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self.is_download = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} satellites received.".format(time.time() - start, len(sats)))
appender.close()
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
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])
class ServicesUpdateDialog(UpdateDialog):
""" Dialog for updating services from the web. """
def __init__(self, transient, settings, callback):
super().__init__(transient=transient, settings=settings, title="Services update")
self._callback = callback
self._satellite_paths = {}
self._transponders = {}
self._services = {}
self._selected_transponders = set()
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
self._transponder_paned.set_visible(True)
s_model = self._source_box.get_model()
s_model.remove(s_model.get_iter_first())
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")
select_all_item.connect("activate", lambda w: self.update_transponder_selection(True))
tr_popup_menu.append(select_all_item)
remove_selection_item = Gtk.ImageMenuItem.new_from_stock("gtk-undo")
remove_selection_item.set_label(get_message("Remove selection"))
remove_selection_item.connect("activate", lambda w: self.update_transponder_selection(False))
tr_popup_menu.append(remove_selection_item)
tr_popup_menu.show_all()
self._sat_view.connect("row-activated", self.on_activate_satellite)
self._transponder_view.connect("row-activated", self.on_activate_transponder)
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))
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_services()
@run_task
def receive_services(self):
self.is_download = True
self.update_expander()
model = self._sat_view.get_model()
appender = self.append_output()
next(appender)
start = time.time()
non_cached_sats = []
sat_names = {}
t_names = {}
t_urls = []
services = []
for r in (r for r in model if r[-1]):
if not self.is_download:
appender.send("\nCanceled\n")
return
sat, url = r[0], r[3]
trs = self._transponders.get(url, None)
if trs:
for t in filter(lambda tp: tp.url in self._selected_transponders, trs):
t_urls.append(t.url)
t_names[t.url] = t.text
else:
non_cached_sats.append(url)
sat_names[url] = sat
if non_cached_sats:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponders_links, u): u for u in non_cached_sats}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send("Getting transponders for: {}.\n".format(sat_names.get(futures[future])))
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)))
non_cached_ts = []
for tr in t_urls:
srvs = self._services.get(tr)
services.extend(srvs) if srvs else non_cached_ts.append(tr)
if non_cached_ts:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponder_services, u): u for u in non_cached_ts}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send("Getting services for: {}.\n".format(t_names.get(futures[future], "")))
list(map(services.append, future.result()))
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} services received.".format(time.time() - start, len(services)))
try:
from app.eparser.enigma.lamedb import get_services_lines, get_services_list
# Used for double checking!
srvs = get_services_list("".join(get_services_lines(services)))
except ValueError as e:
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
else:
self._callback(srvs)
self.is_download = False
@run_task
def get_sat_list(self, src, callback):
sats = self._parser.get_satellites_list(SatelliteSource.LYNGSAT)
if sats:
callback(sats)
self.is_download = False
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
url = model.get_value(model.get_iter(path), 3)
selected = toggle.get_active()
transponders = self._transponders.get(url, None)
if transponders:
for t in transponders:
self._selected_transponders.add(t.url) if selected else self._selected_transponders.discard(t.url)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
itr = model.get_iter(path)
active = not toggle.get_active()
url = self.update_transponder_state(itr, model, active)
s_path = self._satellite_paths.get(url, None)
if s_path:
self.update_sat_state(model, s_path, active)
def update_sat_state(self, model, path, active):
sat_model = self._sat_view.get_model()
if active:
self.update_state(sat_model, path, active)
else:
self.update_state(sat_model, path, any((r[-1] for r in model)))
self.update_receive_button_state(self._filter_model)
def update_transponder_state(self, itr, model, active):
model.set_value(itr, 2, active)
url = model.get_value(itr, 1)
self._selected_transponders.add(url) if active else self._selected_transponders.discard(url)
return url
@run_task
def on_activate_satellite(self, view, path, column):
model = view.get_model()
itr = model.get_iter(path)
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
transponders = self._transponders.get(url, None)
if transponders is None:
GLib.idle_add(view.set_sensitive, False)
transponders = self._services_parser.get_transponders_links(url)
self._transponders[url] = transponders
for t in transponders:
t_url = t.url
self._satellite_paths[t_url] = path
self._selected_transponders.add(t_url) if selected else self._selected_transponders.discard(t_url)
self.append_transponders(self._transponder_view.get_model(), transponders)
@run_idle
def append_transponders(self, model, trs_list):
model.clear()
list(map(model.append, [(t.text, t.url, t.url in self._selected_transponders) for t in trs_list]))
self._sat_view.set_sensitive(True)
@run_task
def on_activate_transponder(self, view, path, column):
url = view.get_model()[path][1]
services = self._services.get(url, None)
if services is None:
GLib.idle_add(view.set_sensitive, False)
services = self._services_parser.get_transponder_services(url)
self._services[url] = services
self.append_services(self._service_view.get_model(), services)
@run_idle
def append_services(self, model, srv_list):
model.clear()
for s in srv_list:
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
self._transponder_view.set_sensitive(True)
def update_transponder_selection(self, select):
m = self._transponder_view.get_model()
if not len(m):
return
s_path = self._satellite_paths.get({self.update_transponder_state(r.iter, m, select) for r in m}.pop(), None)
if s_path:
self.update_sat_state(m, s_path, select)
# ************************* Commons ************************* #
@run_idle @run_idle
def append_satellite(model, sat): def append_satellite(model, sat):

View File

@@ -1,15 +1,16 @@
import re
import os import os
import re
from app.commons import run_idle from app.commons import run_idle
from app.eparser import Service from app.eparser import Service
from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, \ from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION, TrType, \ get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
HIERARCHY)
from app.settings import SettingsType from app.settings import SettingsType
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .main_helper import get_base_model 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
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade" _UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
@@ -438,14 +439,17 @@ class ServiceDetailsDialog:
def update_fav_view(self, old_service, new_service): def update_fav_view(self, old_service, new_service):
model = self._fav_view.get_model() model = self._fav_view.get_model()
for row in filter(lambda r: old_service.fav_id == r[7], model): for row in filter(lambda r: old_service.fav_id == r[7], model):
model.set(row.iter, {1: new_service.coded, itr = row.iter
2: new_service.service, if not model.get_value(itr, Column.FAV_BACKGROUND):
3: new_service.locked, model.set_value(itr, Column.FAV_SERVICE, new_service.service)
4: new_service.hide,
5: new_service.service_type, model.set(itr, {Column.FAV_CODED: new_service.coded,
6: new_service.pos, Column.FAV_LOCKED: new_service.locked,
7: new_service.fav_id, Column.FAV_HIDE: new_service.hide,
8: new_service.picon}) Column.FAV_TYPE: new_service.service_type,
Column.FAV_POS: new_service.pos,
Column.FAV_ID: new_service.fav_id,
Column.FAV_PICON: new_service.picon})
def update_picon_name(self, old_name, new_name): 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_dir_path):

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ class SettingsDialog:
"on_set_color_switch": self.on_set_color_switch, "on_set_color_switch": self.on_set_color_switch,
"on_force_bq_name": self.on_force_bq_name, "on_force_bq_name": self.on_force_bq_name,
"on_http_mode_switch": self.on_http_mode_switch, "on_http_mode_switch": self.on_http_mode_switch,
"on_experimental_switch": self.on_experimental_switch,
"on_yt_dl_switch": self.on_yt_dl_switch, "on_yt_dl_switch": self.on_yt_dl_switch,
"on_default_path_mode_switch": self.on_default_path_mode_switch, "on_default_path_mode_switch": self.on_default_path_mode_switch,
"on_default_data_path_changed": self.on_default_data_path_changed, "on_default_data_path_changed": self.on_default_data_path_changed,
@@ -60,7 +61,7 @@ class SettingsDialog:
"on_theme_changed": self.on_theme_changed, "on_theme_changed": self.on_theme_changed,
"on_theme_add": self.on_theme_add, "on_theme_add": self.on_theme_add,
"on_theme_remove": self.on_theme_remove, "on_theme_remove": self.on_theme_remove,
"on_icon_theme_changed": self.on_icon_theme_changed, "on_appearance_changed": self.on_appearance_changed,
"on_icon_theme_add": self.on_icon_theme_add, "on_icon_theme_add": self.on_icon_theme_add,
"on_icon_theme_remove": self.on_icon_theme_remove} "on_icon_theme_remove": self.on_icon_theme_remove}
@@ -134,8 +135,7 @@ class SettingsDialog:
# Program # Program
self._before_save_switch = builder.get_object("before_save_switch") self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch") self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._program_frame = builder.get_object("program_frame") self._enable_experimental_box = builder.get_object("enable_experimental_box")
self._extra_support_grid = builder.get_object("extra_support_grid")
self._colors_grid = builder.get_object("colors_grid") self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch") self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button") self._new_color_button = builder.get_object("new_color_button")
@@ -156,10 +156,18 @@ class SettingsDialog:
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button") 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_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive") self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._enable_send_to_switch, "sensitive") # EXPERIMENTAL
self._enable_send_to_switch.bind_property("sensitive", builder.get_object("enable_send_to_label"), "sensitive") self._enable_exp_switch = builder.get_object("enable_experimental_switch")
self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive") 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_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
# Enigma2 only
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("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_view = builder.get_object("profile_tree_view")
self._profile_add_button = builder.get_object("profile_add_button") self._profile_add_button = builder.get_object("profile_add_button")
@@ -184,6 +192,10 @@ class SettingsDialog:
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image") self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
self._theme_combo_box = builder.get_object("theme_combo_box") self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_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._layout_switch = builder.get_object("layout_switch")
self._bq_list_switch = builder.get_object("bq_list_switch")
self._layout_switch.bind_property("active", builder.get_object("bouquet_box"), "sensitive")
self._themes_support_switch = builder.get_object("themes_support_switch") self._themes_support_switch = builder.get_object("themes_support_switch")
self._themes_support_switch.bind_property("active", builder.get_object("gtk_theme_frame"), "sensitive") self._themes_support_switch.bind_property("active", builder.get_object("gtk_theme_frame"), "sensitive")
self._themes_support_switch.bind_property("active", builder.get_object("icon_theme_frame"), "sensitive") self._themes_support_switch.bind_property("active", builder.get_object("icon_theme_frame"), "sensitive")
@@ -195,8 +207,6 @@ class SettingsDialog:
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP) self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
self.update_title() self.update_title()
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile) self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
self._program_frame.set_sensitive(is_enigma_profile)
self._extra_support_grid.set_sensitive(is_enigma_profile)
http_active = self._support_http_api_switch.get_active() http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_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) self._lang_combo_box.set_active_id(self._ext_settings.language)
@@ -282,6 +292,7 @@ class SettingsDialog:
self.on_transcoding_preset_changed(self._presets_combo_box) self.on_transcoding_preset_changed(self._presets_combo_box)
if self._s_type is SettingsType.ENIGMA_2: if self._s_type is SettingsType.ENIGMA_2:
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
self._support_ver5_switch.set_active(self._settings.v5_support) self._support_ver5_switch.set_active(self._settings.v5_support)
self._force_bq_name_switch.set_active(self._settings.force_bq_names) self._force_bq_name_switch.set_active(self._settings.force_bq_names)
self._support_http_api_switch.set_active(self._settings.http_api_support) self._support_http_api_switch.set_active(self._settings.http_api_support)
@@ -349,11 +360,15 @@ class SettingsDialog:
self._ext_settings.active_preset = self._presets_combo_box.get_active_id() self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
if self._ext_settings.is_darwin: if self._ext_settings.is_darwin:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.alternate_layout = self._layout_switch.get_active()
self._ext_settings.bq_details_first = self._bq_list_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active() self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id() self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id() self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
if self._s_type is SettingsType.ENIGMA_2: if self._s_type is SettingsType.ENIGMA_2:
self._ext_settings.is_enable_experimental = self._enable_exp_switch.get_active()
self._ext_settings.use_colors = self._set_color_switch.get_active() self._ext_settings.use_colors = self._set_color_switch.get_active()
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string() self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string() self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
@@ -439,6 +454,12 @@ class SettingsDialog:
self._click_mode_zap_and_play_button.get_active())): self._click_mode_zap_and_play_button.get_active())):
self._click_mode_disabled_button.set_active(True) self._click_mode_disabled_button.set_active(True)
def on_experimental_switch(self, switch, state):
if not state:
self._support_ver5_switch.set_active(state)
self._enable_send_to_switch.set_active(state)
self._enable_yt_dl_switch.set_active(state)
def on_force_bq_name(self, switch, state): def on_force_bq_name(self, switch, state):
if self._main_stack.get_visible_child_name() != "extra": if self._main_stack.get_visible_child_name() != "extra":
return return
@@ -686,7 +707,7 @@ class SettingsDialog:
Gtk.Settings().get_default().set_property("gtk-theme-name", "") Gtk.Settings().get_default().set_property("gtk-theme-name", "")
self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path) self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path)
def on_icon_theme_changed(self, button, state=False): def on_appearance_changed(self, button, state=False):
if self._main_stack.get_visible_child_name() != "appearance": if self._main_stack.get_visible_child_name() != "appearance":
return return
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING) self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@@ -758,6 +779,9 @@ class SettingsDialog:
@run_idle @run_idle
def init_appearance(self): def init_appearance(self):
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
self._layout_switch.set_active(self._ext_settings.alternate_layout)
self._bq_list_switch.set_active(self._ext_settings.bq_details_first)
t_support = self._ext_settings.is_themes_support t_support = self._ext_settings.is_themes_support
self._themes_support_switch.set_active(t_support) self._themes_support_switch.set_active(t_support)
if t_support: if t_support:

View File

@@ -7,10 +7,35 @@
margin: 0.1em; margin: 0.1em;
} }
.red-button {
background-image: none;
background-color: red;
}
.green-button {
background-image: none;
background-color: green;
}
.yellow-button {
background-image: none;
background-color: yellow;
}
.blue-button {
background-image: none;
background-color: blue;
}
#textview-large { #textview-large {
font-size: 14px; font-size: 14px;
} }
.time-entry {
padding: 0px;
margin: 0px;
}
.group {} .group {}
.group :first-child { .group :first-child {
@@ -23,10 +48,11 @@
.group :last-child { .group :last-child {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-left-width: 0;
} }
.group :not(:first-child):not(:last-child) { .group :not(:first-child):not(:last-child) {
border-radius: 0; border-radius: 0;
border-left-width: 0; border-left-width: 0;
border-right-width: 0; border-right-width: 1px;
} }

141
app/ui/timer_row.glade Normal file
View File

@@ -0,0 +1,141 @@
<?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>

View File

@@ -6,7 +6,7 @@ from gi.repository import GLib
from app.commons import log from app.commons import log
from app.settings import IS_DARWIN from app.settings import IS_DARWIN
from app.connections import HttpRequestType from app.connections import HttpAPI
from app.tools.yt import YouTube from app.tools.yt import YouTube
from app.ui.iptv import get_yt_icon from app.ui.iptv import get_yt_icon
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
@@ -128,7 +128,7 @@ class LinksTransmitter:
else: else:
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None) self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
self._http_api.send(HttpRequestType.PLAY, url, self.on_done, self.__STREAM_PREFIX) self._http_api.send(HttpAPI.Request.PLAY, url, self.on_done, self.__STREAM_PREFIX)
yield True yield True
def on_done(self, res): def on_done(self, res):
@@ -138,21 +138,21 @@ class LinksTransmitter:
GLib.idle_add(self._tool_bar.set_sensitive, True) GLib.idle_add(self._tool_bar.set_sensitive, True)
def on_previous(self, item): def on_previous(self, item):
self._http_api.send(HttpRequestType.PLAYER_PREV, None, self.on_done) self._http_api.send(HttpAPI.Request.PLAYER_PREV, None, self.on_done)
def on_next(self, item): def on_next(self, item):
self._http_api.send(HttpRequestType.PLAYER_NEXT, None, self.on_done) self._http_api.send(HttpAPI.Request.PLAYER_NEXT, None, self.on_done)
def on_play(self, item): def on_play(self, item):
self._http_api.send(HttpRequestType.PLAYER_PLAY, None, self.on_done) self._http_api.send(HttpAPI.Request.PLAYER_PLAY, None, self.on_done)
def on_stop(self, item): def on_stop(self, item):
self._http_api.send(HttpRequestType.PLAYER_STOP, None, self.on_done) self._http_api.send(HttpAPI.Request.PLAYER_STOP, None, self.on_done)
def on_clear(self, item): def on_clear(self, item):
""" Remove added links in the playlist. """ """ Remove added links in the playlist. """
GLib.idle_add(self._tool_bar.set_sensitive, False) GLib.idle_add(self._tool_bar.set_sensitive, False)
self._http_api.send(HttpRequestType.PLAYER_LIST, None, self.clear_playlist) self._http_api.send(HttpAPI.Request.PLAYER_LIST, None, self.clear_playlist)
def clear_playlist(self, res): def clear_playlist(self, res):
GLib.idle_add(self._tool_bar.set_sensitive, not res) GLib.idle_add(self._tool_bar.set_sensitive, not res)
@@ -163,7 +163,7 @@ class LinksTransmitter:
for ref in res: for ref in res:
GLib.idle_add(self._tool_bar.set_sensitive, False) GLib.idle_add(self._tool_bar.set_sensitive, False)
self._http_api.send(HttpRequestType.PLAYER_REMOVE, self._http_api.send(HttpAPI.Request.PLAYER_REMOVE,
ref.get("e2servicereference", ""), ref.get("e2servicereference", ""),
self.on_done, self.on_done,
self.__STREAM_PREFIX) self.__STREAM_PREFIX)

View File

@@ -26,9 +26,10 @@ except SettingsException:
pass pass
else: else:
os.environ["LANGUAGE"] = settings.language os.environ["LANGUAGE"] = settings.language
st = Gtk.Settings().get_default()
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
if settings.is_themes_support: if settings.is_themes_support:
st = Gtk.Settings().get_default()
st.set_property("gtk-theme-name", settings.theme) st.set_property("gtk-theme-name", settings.theme)
st.set_property("gtk-icon-theme-name", settings.icon_theme) st.set_property("gtk-icon-theme-name", settings.icon_theme)
else: else:
@@ -145,6 +146,7 @@ class KeyboardKey(Enum):
LEFT = 123 if IS_DARWIN else 113 LEFT = 123 if IS_DARWIN else 113
RIGHT = 123 if IS_DARWIN else 114 RIGHT = 123 if IS_DARWIN else 114
F2 = 120 if IS_DARWIN else 68 F2 = 120 if IS_DARWIN else 68
F7 = 98 if IS_DARWIN else 73
SPACE = 49 if IS_DARWIN else 65 SPACE = 49 if IS_DARWIN else 65
DELETE = 51 if IS_DARWIN else 119 DELETE = 51 if IS_DARWIN else 119
BACK_SPACE = 76 if IS_DARWIN else 22 BACK_SPACE = 76 if IS_DARWIN else 22

1188
po/be/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "Charly, Dmitriy Yefremov" msgstr "Charly\nDmitriy Yefremov"
# Main # Main
msgid "Service" msgid "Service"
@@ -105,6 +105,9 @@ msgstr "Standardbezeichnung setzen"
msgid "Insert marker" msgid "Insert marker"
msgstr "Marker einfügen" msgstr "Marker einfügen"
msgid "Insert space"
msgstr "Leerzeichen einfügen"
msgid "Locate in services" msgid "Locate in services"
msgstr "In den Diensten suchen" msgstr "In den Diensten suchen"
@@ -160,7 +163,7 @@ msgid "Remove"
msgstr "Entfernen" msgstr "Entfernen"
msgid "Remove all unavailable" msgid "Remove all unavailable"
msgstr "Entfernt alle nicht verfügbaren" msgstr "Entfernen alle nicht verfügbaren"
msgid "Satellites editor" msgid "Satellites editor"
msgstr "Satelliten-Editor" msgstr "Satelliten-Editor"
@@ -201,8 +204,8 @@ msgstr "Aktueller Datenpfad:"
msgid "Data:" msgid "Data:"
msgstr "Daten:" msgstr "Daten:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Enigma2 Kanal- und Satellitenlisteneditor für GNU/Linux" msgstr "Enigma2 Kanal- und Satellitenlisteneditor für GNU/Linux."
msgid "Host:" msgid "Host:"
msgstr "Host:" msgstr "Host:"
@@ -516,6 +519,9 @@ msgstr "Bitte wählen Sie nur einen Eintrag aus!"
msgid "No png file is selected!" msgid "No png file is selected!"
msgstr "Es ist keine png-Datei ausgewählt!" msgstr "Es ist keine png-Datei ausgewählt!"
msgid "No profile selected!"
msgstr "Kein Profil ausgewählt!"
msgid "No reference is present!" msgid "No reference is present!"
msgstr "Es liegt keine Referenz vor!" msgstr "Es liegt keine Referenz vor!"
@@ -671,11 +677,11 @@ msgstr "Play Stream"
msgid "Disabled" msgid "Disabled"
msgstr "Ausgeschaltet" msgstr "Ausgeschaltet"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Lamedb ver. 5 Unterstützung aktivieren (experimentell)" msgstr "Lamedb ver. 5 Unterstützung aktivieren"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "HTTP-API aktivieren (experimentell)" msgstr "HTTP-API aktivieren"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Umschalten des Kanals (Strg + Z)" msgstr "Umschalten des Kanals (Strg + Z)"
@@ -795,8 +801,8 @@ msgstr "Sprache:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Laden der zuletzt geöffneten Konfiguration beim Programmstart" msgstr "Laden der zuletzt geöffneten Konfiguration beim Programmstart"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Aktivieren der direkten Wiedergabeleiste (experimentell)" msgstr "Aktivieren der direkten Wiedergabeleiste"
msgid "Enables direct sending and playback of media links on the receiver" 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 Box"
@@ -1023,3 +1029,173 @@ msgstr "Screenshot"
msgid "Video" msgid "Video"
msgstr "Video" msgstr "Video"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Die Neutrino hat nur experimentelle Unterstützung. Nicht alle Funktionen werden unterstützt!"
msgid "Enable experimental features"
msgstr "Experimentelle Funktionen aktivieren"
msgid "Can't Playback!"
msgstr "Kann nicht abgespielt werden!"
msgid "Enable Dark Mode"
msgstr "Dunkelmodus aktivieren"
msgid "Extract..."
msgstr "Entpacken..."
msgid "Unsupported format!"
msgstr "Nicht unterstütztes Format!"
msgid "Combine with the current data?"
msgstr "Mit den aktuellen Daten kombinieren?"
msgid "Importing data done!"
msgstr "Daten importieren erledigt!"
msgid "Current service"
msgstr "Aktueller Service"
msgid "Open folder"
msgstr "Ordner öffnen"
msgid "Open archive"
msgstr "Archiv öffnen"
msgid "Import from Web"
msgstr "Import aus dem Web"
msgid "Control"
msgstr "Steuerung"
msgid "Timers"
msgstr "Timers"
msgid "Timer"
msgstr "Timer"
msgid "Add timer"
msgstr "Timer hinzufügen"
msgid "Hr."
msgstr "Std."
msgid "Min."
msgstr "Min."
msgid "Power"
msgstr "Power"
msgid "Standby"
msgstr "Standby"
msgid "Wake Up"
msgstr "Aufwachen"
msgid "Reboot"
msgstr "Neustarten"
msgid "Restart GUI"
msgstr "GUI neustarten"
msgid "Shutdown"
msgstr "Ausschalten"
msgid "Shut down"
msgstr "Ausschalten"
msgid "Do Nothing"
msgstr "Nichts tun"
msgid "Auto"
msgstr "Auto"
msgid "Grab screenshot"
msgstr "Screenshot schnappen"
msgid "Enabled:"
msgstr "Aktiviert:"
msgid "Name:"
msgstr "Name:"
msgid "Description:"
msgstr "Beschreibung:"
msgid "Service:"
msgstr "Service"
msgid "Service reference:"
msgstr "Kanalreferenz"
msgid "Event ID:"
msgstr "Ereignis-ID:"
msgid "Begins:"
msgstr "Beginnt:"
msgid "Ends:"
msgstr "Endet:"
msgid "Repeated:"
msgstr "Wiederhole:"
msgid "Action:"
msgstr "Aktion:"
msgid "After event:"
msgstr "Nach dem Ereignis:"
msgid "Location:"
msgstr "Zielverzeichnis:"
msgid "Mo"
msgstr "Mo"
msgid "Tu"
msgstr "Di"
msgid "We"
msgstr "Mi"
msgid "Th"
msgstr "Do"
msgid "Fr"
msgstr "Fr"
msgid "Sa"
msgstr "Sa"
msgid "Su"
msgstr "So"
msgid "Set"
msgstr "Einstellen"
msgid "Services update"
msgstr "Dienste-Update"
msgid "Create folder"
msgstr "Ordner erstellen"
msgid "FTP client"
msgstr "FTP-Client"
msgid "The file size is too large!"
msgstr "Die Datei ist zu groß!"
msgid "Connect"
msgstr "Verbinden"
msgid "Disconnect"
msgstr "Verbindung trennen"
msgid "Size"
msgstr "Größe"
msgid "Date"
msgstr "Datum"
msgid "Attr."
msgstr "Attr."

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 Frank Neirynck # Copyright (C) 2018-2020 Frank Neirynck
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# Frank Neirynck <frank@insink.be>, 2018-2019. # Frank Neirynck <frank@insink.be>, 2018-2020.
# #
msgid "" msgid ""
msgstr "" msgstr ""
@@ -54,7 +54,7 @@ msgid "Current IP:"
msgstr "IP actual:" msgstr "IP actual:"
msgid "Assign" msgid "Assign"
msgstr "Assignar" msgstr "Asignar"
msgid "Bouquet details" msgid "Bouquet details"
msgstr "Detalles bouquet" msgstr "Detalles bouquet"
@@ -66,7 +66,7 @@ msgid "Copy"
msgstr "Copiar" msgstr "Copiar"
msgid "Copy reference" msgid "Copy reference"
msgstr "Copia de referencia" msgstr "Copiar referencia"
msgid "Download" msgid "Download"
msgstr "Descargar" msgstr "Descargar"
@@ -123,7 +123,7 @@ msgid "New"
msgstr "Nuevo" msgstr "Nuevo"
msgid "New bouquet" msgid "New bouquet"
msgstr "Bouquet nuevo" msgstr "Nuevo bouquet"
msgid "Create bouquet" msgid "Create bouquet"
msgstr "Crear bouquet" msgstr "Crear bouquet"
@@ -156,10 +156,10 @@ msgid "Picons"
msgstr "Picons" msgstr "Picons"
msgid "Picons downloader" msgid "Picons downloader"
msgstr "Descargar picons" msgstr "Descarga de picons"
msgid "Satellites downloader" msgid "Satellites downloader"
msgstr "Descargar satélites" msgstr "Descarga de satélites"
msgid "Remove" msgid "Remove"
msgstr "Quitar" msgstr "Quitar"
@@ -206,11 +206,11 @@ msgstr "Ruta de datos actual:"
msgid "Data:" msgid "Data:"
msgstr "Datos:" msgstr "Datos:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Editor de canales y satélites Enigma2 para GNU/Linux" msgstr "Editor de canales y satélites Enigma2 para GNU/Linux."
msgid "Host:" msgid "Host:"
msgstr "Anfitrión:" msgstr "Host:"
msgid "Loading data..." msgid "Loading data..."
msgstr "Cargando datos..." msgstr "Cargando datos..."
@@ -256,7 +256,7 @@ msgstr "Extra:"
# Filter bar # Filter bar
msgid "Only free" msgid "Only free"
msgstr "Solamente gratis" msgstr "Solamente libres"
msgid "All positions" msgid "All positions"
msgstr "Todas las posiciones" msgstr "Todas las posiciones"
@@ -272,10 +272,10 @@ msgid "Stop playback"
msgstr "Detener la reproducción" msgstr "Detener la reproducción"
msgid "Previous stream in the list" msgid "Previous stream in the list"
msgstr "Anterior flujo en la lista" msgstr "Anterior stream en la lista"
msgid "Next stream in the list" msgid "Next stream in the list"
msgstr "Siguiente flujo en la lista" msgstr "Siguiente stream en la lista"
msgid "Toggle in fullscreen" msgid "Toggle in fullscreen"
msgstr "Cambiar a pantalla completa" msgstr "Cambiar a pantalla completa"
@@ -294,7 +294,7 @@ msgid "Receive picons"
msgstr "Recibir picons" msgstr "Recibir picons"
msgid "Picons name format:" msgid "Picons name format:"
msgstr "Picons formato nombres:" msgstr "Formato del nombre de los picons:"
msgid "Resize:" msgid "Resize:"
msgstr "Redimensionar:" msgstr "Redimensionar:"
@@ -433,7 +433,7 @@ msgstr "Buscar"
# IPTV dialog # IPTV dialog
msgid "Stream data" msgid "Stream data"
msgstr "Transmitir flujo" msgstr "Transmitir stream"
# IPTV list configuration dialog # IPTV list configuration dialog
msgid "Starting values" msgid "Starting values"
@@ -443,7 +443,7 @@ msgid "Reset to default"
msgstr "Restablecer a predeterminado" msgstr "Restablecer a predeterminado"
msgid "IPTV streams list configuration" msgid "IPTV streams list configuration"
msgstr "Configurar lista de flujos IPTV" msgstr "Configurar lista de streams IPTV"
# Settings dialog # Settings dialog
msgid "Preferences" msgid "Preferences"
@@ -511,13 +511,13 @@ msgid "No m3u file is selected!"
msgstr "¡No se ha seleccionado ningún fichero m3u!" msgstr "¡No se ha seleccionado ningún fichero m3u!"
msgid "Not implemented yet!" msgid "Not implemented yet!"
msgstr "¡Aún sin implementar!" msgstr "¡No implementado!"
msgid "The text of marker is empty, please try again!" msgid "The text of marker is empty, please try again!"
msgstr "¡El texto del marcador está vacío, inténtalo de nuevo!" msgstr "¡El texto del marcador está vacío, inténtalo de nuevo!"
msgid "Please, select only one item!" msgid "Please, select only one item!"
msgstr "¡Por favor, seleccione sólo un elemento!" msgstr "¡Por favor, seleccione un único elemento!"
msgid "No png file is selected!" msgid "No png file is selected!"
msgstr "¡No se ha seleccionado ningún fichero png!" msgstr "¡No se ha seleccionado ningún fichero png!"
@@ -526,7 +526,7 @@ msgid "No reference is present!"
msgstr "¡Ninguna referencia presente!" msgstr "¡Ninguna referencia presente!"
msgid "No selected item!" msgid "No selected item!"
msgstr "¡Ningún elemento seleccionado!" msgstr "¡No se ha seleccionado ningún elemento!"
msgid "The task is already running!" msgid "The task is already running!"
msgstr "¡La tarea ya se está ejecutando!" msgstr "¡La tarea ya se está ejecutando!"
@@ -562,23 +562,23 @@ msgid "Operation not allowed in this context!"
msgstr "¡Operación no permitida en este contexto!" msgstr "¡Operación no permitida en este contexto!"
msgid "No VLC is found. Check that it is installed!" msgid "No VLC is found. Check that it is installed!"
msgstr "VLC no encontrado. ¡Verifique que está instalado!" msgstr "VLC no encontrado. ¡Compruebe que está instalado!"
# Search unavailable streams dialog # Search unavailable streams dialog
msgid "Please wait, streams testing in progress..." msgid "Please wait, streams testing in progress..."
msgstr "Por favor espere, hay una prueba de flujo en progreso..." msgstr "Por favor espere, hay una prueba de stream en progreso..."
msgid "Found" msgid "Found"
msgstr "Encontrado" msgstr "Encontrado"
msgid "unavailable streams." msgid "unavailable streams."
msgstr "Flujos no presentes." msgstr "Streams no presentes."
msgid "No changes required!" msgid "No changes required!"
msgstr "¡Ningún cambio requerido!" msgstr "¡No se requieren cambios!"
msgid "This list does not contains IPTV streams!" msgid "This list does not contains IPTV streams!"
msgstr "¡La lista no contiene flujos IPTV!" msgstr "¡La lista no contiene streams IPTV!"
msgid "New empty configuration" msgid "New empty configuration"
msgstr "Nueva configuración vacía" msgstr "Nueva configuración vacía"
@@ -672,16 +672,16 @@ msgid "Zap"
msgstr "Zapear" msgstr "Zapear"
msgid "Play stream" msgid "Play stream"
msgstr "Reproducir flujo" msgstr "Reproducir stream"
msgid "Disabled" msgid "Disabled"
msgstr "Desactivado" msgstr "Desactivado"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Soporte para ver. 5 (experimental)" msgstr "Soporte para lamedb ver. 5"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Habilitar API HTTP (experimental)" msgstr "Habilitar API HTTP"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Poner el canal (Ctrl + Z)" msgstr "Poner el canal (Ctrl + Z)"
@@ -690,7 +690,7 @@ msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Poner el canal y ver en el programa (Ctrl + W)" msgstr "Poner el canal y ver en el programa (Ctrl + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)" msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Reproducir IPTV u otro flujo en el programa (Ctrl + P)" msgstr "Reproducir IPTV u otro stream en el programa (Ctrl + P)"
msgid "Export to m3u" msgid "Export to m3u"
msgstr "Exportar a m3u" msgstr "Exportar a m3u"
@@ -795,10 +795,10 @@ msgid "Language:"
msgstr "Idioma:" msgstr "Idioma:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Cargar la última configuración abierta al iniciar el programa" msgstr "Cargar la última configuración usada al iniciar el programa"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Habilitar la barra de reproducción directa (experimental)" msgstr "Habilitar la barra de reproducción directa"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor" msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor"
@@ -828,16 +828,16 @@ msgid "Reset"
msgstr "Reset" msgstr "Reset"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Fichero"
msgid "Picons manager" msgid "Picons manager"
msgstr "Picons manager" msgstr "Gestor de picons"
msgid "Explorer" msgid "Explorer"
msgstr "Explorador" msgstr "Explorador"
msgid "Satellite url:" msgid "Satellite url:"
msgstr "Url Satelite:" msgstr "Url satélite:"
msgid "Cut" msgid "Cut"
msgstr "Cortar" msgstr "Cortar"
@@ -867,43 +867,43 @@ msgid "IPTV tools"
msgstr "Intrumentos IPTV" msgstr "Intrumentos IPTV"
msgid "Make profile folder as default for the additional data" msgid "Make profile folder as default for the additional data"
msgstr "Has folder de perfil estandar para datos adicionales" msgstr "Usar por defecto el directorio de perfil para los datos adionales"
msgid "Default data path:" msgid "Default data path:"
msgstr "Ruta estandar de datos:" msgstr "Ruta estándar de datos:"
msgid "Streams record path:" msgid "Streams record path:"
msgstr "Ruta de gravacion de stream:" msgstr "Ruta de grabación del stream:"
msgid "Record" msgid "Record"
msgstr "Gravar" msgstr "Grabación"
msgid "Record:" msgid "Record:"
msgstr "Gravar:" msgstr "Grabación:"
msgid "Record to disk:" msgid "Record to disk:"
msgstr "Gravar en disco:" msgstr "Grabación en disco:"
msgid "Streaming" msgid "Streaming"
msgstr "Streameando" msgstr "Streaming"
msgid "Activate transcoding" msgid "Activate transcoding"
msgstr "Activer transcodificacion" msgstr "Activar transcodificación"
msgid "Presets:" msgid "Presets:"
msgstr "Presets:" msgstr "Preajustes:"
msgid "Video options:" msgid "Video options:"
msgstr "Opciones Video:" msgstr "Opciones deo:"
msgid "Audio options:" msgid "Audio options:"
msgstr "Opciones Audio:" msgstr "Opciones audio:"
msgid "Bitrate (kb/s):" msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):" msgstr "Bitrate (kb/s):"
msgid "Codec:" msgid "Codec:"
msgstr "Codec:" msgstr "Códec:"
msgid "Width (px):" msgid "Width (px):"
msgstr "Ancho (px):" msgstr "Ancho (px):"
@@ -915,10 +915,10 @@ msgid "Channels:"
msgstr "Canales:" msgstr "Canales:"
msgid "Sample rate (Hz):" msgid "Sample rate (Hz):"
msgstr "Sample rate (Гц):" msgstr "Frecuencia de muestreo (Hz):"
msgid "Play streams mode:" msgid "Play streams mode:"
msgstr "Tocar en modo streams:" msgstr "Modo de reproducción de los streams:"
msgid "Built-in player" msgid "Built-in player"
msgstr "Reproductor interno" msgstr "Reproductor interno"
@@ -927,40 +927,40 @@ msgid "VLC media player"
msgstr "Reproductor VLC" msgstr "Reproductor VLC"
msgid "Only get m3u file" msgid "Only get m3u file"
msgstr "Solo bajar archivo *.m3u" msgstr "Sólo bajar fichero m3u"
msgid "Save and restart the program to apply the settings." msgid "Save and restart the program to apply the settings."
msgstr "Guarde y reinicie el programa para aplicar la configuración." msgstr "Guardar y reiniciar el programa para aplicar la configuración."
msgid "Some images may have problems displaying the favorites list!" msgid "Some images may have problems displaying the favorites list!"
msgstr "Algunas imágenes pueden tener problemas para mostrar la lista de favoritos!" msgstr "Algunas imágenes pueden tener problemas al mostrar la lista de favoritos!"
msgid "Operates in standby mode or current active transponder!" msgid "Operates in standby mode or current active transponder!"
msgstr "Funciona en modo de espera o transpondedor activo actual!" msgstr "¡Funciona en modo de espera o transpondedor activo actual!"
msgid "No connection to the receiver!" msgid "No connection to the receiver!"
msgstr "Sin conexión al receptor!" msgstr "¡Desconectado del receptor!"
msgid "Signal level" msgid "Signal level"
msgstr "Nivel de señal" msgstr "Nivel de señal"
msgid "Receiver info" msgid "Receiver info"
msgstr "Informacion sobre receptor" msgstr "Información sobre el receptor"
msgid "A profile with that name exists!" msgid "A profile with that name exists!"
msgstr "Existe un perfil con ese nombre!" msgstr "¡Ya existe un perfil con ese nombre!"
msgid "Show short info as hints in the main services list" msgid "Show short info as hints in the main services list"
msgstr "Mostrar información breve como sugerencias en la lista de servicios principal" msgstr "Mostrar información breve como sugerencias en la lista de servicios principal"
msgid "Show detailed info as hints in the bouquet list" msgid "Show detailed info as hints in the bouquet list"
msgstr "Mostrar información detallada como pistas en la lista de bouquet" msgstr "Mostrar información detallada como consejo en la lista de bouquets"
msgid "Enable alternate bouquet file naming" msgid "Enable alternate bouquet file naming"
msgstr "Habilitar nombres alternativos de archivos de bouquet" msgstr "Habilitar nombres alternativos para los ficheros de bouquets"
msgid "Allows you to name bouquet files using their names." msgid "Allows you to name bouquet files using their names."
msgstr "Le permite nombrar archivos de bouquet usando sus nombres." msgstr "Permite nombrar ficheros de bouquets usando sus propios nombres."
msgid "Appearance" msgid "Appearance"
msgstr "Apariencia" msgstr "Apariencia"
@@ -969,16 +969,16 @@ msgid "Enable Themes support"
msgstr "Habilitar compatibilidad con temas" msgstr "Habilitar compatibilidad con temas"
msgid "Gtk3 Theme:" msgid "Gtk3 Theme:"
msgstr "Тема Gtk3:" msgstr "Теma Gtk3:"
msgid "Icon Theme:" msgid "Icon Theme:"
msgstr "Тема Icono:" msgstr "Icono del tema:"
msgid "Gtk3 Themes and Icons:" msgid "Gtk3 Themes and Icons:"
msgstr "Tema Gtk3 e Iconos:" msgstr "Tema Gtk3 e iconos:"
msgid "Deleting data..." msgid "Deleting data..."
msgstr "Borrando datos ..." msgstr "Borrando datos..."
msgid "Download from the receiver" msgid "Download from the receiver"
msgstr "Descargar desde el receptor" msgstr "Descargar desde el receptor"
@@ -987,13 +987,13 @@ msgid "Remove all picons from the receiver"
msgstr "Eliminar todos los picons del receptor" msgstr "Eliminar todos los picons del receptor"
msgid "Service reference" msgid "Service reference"
msgstr "Referencia de servicio" msgstr "Referencia del servicio"
msgid "Enable support for" msgid "Enable support for"
msgstr "Habilitar soporte para" msgstr "Habilitar soporte para"
msgid "Auto-check for updates" msgid "Auto-check for updates"
msgstr "Verificación automática de actualizaciones" msgstr "Comprobación automática de actualizaciones"
msgid "Filter services" msgid "Filter services"
msgstr "Filtrar servicios" msgstr "Filtrar servicios"
@@ -1005,23 +1005,23 @@ msgid "Destination:"
msgstr "Destino:" msgstr "Destino:"
msgid "EXPERIMENTAL!" msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTAL!" msgstr "¡EXPERIMENTAL!"
msgid "Sorting data..." msgid "Sorting data..."
msgstr "Ordenando datos..." msgstr "Ordenando datos..."
msgid "There are unsaved changes.\n\n\t Save them now?" msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Hay cambios sin guardar.\n\n\t ¿Guardarlos ahora?" msgstr "Hay cambios sin guardar.\n\n\t ¿Desea guardarlos ahora?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?" msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "¿Está seguro de que desea cambiar el orden\n\t de servicios en este bouquet?" msgstr "¿Está seguro de querer cambiar el orden\n\t de servicios en este bouquet?"
msgid "Remove from the receiver" msgid "Remove from the receiver"
msgstr "Retirar del receptor" msgstr "Eliminar del receptor"
msgid "Screenshot" msgid "Screenshot"
msgstr "Captura de pantalla" msgstr "Captura de pantalla"
msgid "Video" msgid "Video"
msgstr "Vidео" msgstr "Vídео"

View File

@@ -201,8 +201,8 @@ msgstr "Huidig datapad:"
msgid "Data:" msgid "Data:"
msgstr "Data:" msgstr "Data:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Enigma2 kanaal and satelliet lijst editor voor GNU/Linux" msgstr "Enigma2 kanaal and satelliet lijst editor voor GNU/Linux."
msgid "Host:" msgid "Host:"
msgstr "Host:" msgstr "Host:"
@@ -671,11 +671,11 @@ msgstr "Speel stream af"
msgid "Disabled" msgid "Disabled"
msgstr "Uitgeschakeld" msgstr "Uitgeschakeld"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Ondersteuning voor ver. 5 inschakelen (experimenteel)" msgstr "Ondersteuning voor lamedb ver. 5 inschakelen"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "HTTP API inschakelen (experimenteel)" msgstr "HTTP API inschakelen"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)" msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)"
@@ -791,8 +791,8 @@ msgstr "Taal:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Laad de laatst geopende configuratie op bij opstart programma" msgstr "Laad de laatst geopende configuratie op bij opstart programma"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Laat onmiddelijk playback bar toe (experimenteel)" msgstr "Laat onmiddelijk playback bar toe"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Laat rechtstreeks versturen van and playback en media links op de ontvanger toe" msgstr "Laat rechtstreeks versturen van and playback en media links op de ontvanger toe"

View File

@@ -1,6 +1,7 @@
# Copyright (C) 2018-2019 Dmitriy Yefremov # Copyright (C) 2018-2020 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
#
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: wwns\n" "Last-Translator: wwns\n"
@@ -205,8 +206,8 @@ msgstr "Aktualna ścieżka danych:"
msgid "Data:" msgid "Data:"
msgstr "Dane:" msgstr "Dane:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Edytor kanałów Enigma2 i listy satelitów dla GNU/Linux" msgstr "Edytor kanałów Enigma2 i listy satelitów dla GNU/Linux."
msgid "Host:" msgid "Host:"
msgstr "Host:" msgstr "Host:"
@@ -265,6 +266,9 @@ msgstr "Ukryj/Pomiń"
msgid "Parent lock" msgid "Parent lock"
msgstr "Blokada rodzicielska" msgstr "Blokada rodzicielska"
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Istnieją niezapisane zmiany.\n\n\t Zapisać je teraz?"
# Filter bar # Filter bar
msgid "Only free" msgid "Only free"
msgstr "Tylko FTA" msgstr "Tylko FTA"
@@ -386,6 +390,12 @@ msgid "Remove selection"
msgstr "Usuń wybrane" msgstr "Usuń wybrane"
# Service details dialog # Service details dialog
msgid "To the top"
msgstr "Przenieś na góre bukietu"
msgid "To the end"
msgstr "Przenieś na koniec bukietu"
msgid "Service data:" msgid "Service data:"
msgstr "Dane usług:" msgstr "Dane usług:"
@@ -596,9 +606,6 @@ msgstr "Brak danych do zapisania!"
msgid "Network" msgid "Network"
msgstr "Sieć" msgstr "Sieć"
msgid "Program"
msgstr "Program"
msgid "Backup" msgid "Backup"
msgstr "Kopia" msgstr "Kopia"
@@ -626,6 +633,15 @@ msgstr "Wyjście"
msgid "Tools" msgid "Tools"
msgstr "Narzędzia" msgstr "Narzędzia"
msgid "Cut"
msgstr "Wytnij"
msgid "Paste"
msgstr "Wklej"
msgid "Insert space"
msgstr "Wstaw spację"
# Import # Import
msgid "Import" msgid "Import"
msgstr "Importuj" msgstr "Importuj"
@@ -648,12 +664,12 @@ msgstr "Usuń wszystkie nieużywane"
msgid "Test" msgid "Test"
msgstr "Test" msgstr "Test"
msgid "Details"
msgstr "Właściwości"
msgid "Test connection" msgid "Test connection"
msgstr "Testuj połączenie" msgstr "Testuj połączenie"
msgid "Double click on the service in the bouquet list:"
msgstr "Kliknij dwukrotnie usługę na liście bukietów:"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Przełącz(zap) kanał(Ctrl + Z)" msgstr "Przełącz(zap) kanał(Ctrl + Z)"
@@ -667,7 +683,7 @@ msgid "Export to m3u"
msgstr "Eksportuj do m3u" msgstr "Eksportuj do m3u"
msgid "EPG configuration" msgid "EPG configuration"
msgstr "Koniguruj EPG" msgstr "Konfiguruj EPG"
msgid "Apply" msgid "Apply"
msgstr "Zatwierdź" msgstr "Zatwierdź"
@@ -754,17 +770,11 @@ msgstr "Import listy odtwarzania"
msgid "Getting link error:" msgid "Getting link error:"
msgstr "Błąd pobierania łącza:" msgstr "Błąd pobierania łącza:"
msgid "Apply profile settings"
msgstr "Zastosuj ustawienia profilu"
msgid "Settings type:" msgid "Settings type:"
msgstr "Ustawienia dla:" msgstr "Ustawienia dla:"
msgid "Set default" msgid "Set default"
msgstr "Uataw domyślnie" msgstr "Ustaw domyślnie"
msgid "Enable direct playback bar (experimental)"
msgstr "Włącz pasek bezpośredniego odtwarzania (eksperymentalnie)"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Umożliwia bezpośrednie wysyłanie i odtwarzanie łączy multimedialnych w odbiorniku" msgstr "Umożliwia bezpośrednie wysyłanie i odtwarzanie łączy multimedialnych w odbiorniku"
@@ -785,7 +795,10 @@ msgid "Remove picons from the receiver"
msgstr "Usuń pikony z odbiornika" msgstr "Usuń pikony z odbiornika"
msgid "Use http to reload data in the receiver." msgid "Use http to reload data in the receiver."
msgstr "Użyj http aby ponownie załadować dane do odbiornika." msgstr "Użyj http aby przeładować dane w odbiorniku."
msgid "Apply profile settings"
msgstr "Zastosuj ustawienia profilu"
msgid "Drag or paste the link here" msgid "Drag or paste the link here"
msgstr "Przeciągnij lub wklej tutaj link" msgstr "Przeciągnij lub wklej tutaj link"
@@ -808,10 +821,16 @@ msgstr "Pobierz z odbiornika"
msgid "The Neutrino has only experimental support. Not all features are supported!" msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino ma jedynie wsparcie eksperymentalne. Nie wszystkie funkcje są obsługiwane!" msgstr "Neutrino ma jedynie wsparcie eksperymentalne. Nie wszystkie funkcje są obsługiwane!"
msgid "Some images may have problems displaying the favorites list!"
msgstr "Niektóre obrazy mogą mieć problemy z wyświetlaniem listy ulubionych!"
# Appearance # Appearance
msgid "Appearance" msgid "Appearance"
msgstr "Wygląd" msgstr "Wygląd"
msgid "Enable Dark Mode"
msgstr "Włącz tryb ciemny"
msgid "Enable Themes support" msgid "Enable Themes support"
msgstr "Włącz obsługę motywów" msgstr "Włącz obsługę motywów"
@@ -834,23 +853,17 @@ msgstr "Zapisz i uruchom ponownie program, aby zastosować ustawienia."
msgid "Extra" msgid "Extra"
msgstr "Ekstra" msgstr "Ekstra"
msgid "Enable ver. 5 support (experimental)"
msgstr "Włącz wer. 5 wsparcie (eksperymentalne)"
msgid "Enable alternate bouquet file naming" msgid "Enable alternate bouquet file naming"
msgstr "Włącz alternatywne nazewnictwo plików bukietów" msgstr "Włącz alternatywne nazewnictwo plików bukietów"
msgid "Some images may have problems displaying the favorites list!"
msgstr "Niektóre obrazy mogą mieć problemy z wyświetlaniem listy ulubionych!"
msgid "Allows you to name bouquet files using their names." msgid "Allows you to name bouquet files using their names."
msgstr "Pozwala nazwać pliki bukietów przy użyciu ich nazw." msgstr "Pozwala nazwać pliki bukietów przy użyciu ich nazw."
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Włącz API HTTP (eksperymentalne)" msgstr "Włącz API HTTP"
msgid "Enable send to receiver (experimental)" msgid "Double click on the service in the bouquet list:"
msgstr "Włącz wysyłanie do odbiornika (eksperymentalne)" msgstr "Kliknij dwukrotnie usługę na liście bukietów:"
msgid "Zap" msgid "Zap"
msgstr "Przełącz" msgstr "Przełącz"
@@ -867,6 +880,28 @@ msgstr "Odtwórz strumień"
msgid "Disabled" msgid "Disabled"
msgstr "Wyłączone" msgstr "Wyłączone"
msgid "Enable experimental features"
msgstr "Włącz funkcje eksperymentalne"
msgid "Enable lamedb ver. 5 support"
msgstr "Włącz wsparcie dla lamedb w wer.5"
msgid "Enable support for"
msgstr "Włącz obsługę"
msgid "Enables parsing links using youtube-dl to get direct links to media"
msgstr "Umożliwia analizowanie linków za pomocą youtube-dl, aby uzyskać bezpośrednie linki do multimediów"
msgid "Auto-check for updates"
msgstr "Automatyczne sprawdzanie aktualizacji"
msgid "Enable direct playback bar"
msgstr "Włącz pasek bezpośredniego odtwarzania"
#Program
msgid "Program"
msgstr "Program"
msgid "Language:" msgid "Language:"
msgstr "Język:" msgstr "Język:"
@@ -876,7 +911,7 @@ msgstr "Załaduj ostatnią otwartą konfigurację podczas uruchamiania programu"
msgid "Show short info as hints in the main services list" msgid "Show short info as hints in the main services list"
msgstr "Pokaż krótkie informacje jako wskazówki na głównej liście usług" msgstr "Pokaż krótkie informacje jako wskazówki na głównej liście usług"
msgid "Show detailed info as hints in the bouquet list" msgid "Show detailed info as hints in the bouquet list"
msgstr "Pokaż szczegółowe informacje jako wskazówki na liście bukietów" msgstr "Pokaż szczegółowe informacje jako wskazówki na liście bukietów"
msgid "Set background color for the services" msgid "Set background color for the services"
@@ -962,8 +997,8 @@ msgstr "Ustawia folder profilu jako domyślny do przechowywania pikonów, kopii
msgid "Default data path:" msgid "Default data path:"
msgstr "Domyślna ścieżka danych:" msgstr "Domyślna ścieżka danych:"
msgid "Record" msgid "Record:"
msgstr "Nagrania" msgstr "Nagrania:"
msgid "Streams record path:" msgid "Streams record path:"
msgstr "Ścieżka zapisu nagrań:" msgstr "Ścieżka zapisu nagrań:"

View File

@@ -201,8 +201,8 @@ msgstr "Rota de dados atual:"
msgid "Data:" msgid "Data:"
msgstr "Dados:" msgstr "Dados:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Editor de Canais e Satélites Enigma2 para GNU/Linux" msgstr "Editor de Canais e Satélites Enigma2 para GNU/Linux."
msgid "Host:" msgid "Host:"
msgstr "Anfitrião:" msgstr "Anfitrião:"
@@ -663,11 +663,11 @@ msgstr "Play stream"
msgid "Disabled" msgid "Disabled"
msgstr "Desativado" msgstr "Desativado"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Ativar ver. 5 suporte (experimental)" msgstr "Ativar lamedb ver. 5 suporte"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Ativar HTTP API (experimental)" msgstr "Ativar HTTP API"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Mudar(zap) o canal(Ctrl + Z)" msgstr "Mudar(zap) o canal(Ctrl + Z)"
@@ -783,8 +783,8 @@ msgstr "Idioma:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Cargar la última configuración abierta al iniciar el programa" msgstr "Cargar la última configuración abierta al iniciar el programa"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Habilitar la barra de reproducción directa (experimental)" msgstr "Habilitar la barra de reproducción directa"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor" msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor"

View File

@@ -104,6 +104,9 @@ msgstr "Имя по умолчанию"
msgid "Insert marker" msgid "Insert marker"
msgstr "Вставить маркер" msgstr "Вставить маркер"
msgid "Insert space"
msgstr "Вставить пробел"
msgid "Locate in services" msgid "Locate in services"
msgstr "Найти в списке сервисов" msgstr "Найти в списке сервисов"
@@ -200,8 +203,8 @@ msgstr "Текущий путь к данным:"
msgid "Data:" msgid "Data:"
msgstr "Данные:" msgstr "Данные:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux" msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux."
msgid "Host:" msgid "Host:"
msgstr "Адрес ресивера:" msgstr "Адрес ресивера:"
@@ -507,6 +510,9 @@ msgstr "Пожалуйста, выберите только один элеме
msgid "No png file is selected!" msgid "No png file is selected!"
msgstr "Не выбран png файл!" msgstr "Не выбран png файл!"
msgid "No profile selected!"
msgstr "Не выбран профиль!"
msgid "No reference is present!" msgid "No reference is present!"
msgstr "Ссылка не найдена!" msgstr "Ссылка не найдена!"
@@ -662,17 +668,17 @@ msgstr "Воспр. потока"
msgid "Disabled" msgid "Disabled"
msgstr "Выкл." msgstr "Выкл."
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Включить поддержку lamedb вер. 5 (экспериментально)" msgstr "Включить поддержку lamedb вер. 5"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Включить HTTP API (экспериментально)" msgstr "Включить HTTP API"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Переключить канал(Ctrl + Z)" msgstr "Переключить канал(Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)" msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Переклють канал и просмотр в программе(Ctrl + W)." msgstr "Переключить канал и просмотр в программе(Ctrl + W)."
msgid "Play IPTV or other stream in the program(Ctrl + P)" msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Воспроизведение IPTV или другого потока в программе(Ctrl + P)" msgstr "Воспроизведение IPTV или другого потока в программе(Ctrl + P)"
@@ -782,8 +788,8 @@ msgstr "Язык:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Загружать последнюю открытую конфигурацию при запуске программы" msgstr "Загружать последнюю открытую конфигурацию при запуске программы"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Включить панель прямого воспроизведения (экспериментально)" msgstr "Включить панель прямого воспроизведения"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Включает прямую отправку и воспроизведение медиа-ссылок на ресивере" msgstr "Включает прямую отправку и воспроизведение медиа-ссылок на ресивере"
@@ -1010,3 +1016,173 @@ msgstr "Скриншот"
msgid "Video" msgid "Video"
msgstr "Видео" msgstr "Видео"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino имеет только экспериментальную поддержку. Поддерживаются не все функции!"
msgid "Enable experimental features"
msgstr "Включить экспериментальные функции"
msgid "Can't Playback!"
msgstr "Не удается воспроизвести!"
msgid "Enable Dark Mode"
msgstr "Включить темный режим"
msgid "Extract..."
msgstr "Извлечь..."
msgid "Unsupported format!"
msgstr "Неподдерживаемый формат!"
msgid "Combine with the current data?"
msgstr "Объединить с текущими данными?"
msgid "Importing data done!"
msgstr "Импорт данных завершен!"
msgid "Current service"
msgstr "Текущий сервис"
msgid "Open folder"
msgstr "Открыть папку"
msgid "Open archive"
msgstr "Открыть архив"
msgid "Import from Web"
msgstr "Импорт из сети"
msgid "Control"
msgstr "Управление"
msgid "Timers"
msgstr "Таймеры"
msgid "Timer"
msgstr "Таймер"
msgid "Add timer"
msgstr "Добавить таймер"
msgid "Hr."
msgstr "ч."
msgid "Min."
msgstr "мин."
msgid "Power"
msgstr "Питание"
msgid "Standby"
msgstr "Режим ожидания"
msgid "Wake Up"
msgstr "Пробуждение"
msgid "Reboot"
msgstr "Перезагрузка"
msgid "Restart GUI"
msgstr "Перезагрузить графический интерфейс"
msgid "Shutdown"
msgstr "Выключение"
msgid "Shut down"
msgstr "Выключить"
msgid "Do Nothing"
msgstr "Ничего не делать"
msgid "Auto"
msgstr "Авто"
msgid "Grab screenshot"
msgstr "Сделать скриншот"
msgid "Enabled:"
msgstr "Включен:"
msgid "Name:"
msgstr "Имя:"
msgid "Description:"
msgstr "Описание:"
msgid "Service:"
msgstr "Сервис:"
msgid "Service reference:"
msgstr "Сервисная ссылка:"
msgid "Event ID:"
msgstr "ID события:"
msgid "Begins:"
msgstr "Начало:"
msgid "Ends:"
msgstr "Окончание:"
msgid "Repeated:"
msgstr "Повтор:"
msgid "Action:"
msgstr "Действие:"
msgid "After event:"
msgstr "После события:"
msgid "Location:"
msgstr "Расположение:"
msgid "Mo"
msgstr "Пн"
msgid "Tu"
msgstr "Вт"
msgid "We"
msgstr "Ср"
msgid "Th"
msgstr "Чт"
msgid "Fr"
msgstr "Пт"
msgid "Sa"
msgstr "Сб"
msgid "Su"
msgstr "Вс"
msgid "Set"
msgstr "Установить"
msgid "Services update"
msgstr "Обновление сервисов"
msgid "Create folder"
msgstr "Создать папку"
msgid "FTP client"
msgstr "FTP-клиент"
msgid "The file size is too large!"
msgstr "Размер файла слишком велик!"
msgid "Connect"
msgstr "Соединение"
msgid "Disconnect"
msgstr "Разъединить"
msgid "Size"
msgstr "Размер"
msgid "Date"
msgstr "Дата"
msgid "Attr."
msgstr "Атриб."

View File

@@ -203,8 +203,8 @@ msgstr "Mevcut veri yolu:"
msgid "Data:" msgid "Data:"
msgstr "Veri:" msgstr "Veri:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux" msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü" msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü."
msgid "Host:" msgid "Host:"
msgstr "Ana bilgisayar:" msgstr "Ana bilgisayar:"
@@ -673,11 +673,11 @@ msgstr "Akışı oynat"
msgid "Disabled" msgid "Disabled"
msgstr "Devre dışı" msgstr "Devre dışı"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Sürüm 5 desteğini etkinleştir (deneysel)" msgstr "Sürüm 5 desteğini etkinleştir"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "HTTP API'sini etkinleştir (deneysel)" msgstr "HTTP API'sini etkinleştir"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Kanalı değiştir (zap) (Ctrl + Z)" msgstr "Kanalı değiştir (zap) (Ctrl + Z)"
@@ -797,8 +797,8 @@ msgstr "Dil:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Program açılışında son açık yapılandırmayı yükle" msgstr "Program açılışında son açık yapılandırmayı yükle"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Doğrudan oynatma çubuğunu etkinleştir (deneysel)" msgstr "Doğrudan oynatma çubuğunu etkinleştir"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Alıcıdaki medya bağlantılarının doğrudan gönderilmesini ve oynatılmasını sağlar" msgstr "Alıcıdaki medya bağlantılarının doğrudan gönderilmesini ve oynatılmasını sağlar"