Compare commits

..

79 Commits

Author SHA1 Message Date
DYefremov
d95ec0af17 Russian, Belarusian and German translations update 2021-01-12 11:53:00 +03:00
DYefremov
77c159fc1a added separator to profile toolbar 2021-01-12 11:22:57 +03:00
DYefremov
95508c3015 changed alternatives naming 2021-01-11 17:15:39 +03:00
DYefremov
1f6e6b9812 added 8739 stream type 2021-01-11 12:30:44 +03:00
DYefremov
20697a08f2 naming alternatives fix 2021-01-10 22:42:16 +03:00
DYefremov
95c7213988 added moving alternatives in the list 2021-01-10 14:05:01 +03:00
DYefremov
a988ea6e68 added editing of alternatives 2021-01-08 23:01:16 +03:00
DYefremov
061e42a36e set extra tools visible by default 2021-01-08 13:16:14 +03:00
DYefremov
3a5473ca03 removed bq position option 2021-01-07 10:34:10 +03:00
DYefremov
54a5be9c3d added separate column for picon to fav list 2021-01-06 22:46:12 +03:00
DYefremov
858c64d531 added basic support for alternatives (#42) 2021-01-05 23:07:15 +03:00
DYefremov
f05802841d some streams detection fix 2021-01-02 23:17:22 +03:00
DYefremov
e4af45b0e0 redesigned network settings 2020-12-29 22:41:28 +03:00
DYefremov
0cad69cb00 bump version 2020-12-29 22:41:12 +03:00
DYefremov
071fffb303 _config.yml update 2020-12-27 23:19:29 +03:00
DYefremov
95d51d37e9 README update 2020-12-26 19:55:18 +03:00
DYefremov
f1935e4a56 added display option for bouquet details list 2020-12-24 23:30:29 +03:00
DYefremov
96aeb05dd1 minor compatibility [xenial] fix 2020-12-23 19:39:57 +03:00
DYefremov
af1c3c3ca6 README update 2020-12-23 09:10:03 +03:00
DYefremov
2ba37f1506 Russian, Belarusian and German translations update 2020-12-23 09:09:42 +03:00
DYefremov
8581de5a10 yt fix 2020-12-23 09:09:08 +03:00
DYefremov
155f3b254a ftp client improvements 2020-12-22 14:18:16 +03:00
DYefremov
13e6c858d2 storing app window size on close 2020-12-19 12:36:42 +03:00
DYefremov
7fac48c44d added alternate layout option 2020-12-18 22:13:15 +03:00
DYefremov
9aa2ec191e added ftp client to main window 2020-12-16 23:28:00 +03:00
DYefremov
13e12165eb added ftp client base class 2020-12-16 23:20:51 +03:00
DYefremov
ae37feebaa added keyboard key 2020-12-16 00:32:32 +03:00
DYefremov
573bcba32f ftp refactoring [func extension] 2020-12-13 15:19:38 +03:00
DYefremov
f4e5c56d39 allowed to add dir during path config 2020-12-13 15:19:01 +03:00
DYefremov
4dc0dffc1f README update 2020-12-10 21:28:09 +03:00
DYefremov
ceb329700d added encoding detection for *.m3u import 2020-12-07 09:30:03 +03:00
DYefremov
8cca2da48d data load rework (#37 fix [can't decode byte]) 2020-12-05 14:16:07 +03:00
DYefremov
e9bab4ebb7 bump version 2020-12-04 15:06:12 +03:00
DYefremov
3e7750e4ef added playlist extraction via youtube-dl 2020-12-04 15:03:21 +03:00
DYefremov
184c1cbee5 upd. README 2020-12-02 01:02:25 +03:00
DYefremov
c9e8625ea5 added types to the service parser 2020-11-28 14:19:41 +03:00
DYefremov
fd72615a63 minor changes in control panel gui 2020-11-28 08:24:51 +03:00
DYefremov
f11a40e04d added sorting reset when loading data 2020-11-25 16:27:48 +03:00
DYefremov
9522267dee translations correction for app description 2020-11-25 15:14:24 +03:00
DYefremov
f538d609b9 added timeouts for telnet login 2020-11-25 13:27:53 +03:00
DYefremov
3edc699277 config.yml update 2020-11-25 11:30:16 +03:00
DYefremov
19562c7281 description update 2020-11-25 11:25:50 +03:00
DYefremov
5a104c1778 Russian, Belarusian and German translations update 2020-11-23 13:51:43 +03:00
DYefremov
09afc5b76a fix update of sat positions after web import 2020-11-23 12:58:40 +03:00
DYefremov
6129c5ac23 some changes in the control panel 2020-11-22 15:40:19 +03:00
DYefremov
a552d13933 style correction 2020-11-21 15:09:32 +03:00
DYefremov
c4fa3b1eee Russian, Belarusian and German translations update 2020-11-19 20:27:09 +03:00
DYefremov
795c708a61 added basic support for timers via http api 2020-11-19 18:05:40 +03:00
DYefremov
92721c65e4 added styles 2020-11-19 15:01:07 +03:00
DYefremov
b8470f7dfa added transponder view popup menu 2020-11-16 13:48:05 +03:00
DYefremov
fd02ddedb7 small http api refactoring 2020-11-11 15:34:12 +03:00
DYefremov
f3beec141c added epg display in control panel 2020-11-07 18:38:40 +03:00
DYefremov
8910b6d68b control decoupling 2020-11-03 20:36:21 +03:00
DYefremov
2a4c4576cc minor compatibility fix 2020-11-03 12:17:52 +03:00
DYefremov
4e8fd693a6 added web import for services (#16) 2020-11-02 21:55:34 +03:00
DYefremov
7184642e68 small decoupling for lamedb parsing 2020-11-02 14:33:55 +03:00
DYefremov
10f4a461bd added prototype of simple control panel (#38) 2020-10-25 12:54:08 +03:00
DYefremov
bec7f35b17 added remote control requests 2020-10-24 15:46:59 +03:00
DYefremov
2a6dc3f167 bump version 2020-10-22 15:44:37 +03:00
DYefremov
e9e36c4a9c added comments 2020-10-22 15:43:33 +03:00
DYefremov
0c49096733 upd README 2020-10-13 13:34:47 +03:00
DYefremov
e2aa21060b minor fix for the picon parser 2020-10-10 15:19:00 +03:00
DYefremov
b15691207b minor fix for yt 2020-10-06 11:25:26 +03:00
DYefremov
ab3ca134a7 Russian, Belarusian and German translations update 2020-10-04 14:53:42 +03:00
DYefremov
7f839f3fa0 data loading refactoring (prevent #37) 2020-10-02 13:42:43 +03:00
DYefremov
c3e8eac4b9 changed getting of the drag icon 2020-09-30 20:54:03 +03:00
DYefremov
04f808843d upd README 2020-09-25 18:02:32 +03:00
DYefremov
eea4a68993 Russian, Belarusian and German translations update 2020-09-25 18:01:36 +03:00
DYefremov
bfba5b5237 reworked and improved dnd for lists 2020-09-24 23:17:15 +03:00
DYefremov
e203a38966 added support for loading and importing data via dnd 2020-09-19 12:32:08 +03:00
DYefremov
a3b5609138 version update 2020-09-17 17:18:58 +03:00
DYefremov
b3c131b753 added support for opening archives 2020-09-17 17:16:00 +03:00
DYefremov
a8ea5ad974 minor rework of the chooser dialog 2020-09-17 10:00:02 +03:00
DYefremov
831184af2e displaying sid value in uppercase for tooltips(#34) 2020-09-12 14:11:42 +03:00
DYefremov
1f51766dea Display the sid value in tooltips in hex and dec format(#34). 2020-09-12 11:33:17 +03:00
DYefremov
f8f1536213 copy es *.mo file 2020-09-11 16:36:03 +03:00
Víctor Pont
0accfbd3d1 Spanish translation update (#36) 2020-09-11 16:24:59 +03:00
DYefremov
ee462b24f7 renaming bouquet fix [losing custom names](#33) 2020-09-08 12:21:10 +03:00
DYefremov
17ee189db8 upd README 2020-09-07 20:05:24 +03:00
65 changed files with 9163 additions and 3164 deletions

View File

@@ -1,8 +1,10 @@
[Desktop Entry]
Version=1.0
Name=DemonEditor
Comment=Channels and satellites list editor for Enigma2
Comment=Channel and satellite list editor for Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
Icon=demon-editor
Exec=bash -c 'cd $(dirname %k) && ./start.py'
Terminal=false

110
README.md
View File

@@ -1,27 +1,40 @@
# <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)
## Enigma2 channel and satellites list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![platform](https://img.shields.io/badge/platform-linux%20|%20macos-lightgrey)
### Enigma2 channel and satellite list editor for GNU/Linux.
[<img src="https://user-images.githubusercontent.com/7511379/103152885-4a7bd880-479d-11eb-96e8-4ad0f5dc3e2e.png" width="560"/>](https://user-images.githubusercontent.com/7511379/103152885-4a7bd880-479d-11eb-96e8-4ad0f5dc3e2e.png)
### Main features of the program:
* Editing bouquets, channels, satellites.
* Import function.
* Backup function.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
## Main features of the program
* Editing bouquets, channels, satellites.
[<img src="https://user-images.githubusercontent.com/7511379/100156250-a3fc9900-2eb9-11eb-8729-7bcb6ddcdd4a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/100156250-a3fc9900-2eb9-11eb-8729-7bcb6ddcdd4a.png)
* Import function.
[<img src="https://user-images.githubusercontent.com/7511379/103150878-8a38c500-4789-11eb-9e03-0c8ee832ff99.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150878-8a38c500-4789-11eb-9e03-0c8ee832ff99.png)
* Backup function.
[<img src="https://user-images.githubusercontent.com/7511379/103150886-a0df1c00-4789-11eb-88fa-91996b72d1f9.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150886-a0df1c00-4789-11eb-88fa-91996b72d1f9.png)
* Support of picons.
[<img src="https://user-images.githubusercontent.com/7511379/103150891-accade00-4789-11eb-8804-e1807df89c99.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150891-accade00-4789-11eb-8804-e1807df89c99.png)
* Importing services, downloading picons and updating satellites from the Web.
[<img src="https://user-images.githubusercontent.com/7511379/97909451-4c0ac080-1d59-11eb-9626-85bed91f7ccc.png" width="250"/>](https://user-images.githubusercontent.com/7511379/97909451-4c0ac080-1d59-11eb-9626-85bed91f7ccc.png)
[<img src="https://user-images.githubusercontent.com/7511379/103150872-77be8b80-4789-11eb-8a74-7a49fb3edd98.png" width="292"/>](https://user-images.githubusercontent.com/7511379/103150872-77be8b80-4789-11eb-8a74-7a49fb3edd98.png)
* Extended support of IPTV.
* Support of picons.
* Downloading of picons and updating of satellites (transponders) from the web.
* Import to bouquet(Neutrino WEBTV) from m3u.
* Export of bouquets with IPTV services in m3u.
* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
* Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)).
### Keyboard shortcuts:
* Assignment of EPG from DVB or XML for IPTV services (only Enigma2, experimental).
* Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)).
[<img src="https://user-images.githubusercontent.com/7511379/103151911-89a52c00-4793-11eb-9941-8430f4e87eef.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103151911-89a52c00-4793-11eb-9941-8430f4e87eef.png)
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
[<img src="https://user-images.githubusercontent.com/7511379/103150898-c79d5280-4789-11eb-9d16-e7f89225738b.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150898-c79d5280-4789-11eb-9d16-e7f89225738b.png)
* Simple FTP client (experimental).
[<img src="https://user-images.githubusercontent.com/7511379/103152009-7e9ecb80-4794-11eb-85f1-c97e189a3195.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103152009-7e9ecb80-4794-11eb-85f1-c97e189a3195.png)
#### Keyboard shortcuts
* **Ctrl + X** - only in bouquet list.
* **Ctrl + C** - only in services list.
* **Ctrl + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + Insert** - copies the selected channels from the main list to the the bouquet beginning
or inserts (creates) a new bouquet.
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
beginning or inserts (creates) a new bouquet.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
* **Ctrl + E** - edit.
* **Ctrl + R, F2** - rename.
@@ -30,40 +43,51 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + H** - hide/skip.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Ctrl + W** - switch to the channel and watch in the program.
* **Ctrl + W** - switch to the channel and watch in the program.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
* **Ctrl + O** - (re)load user data from current dir.
* **Ctrl + D** - load data from receiver.
* **Ctrl + U/B** upload data/bouquets to receiver.
* **Ctrl + U/B** - upload data/bouquets to receiver.
* **Ctrl + I** - extra info, details.
* **Ctrl + F** - show/hide search bar.
* **Ctrl + Shift + F** - show/hide filter bar.
* **Ctrl + Shift + F** - show/hide filter bar.
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
For multiple mouse selection (including Drag and Drop), press and hold the **Ctrl** key!
### Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings, python3-requests.
### Launching:
To start the program, in most cases it is enough to download the archive, unpack and run it by
double clicking on DemonEditor.desktop in the root directory, or launching from the console
with the command: ```./start.py```
## Minimum requirements
*Python >= 3.5.2, GTK+ >= 3.16 with PyGObject bindings, python3-requests.*
***Optional:** python3-gi-cairo, python3-pil, python3-chardet.*
## Installation and Launch
* ### Linux
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
and run it by double clicking on DemonEditor.desktop in the root directory,
or launching from the console with the command:```./start.py```
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
### Note:
To create a simple **debian package**, you can use the *build-deb.sh.*
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or those based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in my favourite Linux distributions
(the latest versions of [Linux Mint](https://linuxmint.com/) 18.* and 19* MATE 64-bit)!
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
* ### macOS (experimental)
**This program can also be run on macOS.**
To work in this OS, you must use a [separate branch](https://github.com/DYefremov/DemonEditor/tree/experimental-mac).
**The functionality and performance of this version may be different from the Linux version!**
## Important
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
### Important:
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
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!
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.
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.
## License
Licensed under the [MIT](LICENSE) license.

View File

@@ -1,2 +1,4 @@
theme: jekyll-theme-cayman
show_downloads: true
theme: jekyll-theme-slate
title: DemonEditor
description: Enigma2 channel and satellite list editor.
show_downloads: false

View File

@@ -5,7 +5,7 @@ import time
import urllib
import xml.etree.ElementTree as ETree
from enum import Enum
from ftplib import FTP, error_perm
from ftplib import FTP, CRLF, Error, error_perm
from http.client import RemoteDisconnected
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
@@ -35,25 +35,6 @@ class DownloadType(Enum):
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):
pass
@@ -62,8 +43,289 @@ class HttpApiException(Exception):
pass
def download_data(*, settings, download_type=DownloadType.ALL, callback=print, files_filter=None):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
class UtfFTP(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"
callback("FTP OK.\n")
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:
ftp.cwd(settings.services_path)
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()):
download_file(ftp, file, save_path, callback)
ftp.download_files(save_path, file_list, callback)
# *.xml and webtv
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):
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:
picons_path = settings.picons_local_path
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
if download_type is DownloadType.EPG:
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)
ftp.cwd(stb_path)
for file in filter(lambda f: f.endswith("epg.dat"), ftp.nlst()):
download_file(ftp, file, save_path, callback)
ftp.download_files(save_path, "epg.dat", callback)
callback("\nDone.\n")
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
data_path = settings.data_local_path
host = settings.host
@@ -110,7 +370,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
try:
if s_type is SettingsType.ENIGMA_2 and use_http:
ht = http(settings.http_user, settings.http_password, base_url, callback, settings.http_use_ssl)
ht = http(settings.user, settings.password, base_url, callback, settings.http_use_ssl)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
@@ -133,44 +393,47 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
if download_type is not DownloadType.PICONS:
# telnet
tn = telnet(host=host,
user=settings.telnet_user,
password=settings.telnet_password,
user=settings.user,
password=settings.password,
timeout=settings.telnet_timeout)
next(tn)
# 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"
callback("FTP OK.\n")
sat_xml_path = settings.satellites_xml_path
services_path = settings.services_path
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:
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:
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:
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:
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)
upload_bouquets(ftp, data_path, remove_unused, callback)
upload_files(ftp, data_path, DATA_FILES_LIST, callback)
ftp.upload_bouquets(data_path, remove_unused, callback)
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
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:
# resume enigma or restart neutrino
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
callback("Starting...\n" if s_type is SettingsType.ENIGMA_2 else "Rebooting...\n")
elif ht and use_http:
if download_type is DownloadType.BOUQUETS:
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
@@ -187,80 +450,13 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
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 *******************#
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):
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"
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:
done_callback()
@@ -269,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)
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):
init_auth(user, password, url, use_ssl)
data = get_post_data(url, password, url)
while True:
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 ""))
@@ -304,11 +484,11 @@ def telnet(host, port=23, user="", password="", timeout=5):
time.sleep(1)
command = yield
if user != "":
tn.read_until(b"login: ")
tn.read_until(b"login: ", timeout)
tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout)
if password != "":
tn.read_until(b"Password: ")
tn.read_until(b"Password: ", timeout)
tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
@@ -325,6 +505,58 @@ def telnet(host, port=23, user="", password="", timeout=5):
class HttpAPI:
__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):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
@@ -345,13 +577,19 @@ class HttpAPI:
url = self._base_url + req_type.value
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)
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"))
elif req_type is HttpRequestType.GRUB:
elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
elif req_type in (self.Request.REMOTE,
self.Request.POWER,
self.Request.VOL,
self.Request.EPG,
self.Request.TIMER):
url += ref
def done_callback(f):
callback(f.result())
@@ -361,17 +599,17 @@ class HttpAPI:
@run_task
def init(self):
user, password = self._settings.http_user, self._settings.http_password
user, password = self._settings.user, self._settings.password
use_ssl = self._settings.http_use_ssl
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(self._main_url, HttpRequestType.TOKEN.value)
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
s_id = get_session_id(user, password, url)
if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(HttpRequestType.INFO, None, self.init_callback)
self.send(self.Request.INFO, None, self.init_callback)
def init_callback(self, info):
if info:
@@ -394,24 +632,30 @@ class HttpAPI:
def get_response(req_type, url, data=None):
try:
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")}
elif req_type is HttpRequestType.GRUB:
elif req_type is HttpAPI.Request.GRUB:
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"):
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
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type is HttpAPI.Request.EPG:
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:
if req_type is HttpRequestType.TEST:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpRequestType.TEST:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
@@ -438,11 +682,11 @@ def init_auth(user, password, url, use_ssl=False):
def get_session_id(user, password, url):
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):
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
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
@@ -468,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)
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:
raise TestException(e)

View File

@@ -2,7 +2,7 @@ from app.commons import run_task
from app.settings import SettingsType
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
from .enigma.blacklist import get_blacklist, write_blacklist
from .enigma.bouquets import get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id
from .enigma.bouquets import to_bouquet_id, BouquetsWriter, BouquetsReader
from .enigma.lamedb import get_services as get_enigma_services, write_services as write_enigma_services
from .iptv import parse_m3u
from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets
@@ -27,7 +27,7 @@ def write_services(path, channels, s_type, format_version):
def get_bouquets(path, s_type):
if s_type is SettingsType.ENIGMA_2:
return get_enigma_bouquets(path)
return BouquetsReader(path).get()
elif s_type is SettingsType.NEUTRINO_MP:
return get_neutrino_bouquets(path)
@@ -35,7 +35,7 @@ def get_bouquets(path, s_type):
@run_task
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
if s_type is SettingsType.ENIGMA_2:
write_enigma_bouquets(path, bouquets, force_bq_names)
BouquetsWriter(path, bouquets, force_bq_names).write()
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)

View File

@@ -14,6 +14,7 @@ class BqServiceType(Enum):
IPTV = "IPTV"
MARKER = "MARKER" # 64
SPACE = "SPACE" # 832 [hidden marker]
ALT = "ALT" # Service with alternatives
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])

View File

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

View File

@@ -1,76 +1,187 @@
""" Module for working with Enigma2 bouquets. """
import re
from collections import Counter
from pathlib import Path
from app.commons import log
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType
_TV_ROOT_FILE_NAME = "bouquets.tv"
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
_TV_FILE = "bouquets.tv"
_RADIO_FILE = "bouquets.radio"
_DEFAULT_BOUQUET_NAME = "favourites"
def get_bouquets(path):
return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio",
BqType.RADIO.value)
def write_bouquets(path, bouquets, force_bq_names=False):
""" Creating and writing bouquets files.
class BouquetsWriter:
""" Class for creating and writing bouquet files..
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
"""
srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
line = []
pattern = re.compile("[^\\w_()]+")
m_index = [0]
s_index = [0]
_SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
for bqs in bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
def __init__(self, path, bouquets, force_bq_names=False):
self._path = path
self._bouquets = bouquets
self._force_bq_names = force_bq_names
self._marker_index = 0
self._space_index = 0
self._alt_index = 0
for index, bq in enumerate(bqs.bouquets):
bq_name = bq.name
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
bq_name = _DEFAULT_BOUQUET_NAME
def write(self):
line = []
pattern = re.compile("[^\\w_()]+")
for bqs in self._bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
for index, bq in enumerate(bqs.bouquets):
bq_name = bq.name
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
bq_name = _DEFAULT_BOUQUET_NAME
else:
bq_name = re.sub(pattern, "_", bq.name) if self._force_bq_names else "de{0:02d}".format(index)
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(self, path, name, services):
""" Writes single bouquet file. """
bouquet = ["#NAME {}\n".format(name)]
for srv in services:
s_type = srv.service_type
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = self._marker_index
self._marker_index += 1
bouquet.append(self._MARKER.format(m_data[2], m_data[-1]))
elif s_type == BqServiceType.SPACE.name:
bouquet.append(self._SPACE.format(self._space_index))
self._space_index += 1
elif s_type == BqServiceType.ALT.name:
services = srv.transponder
if services:
p = Path(path)
if self._force_bq_names:
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
else:
f_name = "alternatives.de{:02d}{}".format(self._alt_index, p.suffix)
self._alt_index += 1
alt_path = "{}/{}".format(p.parent, f_name)
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(alt_path, srv.service, services)
else:
bq_name = re.sub(pattern, "_", bq.name) if force_bq_names else "de{0:02d}".format(index)
line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services, m_index, s_index)
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
else:
bouquet.append("#SERVICE {}\n".format(data))
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def write_bouquet(path, name, services, current_marker, current_space):
bouquet = ["#NAME {}\n".format(name)]
marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
space = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
for srv in services:
s_type = srv.service_type
__slots__ = ["_path"]
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = current_marker[0]
current_marker[0] += 1
bouquet.append(marker.format(m_data[2], m_data[-1]))
elif s_type == BqServiceType.SPACE.name:
bouquet.append(space.format(current_space[0]))
current_space[0] += 1
else:
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
else:
bouquet.append("#SERVICE {}\n".format(data))
def __init__(self, path):
self._path = path
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def get(self):
""" Returns a tuple of TV and Radio bouquets. """
return self.parse_bouquets(_TV_FILE, BqType.TV.value), self.parse_bouquets(_RADIO_FILE, BqType.RADIO.value)
def parse_bouquets(self, bq_name, bq_type):
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
b_names = set()
real_b_names = Counter()
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = re.match(self._BQ_PAT, line)
if name:
b_name = name.group(1)
if b_name in b_names:
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
else:
b_names.add(b_name)
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
if rb_name in real_b_names:
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
rb_name))
real_b_names[rb_name] += 1
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None))
else:
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
""" Parsing services ids from bouquet file. """
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
bq_name = srvs.pop(0)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
s_type = srv_data[1]
if s_type == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif s_type == "832":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif s_type == "134":
alt = re.match(BouquetsReader._ALT_PAT, srv)
if alt:
alt_name, alt_type = alt.group(1), alt.group(2)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, srv.lstrip(), tuple(alt_srvs)))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
if len(srv_data) == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
return bq_name.lstrip("#NAME").strip(), services
def to_bouquet_id(srv):
@@ -82,81 +193,5 @@ def to_bouquet_id(srv):
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
def get_bouquet(path, bq_name, bq_type):
""" Parsing services ids from bouquet file. """
with open(path + "userbouquet.{}.{}".format(bq_name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
bq_name = srvs.pop(0)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
if srv_data[1] == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif srv_data[1] == "832":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif "http" in srv or srv_data[0] == "8193":
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
if len(srv_data) == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
return bq_name.lstrip("#NAME").strip(), services
def parse_bouquets(path, bq_name, bq_type):
with open(path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
bq_pattern = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
b_names = set()
real_b_names = Counter()
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = re.match(bq_pattern, line)
if name:
b_name = name.group(1)
if b_name in b_names:
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
else:
b_names.add(b_name)
rb_name, services = get_bouquet(path, b_name, bq_type)
if rb_name in real_b_names:
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type, rb_name))
real_b_names[rb_name] += 1
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(name=rb_name,
type=bq_type,
services=services,
locked=None,
hidden=None))
else:
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets
if __name__ == "__main__":
pass

View File

@@ -25,11 +25,16 @@ def write_services(path, services, format_version=4):
def write_to_lamedb(path, services):
""" 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"]
tr_lines = []
services_lines = ["end\nservices\n"]
tr_set = set()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
@@ -44,12 +49,12 @@ def write_to_lamedb(path, services):
lines.extend(tr_lines)
lines.extend(services_lines)
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):
""" Writing lamedb5 file """
""" Writing lamedb5 file. """
lines = [_HEADER.format(5) + "\n"]
services_lines = []
tr_set = set()
@@ -73,7 +78,7 @@ def write_to_lamedb5(path, services):
def parse(path, version=4):
""" Parsing lamedb """
""" Parsing lamedb. """
if version == 4:
return parse_v4(path)
elif version == 5:
@@ -82,7 +87,7 @@ def parse(path, version=4):
def parse_v3(services, transponders, path):
""" Parsing version 3 """
""" Parsing version 3. """
for t in transponders:
tr = transponders[t].lower()
tr_type = tr[0:1]
@@ -108,32 +113,37 @@ def parse_v3(services, transponders, path):
def parse_v4(path):
""" Parsing version 4 """
""" Parsing version 4. """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
else:
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 get_services_list(data, 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)
def get_services_list(data, path=None):
""" 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):
""" Parsing version 5 """
""" Parsing version 5. """
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines()
@@ -141,9 +151,9 @@ def parse_v5(path):
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
for l in lns:
if l.startswith("s:"):
srv_data = l.strip("s:").split(",", 2)
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
data_len = len(srv_data)
if data_len == 3:
@@ -151,15 +161,15 @@ def parse_v5(path):
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
elif l.startswith("t:"):
tr, srv = l.split(",")
elif line.startswith("t:"):
tr, srv = line.split(",")
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
return parse_services(srvs, trs, path)
def parse_transponders(arg):
""" Parsing transponders """
""" Parsing transponders. """
transponders = {}
for ar in arg:
tr = ar.replace("\n", "").split("\t")
@@ -170,9 +180,9 @@ def parse_transponders(arg):
def parse_services(services, transponders, path):
""" Parsing services """
""" Parsing services. """
services_list = []
blacklist = str(get_blacklist(path))
blacklist = get_blacklist(path) if path else {}
srvs = split(services, 3)
if srvs[0][0] == "": # remove first empty element
srvs.remove(srvs[0])
@@ -208,12 +218,13 @@ def parse_services(services, transponders, path):
# For comparison in bouquets. Needed in upper case!!!
fav_id = "{}:{}:{}:{}".format(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(",")
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))
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 = package[0][2:] if package else ""

View File

@@ -19,17 +19,30 @@ class StreamType(Enum):
NONE_REC_1 = "5001"
NONE_REC_2 = "5002"
E_SERVICE_URI = "8193"
E_SERVICE_HLS = "8739"
def parse_m3u(path, s_type):
with open(path) as file:
def parse_m3u(path, s_type, detect_encoding=True):
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
services = []
groups = set()
counter = 0
name = None
for line in file.readlines():
for line in str(data, encoding=encoding, errors="ignore").splitlines():
if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip()
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:

View File

@@ -99,10 +99,10 @@ class SettingsType(IntEnum):
""" Returns default settings for current type """
if self is self.ENIGMA_2:
return {"setting_type": self.value,
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
"http_user": "root", "http_password": "", "http_port": "80",
"http_timeout": 5, "http_use_ssl": False,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 5, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon/",
@@ -110,9 +110,10 @@ class SettingsType(IntEnum):
"backup_local_path": DATA_PATH + "enigma2/backup/"}
elif self is self.NEUTRINO_MP:
return {"setting_type": self,
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
@@ -279,22 +280,6 @@ class Settings:
def password(self, value):
self._cp_settings["password"] = value
@property
def http_user(self):
return self._cp_settings.get("http_user", self.get_default("http_user"))
@http_user.setter
def http_user(self, value):
self._cp_settings["http_user"] = value
@property
def http_password(self):
return self._cp_settings.get("http_password", self.get_default("http_password"))
@http_password.setter
def http_password(self, value):
self._cp_settings["http_password"] = value
@property
def http_port(self):
return self._cp_settings.get("http_port", self.get_default("http_port"))
@@ -319,22 +304,6 @@ class Settings:
def http_use_ssl(self, value):
self._cp_settings["http_use_ssl"] = value
@property
def telnet_user(self):
return self._cp_settings.get("telnet_user", self.get_default("telnet_user"))
@telnet_user.setter
def telnet_user(self, value):
self._cp_settings["telnet_user"] = value
@property
def telnet_password(self):
return self._cp_settings.get("telnet_password", self.get_default("telnet_password"))
@telnet_password.setter
def telnet_password(self, value):
self._cp_settings["telnet_password"] = value
@property
def telnet_port(self):
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
@@ -620,6 +589,22 @@ class Settings:
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
def is_themes_support(self):
return self._settings.get("is_themes_support", False)

View File

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

View File

@@ -1,15 +1,20 @@
""" Module for download satellites from internet ("flysat.com")
for replace or update current satellites.xml file.
""" Module for downloading satellites, transponders ans services from the web.
Sources: www.flysat.com, www.lyngsat.com.
Replaces or updates the current satellites.xml file.
"""
import re
import requests
from enum import Enum
from html.parser import HTMLParser
import requests
from app.commons import log
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):
@@ -22,11 +27,55 @@ class SatelliteSource(Enum):
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):
""" 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=' '):
HTMLParser.__init__(self)
@@ -42,9 +91,9 @@ class SatellitesParser(HTMLParser):
self._source = source
def handle_starttag(self, tag, attrs):
if tag == 'td':
if tag == "td":
self._is_td = True
if tag == 'tr':
if tag == "tr":
self._is_th = True
if tag == "a":
self._current_row.append(attrs[0][1])
@@ -55,16 +104,16 @@ class SatellitesParser(HTMLParser):
self._current_cell.append(data.strip())
def handle_endtag(self, tag):
if tag == 'td':
if tag == "td":
self._is_td = False
elif tag == 'tr':
elif tag == "tr":
self._is_th = False
if tag in ('td', 'th'):
if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
elif tag == "tr":
row = self._current_row
self._rows.append(row)
self._current_row = []
@@ -80,7 +129,7 @@ class SatellitesParser(HTMLParser):
for src in SatelliteSource.get_sources(self._source):
try:
request = requests.get(url=src, headers=self._HEADERS)
request = requests.get(url=src, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log(repr(e))
return []
@@ -98,17 +147,24 @@ class SatellitesParser(HTMLParser):
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
elif self._source is SatelliteSource.LYNGSAT:
extra_pattern = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html")
extra_pattern = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html")
base_url = "https://www.lyngsat.com/"
sats = []
names = set()
current_pos = "0"
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
r_len = len(row)
if r_len == 7:
current_pos = self.parse_position(row[2])
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, row[5], base_url + row[1], False)) # [all in one] satellites
sats.append((row[4], current_pos, row[5], base_url + row[3], False))
if name not in names:
# [all in one] satellites
sats.append((name, current_pos, row[5], base_url + row[1], False))
names.add(name)
name = row[4]
if name not in names:
sats.append((name, current_pos, row[5], base_url + row[3], False))
names.add(name)
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
@@ -146,7 +202,7 @@ class SatellitesParser(HTMLParser):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
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
trs = []
if reason == "OK":
@@ -247,5 +303,198 @@ class SatellitesParser(HTMLParser):
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__":
pass

View File

@@ -129,6 +129,21 @@ class YouTube:
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):
""" Very simple parser to handle YouTube playlist pages. """
@@ -139,6 +154,7 @@ class PlayListParser(HTMLParser):
self._header = ""
self._playlist = []
self._is_script = False
self._scr_start = ('var ytInitialData = ', 'window["ytInitialData"] = ')
def handle_starttag(self, tag, attrs):
if tag == "script":
@@ -147,8 +163,11 @@ class PlayListParser(HTMLParser):
def handle_data(self, data):
if self._is_script:
data = data.lstrip()
if data.startswith('window["ytInitialData"] = '):
data = data.split(";")[0].lstrip('window["ytInitialData"] = ')
if data.startswith(self._scr_start):
data = data.split(";")[0]
for s in self._scr_start:
data = data.lstrip(s)
try:
resp = json.loads(data)
except JSONDecodeError as e:
@@ -164,7 +183,7 @@ class PlayListParser(HTMLParser):
ct = resp.get("contents", None)
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)]:
self._playlist.append(d)
self._is_script = False
@@ -205,6 +224,7 @@ class YouTubeDL:
_DownloadError = None
_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].
"extract_flat": False, # Do not resolve URLs, return the immediate result.
"quiet": True, # Do not print messages to stdout.
"simulate": True, # Do not download the video files.
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
@@ -316,8 +336,17 @@ class YouTubeDL:
self._callback("Update process. Please wait.", False)
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:
info = self._dl.extract_info(url, download=False)
return self._dl.extract_info(url, download=False)
except URLError as e:
log(str(e))
raise YouTubeException(e)
@@ -325,13 +354,13 @@ class YouTubeDL:
log(str(e))
if not skip_errors:
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):

1850
app/ui/control.glade Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,676 @@
""" 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()
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,17 +40,17 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">1.0.0 Beta</property>
<property name="version">1.0.4 Beta</property>
<property name="copyright">2018-2020 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellites list editor for GNU/Linux</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for GNU/Linux.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
Подробнее в &lt;a href="http://opensource.org/licenses/mit-license.php"&gt;The MIT License (MIT)&lt;/a&gt;.</property>
<property name="authors">Dmitriy Yefremov
</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="wrap_license">True</property>
<property name="license_type">mit-x11</property>
@@ -176,6 +176,7 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="box4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSpinner" id="spinner">
@@ -212,10 +213,10 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<style>
<class name="app-notification"/>
</style>
</object>
</child>
<style>
<class name="app-notification"/>
</style>
</object>
</interface>

View File

@@ -2,6 +2,7 @@
import locale
from enum import Enum
from functools import lru_cache
from pathlib import Path
from app.commons import run_idle
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
@@ -72,12 +73,13 @@ class WaitDialog:
self._dialog.destroy()
def show_dialog(dialog_type: DialogType, transient, text=None, settings=None, action_type=None, file_filter=None):
""" Shows dialogs by name """
def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
title=None, create_dir=False):
""" Shows dialogs by name. """
if dialog_type in (DialogType.INFO, DialogType.ERROR):
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
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:
return get_input_dialog(transient, text)
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)
def get_chooser_dialog(transient, settings, name, patterns):
def get_chooser_dialog(transient, settings, name, patterns, title=None):
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
@@ -97,26 +99,30 @@ def get_chooser_dialog(transient, settings, name, patterns):
transient=transient,
settings=settings,
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):
dialog = Gtk.FileChooserDialog(get_message(text) if text else "", transient,
action_type if action_type is not None else Gtk.FileChooserAction.SELECT_FOLDER,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
use_header_bar=IS_GNOME_SESSION)
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
text = get_message(text) if text else ""
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
buttons = buttons or (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
dialog = Gtk.FileChooserDialog(text, transient, action_type, buttons, use_header_bar=IS_GNOME_SESSION)
dialog.set_title(get_message(title) if title else "")
dialog.set_create_folders(dirs)
if file_filter is not None:
dialog.add_filter(file_filter)
path = settings.data_local_path
dialog.set_current_folder(path)
dialog.set_current_folder(settings.data_local_path)
response = dialog.run()
if response == Gtk.ResponseType.OK:
if dialog.get_filename():
path = dialog.get_filename()
if action_type is not Gtk.FileChooserAction.OPEN:
path = path + "/"
response = path
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
path = Path(dialog.get_filename() or dialog.get_current_folder())
if path.is_dir():
response = "{}/".format(path.resolve())
elif path.is_file():
response = str(path.resolve())
dialog.destroy()
return response

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
<!-- Generated with glade 3.22.2
The MIT License (MIT)
@@ -34,7 +34,7 @@ Author: Dmitriy Yefremov
<!-- interface-copyright 2018 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkWindow" id="download_dialog_window">
<property name="width_request">500</property>
<property name="width_request">550</property>
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
@@ -217,7 +217,7 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkBox" id="profile_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
@@ -264,6 +264,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
@@ -423,201 +424,6 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="settings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="settings_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkLabel" id="login_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Login:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="login_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="text">root</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">avatar-default-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="password_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Password:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="visibility">False</property>
<property name="invisible_char">●</property>
<property name="text">root</property>
<property name="primary_icon_name">emblem-readonly</property>
<property name="input_purpose">password</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="port_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Port:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="port_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="text" translatable="yes">21</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="timeout_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">alarm-symbolic</property>
<property name="input_purpose">digits</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="timeout_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Timeout:</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkBox" id="settings_buttons_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkRadioButton" id="ftp_radio_button">
<property name="label" translatable="yes">FTP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">telnet_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="http_radio_button">
<property name="label" translatable="yes">HTTP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<property name="group">telnet_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="telnet_radio_button">
<property name="label" translatable="yes">Telnet</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<property name="group">ftp_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="group"/>
</style>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<property name="visible">True</property>
@@ -713,17 +519,7 @@ Author: Dmitriy Yefremov
<property name="position">5</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<object class="GtkSizeGroup" id="settings_size_group">
<widgets>
<widget name="ftp_radio_button"/>
<widget name="http_radio_button"/>
<widget name="telnet_radio_button"/>
</widgets>
</object>
</interface>

View File

@@ -21,7 +21,6 @@ class DownloadDialog:
handlers = {"on_receive": self.on_receive,
"on_send": self.on_send,
"on_settings_button": self.on_settings_button,
"on_settings": self.on_settings,
"on_profile_changed": self.on_profile_changed,
"on_use_http_state_set": self.on_use_http_state_set,
@@ -32,7 +31,6 @@ class DownloadDialog:
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
builder.connect_signals(handlers)
self._current_property = "FTP"
self._dialog_window = builder.get_object("download_dialog_window")
self._dialog_window.set_transient_for(transient)
self._info_bar = builder.get_object("info_bar")
@@ -46,12 +44,6 @@ class DownloadDialog:
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
self._satellites_radio_button = builder.get_object("satellites_radio_button")
self._webtv_radio_button = builder.get_object("webtv_radio_button")
self._login_entry = builder.get_object("login_entry")
self._password_entry = builder.get_object("password_entry")
self._host_entry = builder.get_object("host_entry")
self._port_entry = builder.get_object("port_entry")
self._timeout_entry = builder.get_object("timeout_entry")
self._settings_buttons_box = builder.get_object("settings_buttons_box")
self._use_http_switch = builder.get_object("use_http_switch")
self._http_radio_button = builder.get_object("http_radio_button")
self._use_http_box = builder.get_object("use_http_box")
@@ -71,7 +63,6 @@ class DownloadDialog:
self._data_path_entry.set_text(self._settings.data_local_path)
is_enigma = self._s_type is SettingsType.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._http_radio_button.set_visible(is_enigma)
self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma and self._settings.use_http)
self._remove_unused_check_button.set_active(self._settings.remove_unused_bouquets)
@@ -104,26 +95,6 @@ class DownloadDialog:
def destroy(self):
self._dialog_window.destroy()
def on_settings_button(self, button):
if button.get_active():
label = button.get_label()
if label == "Telnet":
self._login_entry.set_text(self._settings.telnet_user)
self._password_entry.set_text(self._settings.telnet_password)
self._port_entry.set_text(self._settings.telnet_port)
self._timeout_entry.set_text(str(self._settings.telnet_timeout))
elif label == "HTTP":
self._login_entry.set_text(self._settings.http_user)
self._password_entry.set_text(self._settings.http_password)
self._port_entry.set_text(self._settings.http_port)
self._timeout_entry.set_text(str(self._settings.http_timeout))
elif label == "FTP":
self._login_entry.set_text(self._settings.user)
self._password_entry.set_text(self._settings.password)
self._port_entry.set_text(self._settings.port)
self._timeout_entry.set_text("")
self._current_property = label
def on_settings(self, item):
response = show_settings_dialog(self._dialog_window, self._settings)
if response != Gtk.ResponseType.CANCEL:
@@ -132,11 +103,6 @@ class DownloadDialog:
gen = self._update_settings_callback()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
for button in self._settings_buttons_box.get_children():
if button.get_active():
self.on_settings_button(button)
break
def on_profile_changed(self, box):
active = box.get_active_text()
if active in self._settings.profiles:

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

@@ -0,0 +1,658 @@
<?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_top">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="GDK_CONTROL_MASK"/>
<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="GDK_CONTROL_MASK"/>
<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,10 +1,9 @@
from contextlib import suppress
from pathlib import Path
from app.commons import run_idle
from app.eparser import get_bouquets, get_services
from app.commons import run_idle, log
from app.eparser import get_bouquets, get_services, BouquetsReader
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.enigma.bouquets import get_bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
@@ -63,7 +62,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
def get_enigma2_bouquet(path):
path, sep, f_name = path.rpartition("userbouquet.")
name, sep, suf = f_name.rpartition(".")
bq = get_bouquet(path, name, suf)
bq = BouquetsReader.get_bouquet(path, name, suf)
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
return bouquet
@@ -119,6 +118,7 @@ class ImportDialog:
self._services_model.clear()
try:
if not self._bouquets:
log("Import [init data]: getting bouquets...")
self._bouquets = get_bouquets(path, self._profile)
for bqs in self._bouquets:
for bq in bqs.bouquets:
@@ -129,6 +129,7 @@ class ImportDialog:
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:
log("Import error [init data]: {}".format(e))
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_import(self, item):
@@ -139,9 +140,17 @@ class ImportDialog:
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return
self.import_data()
@run_idle
def import_data(self):
""" Importing data into models. """
if not self._bouquets:
return
log("Importing data...")
services = set()
to_delete = set()
for row in self._main_model:
bq = (row[0], row[1])
if row[-1]:
@@ -151,19 +160,16 @@ class ImportDialog:
services.add(srv)
else:
to_delete.add(bq)
bqs_to_delete = []
for bqs in self._bouquets:
for bq in bqs.bouquets:
if (bq.name, bq.type) in to_delete:
bqs_to_delete.append(bq)
for bqs in self._bouquets:
bq = bqs.bouquets
for b in bqs_to_delete:
with suppress(ValueError):
bq.remove(b)
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services)))
self._dialog_window.destroy()

View File

@@ -222,6 +222,9 @@ Author: Dmitriy Yefremov
<row>
<col id="0">eServiceUri</col>
</row>
<row>
<col id="0">eServiceHLS</col>
</row>
</data>
</object>
<object class="GtkDialog" id="iptv_dialog">

View File

@@ -11,7 +11,7 @@ from app.commons import run_idle, run_task
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.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 .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,
@@ -40,7 +40,9 @@ def get_stream_type(box):
return StreamType.NONE_REC_1.value
elif active == 3:
return StreamType.NONE_REC_2.value
return StreamType.E_SERVICE_URI.value
elif active == 4:
return StreamType.E_SERVICE_URI.value
return StreamType.E_SERVICE_HLS.value
class IptvDialog:
@@ -161,6 +163,8 @@ class IptvDialog:
self._stream_type_combobox.set_active(3)
elif stream_type is StreamType.E_SERVICE_URI:
self._stream_type_combobox.set_active(4)
elif stream_type is StreamType.E_SERVICE_HLS:
self._stream_type_combobox.set_active(5)
except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
@@ -200,7 +204,8 @@ class IptvDialog:
def on_url_changed(self, entry):
url_str = entry.get_text()
url = urlparse(url_str)
cond = all([url.scheme, url.netloc, url.path]) or self.get_type() == StreamType.E_SERVICE_URI.value
e_types = (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value)
cond = all([url.scheme, url.netloc, url.path]) or self.get_type() in e_types
entry.set_name("GtkEntry" if cond else _DIGIT_ENTRY_NAME)
yt_id = YouTube.get_yt_id(url_str)
@@ -251,7 +256,7 @@ class IptvDialog:
yield True
def on_stream_type_changed(self, item):
if self.get_type() == StreamType.E_SERVICE_URI.value:
if self.get_type() in (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value):
self.show_info_message("DreamOS only!", Gtk.MessageType.WARNING)
self.update_reference_entry()
@@ -665,7 +670,9 @@ class YtListImportDialog:
def update_refs_list(self):
if self._yt_list_id:
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:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
return

File diff suppressed because it is too large Load Diff

View File

@@ -46,54 +46,57 @@ def move_items(key, view: Gtk.TreeView):
""" Move items in the tree view """
selection = view.get_selection()
model, paths = selection.get_selected_rows()
if not paths:
return
if paths:
mod_length = len(model)
if mod_length == len(paths):
return
cursor_path = view.get_cursor()[0]
max_path = Gtk.TreePath.new_from_indices((mod_length,))
min_path = Gtk.TreePath.new_from_indices((0,))
is_tree_store = type(model) is Gtk.TreeStore
mod_length = len(model)
if not is_tree_store and mod_length == len(paths):
return
cursor_path = view.get_cursor()[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
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:
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 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)
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):
@@ -161,7 +164,8 @@ def rename(view, parent_window, target, fav_view=None, service_view=None, servic
return
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:
for row in get_base_model(service_view.get_model()):
@@ -587,8 +591,8 @@ def get_bouquets_names(model):
# ***************** Others *********************#
def update_entry_data(entry, dialog, settings):
""" Updates value in text entry from chooser dialog """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings)
""" Updates value in text entry from chooser dialog. """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings, create_dir=True)
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
entry.set_text(response)
return response

File diff suppressed because it is too large Load Diff

View File

@@ -1328,7 +1328,35 @@ Author: Dmitriy Yefremov
</row>
</data>
</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">
<property name="width_request">480</property>
<property name="height_request">400</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
@@ -1338,7 +1366,7 @@ Author: Dmitriy Yefremov
<property name="gravity">center</property>
<signal name="delete-event" handler="on_quit" swapped="no"/>
<child type="titlebar">
<object class="GtkHeaderBar">
<object class="GtkHeaderBar" id="sat_update_header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Satellites update</property>
@@ -1398,6 +1426,7 @@ Author: Dmitriy Yefremov
<property name="stock">gtk-refresh</property>
</object>
</child>
<accelerator key="o" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
@@ -1412,8 +1441,8 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<object class="GtkButton" id="cancel_data_button">
<property name="visible">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Cancel</property>
@@ -1434,13 +1463,14 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="receive_sat_list_tool_button">
<object class="GtkButton" id="receive_data_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Receive</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"/>
<child>
<object class="GtkImage" id="receive_sat_list_tool_button_image">
<property name="visible">True</property>
@@ -1604,7 +1634,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">0</property>
</packing>
</child>
<child>
@@ -1727,97 +1757,290 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="sat_update_scrolled_window">
<property name="width_request">480</property>
<property name="height_request">320</property>
<object class="GtkPaned" id="sat_update_main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">2</property>
<property name="shadow_type">in</property>
<property name="orientation">vertical</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkTreeView" id="sat_update_tree_view">
<object class="GtkScrolledWindow" id="sat_update_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_sat_list_model_sort</property>
<signal name="button-press-event" handler="on_popup_menu" object="satellites_update_popup_menu" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="sat_update_treeview_selection">
<property name="mode">multiple</property>
</object>
</child>
<property name="margin_top">2</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeViewColumn" id="upd_satellite_column">
<property name="title" translatable="yes">Satellite</property>
<property name="expand">True</property>
<property name="reorderable">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="upd_satellite_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_position_column">
<property name="title" translatable="yes">Position</property>
<property name="reorderable">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText" id="upd_position_cellrenderertext"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_type_column">
<property name="title" translatable="yes">Type</property>
<property name="reorderable">True</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererText" id="upd_type_cellrenderertext"/>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_url_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Url</property>
<child>
<object class="GtkCellRendererText" id="upd_url_cellrenderertext"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_selected_treeviewcolumn">
<property name="title" translatable="yes">Selected</property>
<property name="reorderable">True</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererToggle" id="upd_selected_cellrenderertoggle">
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
<object class="GtkTreeView" id="sat_update_tree_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">update_sat_list_model_sort</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_popup_menu" object="satellites_update_popup_menu" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="sat_update_treeview_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_satellite_column">
<property name="title" translatable="yes">Satellite</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="upd_satellite_cellrenderertext">
<property name="xalign">0.01</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_position_column">
<property name="title" translatable="yes">Position</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText" id="upd_position_cellrenderertext">
<property name="xalign">0.49</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererText" id="upd_type_cellrenderertext">
<property name="xalign">0.49</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_url_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Url</property>
<child>
<object class="GtkCellRendererText" id="upd_url_cellrenderertext"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="upd_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="reorderable">True</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererToggle" id="upd_selected_cellrenderer">
<signal name="toggled" handler="on_satellite_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">4</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="active">4</attribute>
</attributes>
</child>
</object>
</child>
</object>
<packing>
<property name="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="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.01</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>
<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.01</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.49</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.49</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.49</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>
</object>
<packing>
@@ -1834,7 +2057,6 @@ Author: Dmitriy Yefremov
<property name="margin_right">1</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="resize_toplevel">True</property>
<child>
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
<property name="height_request">120</property>

View File

@@ -1,15 +1,15 @@
import concurrent.futures
import re
import time
import concurrent.futures
from math import fabs
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.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog, get_message
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
@@ -279,7 +279,7 @@ class SatellitesDialog:
@run_idle
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
def parse_data(model, path, itr, sats):
@@ -326,7 +326,7 @@ class TransponderDialog:
self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry")
# pattern for frequency and rate entries (only digits)
self._pattern = re.compile("\D")
self._pattern = re.compile(r"\D")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -428,16 +428,17 @@ class SatelliteDialog:
return Satellite(name=name, flags="0", position=pos, transponders=None)
# ***************** Satellite update dialog *******************#
# ********************** Update dialogs ************************ #
class SatellitesUpdateDialog:
""" Dialog for update satellites over internet """
class UpdateDialog:
""" 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,
"on_receive_satellites_list": self.on_receive_satellites_list,
"on_receive_data": self.on_receive_data,
"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_filter_toggled": self.on_filter_toggled,
"on_find_toggled": self.on_find_toggled,
@@ -450,26 +451,36 @@ class SatellitesUpdateDialog:
"on_search_up": self.on_search_up,
"on_quit": self.on_quit}
self._settings = settings
self._download_task = False
self._parser = None
self._size_name = "{}_window_size".format("_".join(re.findall("[A-Z][^A-Z]*", self.__class__.__name__))).lower()
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_update_window", "update_source_store", "update_sat_list_store",
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
"remove_selection_image"))
"remove_selection_image", "update_transponder_store", "update_service_store"))
builder.connect_signals(handlers)
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
self._main_model = main_model
# self._dialog.get_content_area().set_border_width(0)
if title:
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._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._sat_update_expander = builder.get_object("sat_update_expander")
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._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
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._from_pos_button = builder.get_object("from_pos_button")
@@ -485,21 +496,31 @@ class SatellitesUpdateDialog:
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
self._download_task = False
self._parser = None
window_size = self._settings.get(self._size_name)
if window_size:
self._window.resize(*window_size)
def show(self):
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
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!")
return
model = get_base_model(self._sat_view.get_model())
model.clear()
self._download_task = True
self.is_download = True
src = self._source_box.get_active()
if not self._parser:
self._parser = SatellitesParser()
@@ -511,7 +532,7 @@ class SatellitesUpdateDialog:
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
if sats:
callback(sats)
self._download_task = False
self.is_download = False
@run_idle
def append_satellites(self, sats):
@@ -520,70 +541,16 @@ class SatellitesUpdateDialog:
model.append(sat)
@run_idle
def on_receive_satellites_list(self, item):
if self._download_task:
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._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
def update_expander(self):
self._sat_update_expander.set_expanded(True)
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):
@run_idle
def append(t):
@@ -596,11 +563,15 @@ class SatellitesUpdateDialog:
def on_cancel_receive(self, item=None):
self._download_task = False
def on_selected_toggled(self, toggle, path):
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)
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
def update_receive_button_state(self, model):
self._receive_button.set_sensitive((any(r[4] for r in model)))
@@ -625,7 +596,7 @@ class SatellitesUpdateDialog:
self._filter_positions = self.get_positions()
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":
return True
@@ -636,7 +607,7 @@ class SatellitesUpdateDialog:
if from_pos > to_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):
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
@@ -669,10 +640,298 @@ class SatellitesUpdateDialog:
self._filter_model.get_model().set_value(itr, 4, select)
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
def append_satellite(model, sat):

View File

@@ -1,15 +1,16 @@
import re
import os
import re
from app.commons import run_idle
from app.eparser import Service
from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, \
get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION, TrType, \
SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
HIERARCHY)
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 .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"
@@ -438,14 +439,17 @@ class ServiceDetailsDialog:
def update_fav_view(self, old_service, new_service):
model = self._fav_view.get_model()
for row in filter(lambda r: old_service.fav_id == r[7], model):
model.set(row.iter, {1: new_service.coded,
2: new_service.service,
3: new_service.locked,
4: new_service.hide,
5: new_service.service_type,
6: new_service.pos,
7: new_service.fav_id,
8: new_service.picon})
itr = row.iter
if not model.get_value(itr, Column.FAV_BACKGROUND):
model.set_value(itr, Column.FAV_SERVICE, new_service.service)
model.set(itr, {Column.FAV_CODED: new_service.coded,
Column.FAV_LOCKED: new_service.locked,
Column.FAV_HIDE: new_service.hide,
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):
if not os.path.isdir(self._picons_dir_path):

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import os
import re
from enum import Enum
from app.commons import run_task, run_idle, log
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
@@ -14,12 +13,6 @@ def show_settings_dialog(transient, options):
return SettingsDialog(transient, options).show()
class Property(Enum):
FTP = "ftp"
HTTP = "http"
TELNET = "telnet"
class SettingsDialog:
_DIGIT_ENTRY_NAME = "digit-entry"
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
@@ -50,7 +43,6 @@ class SettingsDialog:
"on_profile_set_default": self.on_profile_set_default,
"on_lang_changed": self.on_lang_changed,
"on_main_settings_visible": self.on_main_settings_visible,
"on_network_settings_visible": self.on_network_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
"on_click_mode_togged": self.on_click_mode_togged,
"on_play_mode_changed": self.on_play_mode_changed,
@@ -61,7 +53,7 @@ class SettingsDialog:
"on_theme_changed": self.on_theme_changed,
"on_theme_add": self.on_theme_add,
"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_remove": self.on_icon_theme_remove}
@@ -84,15 +76,13 @@ class SettingsDialog:
self._port_field = builder.get_object("port_field")
self._login_field = builder.get_object("login_field")
self._password_field = builder.get_object("password_field")
self._http_login_field = builder.get_object("http_login_field")
self._http_password_field = builder.get_object("http_password_field")
self._http_port_field = builder.get_object("http_port_field")
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
self._telnet_login_field = builder.get_object("telnet_login_field")
self._telnet_password_field = builder.get_object("telnet_password_field")
self._telnet_port_field = builder.get_object("telnet_port_field")
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
self._settings_stack = builder.get_object("settings_stack")
# Test
self._ftp_radio_button = builder.get_object("ftp_radio_button")
self._http_radio_button = builder.get_object("http_radio_button")
# Paths
self._services_field = builder.get_object("services_field")
self._user_bouquet_field = builder.get_object("user_bouquet_field")
@@ -176,7 +166,6 @@ class SettingsDialog:
self._profile_remove_button = builder.get_object("profile_remove_button")
self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", header_separator, "visible")
self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible")
# Style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -196,6 +185,8 @@ class SettingsDialog:
self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
self._dark_mode_switch = builder.get_object("dark_mode_switch")
self._layout_switch = builder.get_object("layout_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.bind_property("active", builder.get_object("gtk_theme_frame"), "sensitive")
self._themes_support_switch.bind_property("active", builder.get_object("icon_theme_frame"), "sensitive")
@@ -206,7 +197,6 @@ class SettingsDialog:
is_enigma_profile = s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
self.update_header_bar()
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._lang_combo_box.set_active_id(self._ext_settings.language)
@@ -262,12 +252,8 @@ class SettingsDialog:
self._port_field.set_text(self._settings.port)
self._login_field.set_text(self._settings.user)
self._password_field.set_text(self._settings.password)
self._http_login_field.set_text(self._settings.http_user)
self._http_password_field.set_text(self._settings.http_password)
self._http_port_field.set_text(self._settings.http_port)
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
self._telnet_login_field.set_text(self._settings.telnet_user)
self._telnet_password_field.set_text(self._settings.telnet_password)
self._telnet_port_field.set_text(self._settings.telnet_port)
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
self._services_field.set_text(self._settings.services_path)
@@ -323,12 +309,8 @@ class SettingsDialog:
self._settings.port = self._port_field.get_text()
self._settings.user = self._login_field.get_text()
self._settings.password = self._password_field.get_text()
self._settings.http_user = self._http_login_field.get_text()
self._settings.http_password = self._http_password_field.get_text()
self._settings.http_port = self._http_port_field.get_text()
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
self._settings.telnet_user = self._telnet_login_field.get_text()
self._settings.telnet_password = self._telnet_password_field.get_text()
self._settings.telnet_port = self._telnet_port_field.get_text()
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
self._settings.services_path = self._services_field.get_text()
@@ -361,6 +343,7 @@ class SettingsDialog:
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.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
@@ -386,16 +369,15 @@ class SettingsDialog:
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
return
self.show_spinner(True)
current_property = Property(self._settings_stack.get_visible_child_name())
if current_property is Property.HTTP:
self.test_http()
elif current_property is Property.TELNET:
self.test_telnet()
elif current_property is Property.FTP:
if self._ftp_radio_button.get_active():
self.test_ftp()
elif self._http_radio_button.get_active():
self.test_http()
else:
self.test_telnet()
def test_http(self):
user, password = self._http_login_field.get_text(), self._http_password_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
host, port = self._host_field.get_text(), self._http_port_field.get_text()
use_ssl = self._http_use_ssl_check_button.get_active()
try:
@@ -410,7 +392,7 @@ class SettingsDialog:
def test_telnet(self):
timeout = int(self._telnet_timeout_spin_button.get_value())
host, port = self._host_field.get_text(), self._telnet_port_field.get_text()
user, password = self._telnet_login_field.get_text(), self._telnet_password_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
self.show_spinner(False)
@@ -576,9 +558,6 @@ class SettingsDialog:
self._apply_profile_button.set_visible(name == "profiles")
self._apply_presets_button.set_visible(name == "streaming")
def on_network_settings_visible(self, stack, param):
self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP)
def on_http_use_ssl_toggled(self, button):
active = button.get_active()
self._settings.http_use_ssl = active
@@ -705,7 +684,7 @@ class SettingsDialog:
Gtk.Settings().get_default().set_property("gtk-theme-name", "")
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":
return
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@@ -778,6 +757,7 @@ class SettingsDialog:
@run_idle
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)
t_support = self._ext_settings.is_themes_support
self._themes_support_switch.set_active(t_support)
if t_support:

View File

@@ -7,6 +7,37 @@
margin: 1px;
}
paned > separator {
background-repeat: no-repeat;
background-position: center;
background-size: 2px 24px;
}
.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;
}
.time-entry {
padding: 0px;
margin: 0px;
}
.group {}
.group :first-child {
@@ -17,10 +48,11 @@
.group :last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-width: 0;
}
.group :not(:first-child):not(:last-child) {
border-radius: 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

@@ -5,7 +5,7 @@ import gi
from gi.repository import GLib
from app.commons import log
from app.connections import HttpRequestType
from app.connections import HttpAPI
from app.tools.yt import YouTube
from app.ui.iptv import get_yt_icon
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
@@ -124,7 +124,7 @@ class LinksTransmitter:
else:
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
def on_done(self, res):
@@ -134,21 +134,21 @@ class LinksTransmitter:
GLib.idle_add(self._tool_bar.set_sensitive, True)
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):
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):
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):
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):
""" Remove added links in the playlist. """
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):
GLib.idle_add(self._tool_bar.set_sensitive, not res)
@@ -159,7 +159,7 @@ class LinksTransmitter:
for ref in res:
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", ""),
self.on_done,
self.__STREAM_PREFIX)

View File

@@ -107,6 +107,7 @@ class KeyboardKey(Enum):
LEFT = 113
RIGHT = 114
F2 = 68
F7 = 73
SPACE = 65
DELETE = 119
BACK_SPACE = 22
@@ -156,7 +157,7 @@ class BqGenType(Enum):
class Column(IntEnum):
""" Column nums in the views """
# main view
# Main view
SRV_CAS_FLAGS = 0
SRV_STANDARD = 1
SRV_CODED = 2
@@ -179,7 +180,7 @@ class Column(IntEnum):
SRV_TRANSPONDER = 19
SRV_TOOLTIP = 20
SRV_BACKGROUND = 21
# fav view
# FAV view
FAV_NUM = 0
FAV_CODED = 1
FAV_SERVICE = 2
@@ -191,11 +192,20 @@ class Column(IntEnum):
FAV_PICON = 8
FAV_TOOLTIP = 9
FAV_BACKGROUND = 10
# bouquets view
# Bouquets view
BQ_NAME = 0
BQ_LOCKED = 1
BQ_HIDDEN = 2
BQ_TYPE = 3
# Alternatives view
ALT_NUM = 0
ALT_PICON = 1
ALT_SERVICE = 2
ALT_TYPE = 3
ALT_POS = 4
ALT_FAV_ID = 5
ALT_ID = 6
ALT_ITER = 7
def __index__(self):
""" Overridden to get the index in slices directly """

View File

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

View File

@@ -4,7 +4,7 @@ DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc)
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
Main features of the program:
Editing bouquets, channels, satellites.
@@ -40,7 +40,7 @@ Keyboard shortcuts:
Ctrl + F - show/hide search bar.
Ctrl + Shift + F - show/hide filter bar.
For multiple mouse selection (including Drag and Drop), press and hold the **Ctrl** key!
For multiple selection with the mouse, press and hold the Ctrl key!
Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings, python3-requests.

View File

@@ -1,9 +1,9 @@
Package: demon-editor
Version: 1.0.0-Beta
Version: 1.0.4-Beta
Section: utils
Priority: optional
Architecture: all
Essential: no
Depends: python3 (>= 3.5)
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Description: Enigma2 channel and satellites list editor
Description: Enigma2 channel and satellite list editor

View File

@@ -1,8 +1,10 @@
[Desktop Entry]
Version=1.0
Name=DemonEditor
Comment=Channels and satellites list editor for Enigma2
Comment=Channel and satellite list editor for Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
Icon=demon-editor
Exec=/usr/bin/demon-editor
Terminal=false

View File

@@ -203,8 +203,8 @@ msgstr "Цякучы шлях да дадзеных:"
msgid "Data:"
msgstr "Дадзеныя:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Рэдактар спіса каналаў і спадарожнікаў Enigma2\n для GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Рэдактар спіса каналаў і спадарожнікаў Enigma2\n для GNU/Linux."
msgid "Host:"
msgstr "Адрас рэсівера:"
@@ -1027,3 +1027,177 @@ 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 "Атрыб."
msgid "Toggle display position"
msgstr "Перамкнуць пазіцыю адлюстравання"
msgid "Alternatives"
msgstr "Альтэрнатывы"
msgid "Add alternatives"
msgstr "Дадаць альтэрнатывы"
msgid "DreamOS only!"
msgstr "Толькі DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Падобны сэрвіс ужо ёсць у гэтым спісе!"

View File

@@ -22,7 +22,7 @@ msgid "Package"
msgstr "Paket"
msgid "Type"
msgstr "Model"
msgstr "Typ"
msgid "Picon"
msgstr "Picon"
@@ -204,8 +204,8 @@ msgstr "Aktueller Datenpfad:"
msgid "Data:"
msgstr "Daten:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Enigma2 Kanal- und Satellitenlisteneditor für GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Enigma2 Kanal- und Satellitenlisteneditor für GNU/Linux."
msgid "Host:"
msgstr "Host:"
@@ -1041,6 +1041,176 @@ 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."
msgid "Toggle display position"
msgstr "Anzeigeposition umschalten"
msgid "Alternatives"
msgstr "Alternativen"
msgid "Add alternatives"
msgstr "Alternativen hinzufügen"
msgid "DreamOS only!"
msgstr "Nur DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Ein ähnlicher Dienst ist bereits in dieser Liste enthalten!"

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 Frank Neirynck
# This file is distributed under the MIT license.
#
# Frank Neirynck <frank@insink.be>, 2018-2019.
# Frank Neirynck <frank@insink.be>, 2018-2020.
#
msgid ""
msgstr ""
@@ -54,7 +54,7 @@ msgid "Current IP:"
msgstr "IP actual:"
msgid "Assign"
msgstr "Assignar"
msgstr "Asignar"
msgid "Bouquet details"
msgstr "Detalles bouquet"
@@ -66,7 +66,7 @@ msgid "Copy"
msgstr "Copiar"
msgid "Copy reference"
msgstr "Copia de referencia"
msgstr "Copiar referencia"
msgid "Download"
msgstr "Descargar"
@@ -123,7 +123,7 @@ msgid "New"
msgstr "Nuevo"
msgid "New bouquet"
msgstr "Bouquet nuevo"
msgstr "Nuevo bouquet"
msgid "Create bouquet"
msgstr "Crear bouquet"
@@ -156,10 +156,10 @@ msgid "Picons"
msgstr "Picons"
msgid "Picons downloader"
msgstr "Descargar picons"
msgstr "Descarga de picons"
msgid "Satellites downloader"
msgstr "Descargar satélites"
msgstr "Descarga de satélites"
msgid "Remove"
msgstr "Quitar"
@@ -206,11 +206,11 @@ msgstr "Ruta de datos actual:"
msgid "Data:"
msgstr "Datos:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Editor de canales y satélites Enigma2 para GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Editor de canales y satélites Enigma2 para GNU/Linux."
msgid "Host:"
msgstr "Anfitrión:"
msgstr "Host:"
msgid "Loading data..."
msgstr "Cargando datos..."
@@ -256,7 +256,7 @@ msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Solamente gratis"
msgstr "Solamente libres"
msgid "All positions"
msgstr "Todas las posiciones"
@@ -272,10 +272,10 @@ msgid "Stop playback"
msgstr "Detener la reproducción"
msgid "Previous stream in the list"
msgstr "Anterior flujo en la lista"
msgstr "Anterior stream en la lista"
msgid "Next stream in the list"
msgstr "Siguiente flujo en la lista"
msgstr "Siguiente stream en la lista"
msgid "Toggle in fullscreen"
msgstr "Cambiar a pantalla completa"
@@ -294,7 +294,7 @@ msgid "Receive picons"
msgstr "Recibir picons"
msgid "Picons name format:"
msgstr "Picons formato nombres:"
msgstr "Formato del nombre de los picons:"
msgid "Resize:"
msgstr "Redimensionar:"
@@ -433,7 +433,7 @@ msgstr "Buscar"
# IPTV dialog
msgid "Stream data"
msgstr "Transmitir flujo"
msgstr "Transmitir stream"
# IPTV list configuration dialog
msgid "Starting values"
@@ -443,7 +443,7 @@ msgid "Reset to default"
msgstr "Restablecer a predeterminado"
msgid "IPTV streams list configuration"
msgstr "Configurar lista de flujos IPTV"
msgstr "Configurar lista de streams IPTV"
# Settings dialog
msgid "Preferences"
@@ -511,13 +511,13 @@ msgid "No m3u file is selected!"
msgstr "¡No se ha seleccionado ningún fichero m3u!"
msgid "Not implemented yet!"
msgstr "¡Aún sin implementar!"
msgstr "¡No implementado!"
msgid "The text of marker is empty, please try again!"
msgstr "¡El texto del marcador está vacío, inténtalo de nuevo!"
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!"
msgstr "¡No se ha seleccionado ningún fichero png!"
@@ -526,7 +526,7 @@ msgid "No reference is present!"
msgstr "¡Ninguna referencia presente!"
msgid "No selected item!"
msgstr "¡Ningún elemento seleccionado!"
msgstr "¡No se ha seleccionado ningún elemento!"
msgid "The task is already running!"
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!"
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
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"
msgstr "Encontrado"
msgid "unavailable streams."
msgstr "Flujos no presentes."
msgstr "Streams no presentes."
msgid "No changes required!"
msgstr "¡Ningún cambio requerido!"
msgstr "¡No se requieren cambios!"
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"
msgstr "Nueva configuración vacía"
@@ -672,7 +672,7 @@ msgid "Zap"
msgstr "Zapear"
msgid "Play stream"
msgstr "Reproducir flujo"
msgstr "Reproducir stream"
msgid "Disabled"
msgstr "Desactivado"
@@ -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)"
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"
msgstr "Exportar a m3u"
@@ -795,7 +795,7 @@ msgid "Language:"
msgstr "Idioma:"
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"
msgstr "Habilitar la barra de reproducción directa"
@@ -828,16 +828,16 @@ msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "Archivo"
msgstr "Fichero"
msgid "Picons manager"
msgstr "Picons manager"
msgstr "Gestor de picons"
msgid "Explorer"
msgstr "Explorador"
msgid "Satellite url:"
msgstr "Url Satelite:"
msgstr "Url satélite:"
msgid "Cut"
msgstr "Cortar"
@@ -867,43 +867,43 @@ msgid "IPTV tools"
msgstr "Intrumentos IPTV"
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:"
msgstr "Ruta estandar de datos:"
msgstr "Ruta estándar de datos:"
msgid "Streams record path:"
msgstr "Ruta de gravacion de stream:"
msgstr "Ruta de grabación del stream:"
msgid "Record"
msgstr "Gravar"
msgstr "Grabación"
msgid "Record:"
msgstr "Gravar:"
msgstr "Grabación:"
msgid "Record to disk:"
msgstr "Gravar en disco:"
msgstr "Grabación en disco:"
msgid "Streaming"
msgstr "Streameando"
msgstr "Streaming"
msgid "Activate transcoding"
msgstr "Activer transcodificacion"
msgstr "Activar transcodificación"
msgid "Presets:"
msgstr "Presets:"
msgstr "Preajustes:"
msgid "Video options:"
msgstr "Opciones Video:"
msgstr "Opciones deo:"
msgid "Audio options:"
msgstr "Opciones Audio:"
msgstr "Opciones audio:"
msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgstr "Códec:"
msgid "Width (px):"
msgstr "Ancho (px):"
@@ -915,10 +915,10 @@ msgid "Channels:"
msgstr "Canales:"
msgid "Sample rate (Hz):"
msgstr "Sample rate (Гц):"
msgstr "Frecuencia de muestreo (Hz):"
msgid "Play streams mode:"
msgstr "Tocar en modo streams:"
msgstr "Modo de reproducción de los streams:"
msgid "Built-in player"
msgstr "Reproductor interno"
@@ -927,40 +927,40 @@ msgid "VLC media player"
msgstr "Reproductor VLC"
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."
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!"
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!"
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!"
msgstr "Sin conexión al receptor!"
msgstr "¡Desconectado del receptor!"
msgid "Signal level"
msgstr "Nivel de señal"
msgid "Receiver info"
msgstr "Informacion sobre receptor"
msgstr "Información sobre el receptor"
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"
msgstr "Mostrar información breve como sugerencias en la lista de servicios principal"
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"
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."
msgstr "Le permite nombrar archivos de bouquet usando sus nombres."
msgstr "Permite nombrar ficheros de bouquets usando sus propios nombres."
msgid "Appearance"
msgstr "Apariencia"
@@ -969,16 +969,16 @@ msgid "Enable Themes support"
msgstr "Habilitar compatibilidad con temas"
msgid "Gtk3 Theme:"
msgstr "Тема Gtk3:"
msgstr "Теma Gtk3:"
msgid "Icon Theme:"
msgstr "Тема Icono:"
msgstr "Icono del tema:"
msgid "Gtk3 Themes and Icons:"
msgstr "Tema Gtk3 e Iconos:"
msgstr "Tema Gtk3 e iconos:"
msgid "Deleting data..."
msgstr "Borrando datos ..."
msgstr "Borrando datos..."
msgid "Download from the receiver"
msgstr "Descargar desde el receptor"
@@ -987,13 +987,13 @@ msgid "Remove all picons from the receiver"
msgstr "Eliminar todos los picons del receptor"
msgid "Service reference"
msgstr "Referencia de servicio"
msgstr "Referencia del servicio"
msgid "Enable support for"
msgstr "Habilitar soporte para"
msgid "Auto-check for updates"
msgstr "Verificación automática de actualizaciones"
msgstr "Comprobación automática de actualizaciones"
msgid "Filter services"
msgstr "Filtrar servicios"
@@ -1005,23 +1005,23 @@ msgid "Destination:"
msgstr "Destino:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTAL!"
msgstr "¡EXPERIMENTAL!"
msgid "Sorting data..."
msgstr "Ordenando datos..."
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?"
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"
msgstr "Retirar del receptor"
msgstr "Eliminar del receptor"
msgid "Screenshot"
msgstr "Captura de pantalla"
msgid "Video"
msgstr "Vidео"
msgstr "Vídео"

View File

@@ -201,8 +201,8 @@ msgstr "Huidig datapad:"
msgid "Data:"
msgstr "Data:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Enigma2 kanaal and satelliet lijst editor voor GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Enigma2 kanaal and satelliet lijst editor voor GNU/Linux."
msgid "Host:"
msgstr "Host:"

View File

@@ -206,8 +206,8 @@ msgstr "Aktualna ścieżka danych:"
msgid "Data:"
msgstr "Dane:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Edytor kanałów Enigma2 i listy satelitów dla GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Edytor kanałów Enigma2 i listy satelitów dla GNU/Linux."
msgid "Host:"
msgstr "Host:"

View File

@@ -201,8 +201,8 @@ msgstr "Rota de dados atual:"
msgid "Data:"
msgstr "Dados:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Editor de Canais e Satélites Enigma2 para GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Editor de Canais e Satélites Enigma2 para GNU/Linux."
msgid "Host:"
msgstr "Anfitrião:"

View File

@@ -203,8 +203,8 @@ msgstr "Текущий путь к данным:"
msgid "Data:"
msgstr "Данные:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux."
msgid "Host:"
msgstr "Адрес ресивера:"
@@ -1028,3 +1028,173 @@ 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 "Toggle display position"
msgstr "Переключить позицию отображения"
msgid "Alternatives"
msgstr "Альтернативы"
msgid "Add alternatives"
msgstr "Добавить альтернативы"
msgid "DreamOS only!"
msgstr "Только DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Подобный сервис уже есть в этом списке!"

View File

@@ -203,8 +203,8 @@ msgstr "Mevcut veri yolu:"
msgid "Data:"
msgstr "Veri:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü."
msgid "Host:"
msgstr "Ana bilgisayar:"