mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-09 05:16:07 +02:00
Compare commits
341 Commits
1.0.0-macO
...
1.0.10-b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8445d55a | ||
|
|
8153c5e6d6 | ||
|
|
51fd013e0f | ||
|
|
872f3f0f81 | ||
|
|
95aa8aaed6 | ||
|
|
b18207b376 | ||
|
|
8f840ed3f5 | ||
|
|
6841090cc0 | ||
|
|
4e700ca1e8 | ||
|
|
3312576416 | ||
|
|
b047153591 | ||
|
|
b15cae8d79 | ||
|
|
c5e8e19941 | ||
|
|
50d8e3365e | ||
|
|
42fd490a33 | ||
|
|
ef84a45ebb | ||
|
|
d924643436 | ||
|
|
a7bde2e25f | ||
|
|
f64d9d31ab | ||
|
|
ec4ebfa24a | ||
|
|
03ba1af356 | ||
|
|
d231edd0bb | ||
|
|
97fef050b7 | ||
|
|
61fe46da8a | ||
|
|
dc7dc9a087 | ||
|
|
e27c34df4e | ||
|
|
a3b6d37fd3 | ||
|
|
d01af855f9 | ||
|
|
5353af94ac | ||
|
|
2656d0b3a9 | ||
|
|
a99c1e00b9 | ||
|
|
73a1f2ccef | ||
|
|
a4892efe61 | ||
|
|
5ded562e12 | ||
|
|
d2508218f7 | ||
|
|
1e10df6f03 | ||
|
|
06887d9440 | ||
|
|
3e9776e8e0 | ||
|
|
6b2c4dc79f | ||
|
|
726abc4965 | ||
|
|
6450cf1b17 | ||
|
|
2aaa196acf | ||
|
|
4d35364a30 | ||
|
|
f1c5a57abd | ||
|
|
dada57141f | ||
|
|
d45d1d42c2 | ||
|
|
9ee757aa19 | ||
|
|
17de98991d | ||
|
|
2862f22243 | ||
|
|
9c2ab5c3e6 | ||
|
|
4d6fbfce6c | ||
|
|
5c0d1d27f5 | ||
|
|
1c26887e8f | ||
|
|
2d67fbfe44 | ||
|
|
f502bce2aa | ||
|
|
8c442280ec | ||
|
|
3b79677a9b | ||
|
|
33529ca010 | ||
|
|
eae14a472f | ||
|
|
f85bbceceb | ||
|
|
f7cfc5a9a0 | ||
|
|
6b57c8f617 | ||
|
|
1006251490 | ||
|
|
e871492006 | ||
|
|
353fb9cc53 | ||
|
|
497964822e | ||
|
|
a067821d46 | ||
|
|
ab52964a20 | ||
|
|
344ac43996 | ||
|
|
0e07b8d492 | ||
|
|
7580a448c8 | ||
|
|
4886d2dfcf | ||
|
|
2dfc571a64 | ||
|
|
7922f368b5 | ||
|
|
d87c878d4d | ||
|
|
564adf1c61 | ||
|
|
9057dc1f81 | ||
|
|
108b3a72b9 | ||
|
|
0090cc6262 | ||
|
|
acc946eb30 | ||
|
|
b1dffe622d | ||
|
|
ab008aeb9c | ||
|
|
c4b6e009d5 | ||
|
|
7164c6debd | ||
|
|
fb06e4364f | ||
|
|
7ff8212fa6 | ||
|
|
9822129307 | ||
|
|
876aaeb4fe | ||
|
|
b8140c4df0 | ||
|
|
a364ff22bf | ||
|
|
bde2abf2e4 | ||
|
|
42644e37c6 | ||
|
|
9016143bb9 | ||
|
|
45cc506dd0 | ||
|
|
c6a841de76 | ||
|
|
d95ec0af17 | ||
|
|
77c159fc1a | ||
|
|
95508c3015 | ||
|
|
1f6e6b9812 | ||
|
|
20697a08f2 | ||
|
|
95c7213988 | ||
|
|
a988ea6e68 | ||
|
|
061e42a36e | ||
|
|
3a5473ca03 | ||
|
|
54a5be9c3d | ||
|
|
858c64d531 | ||
|
|
f05802841d | ||
|
|
e4af45b0e0 | ||
|
|
0cad69cb00 | ||
|
|
071fffb303 | ||
|
|
95d51d37e9 | ||
|
|
f1935e4a56 | ||
|
|
96aeb05dd1 | ||
|
|
af1c3c3ca6 | ||
|
|
2ba37f1506 | ||
|
|
8581de5a10 | ||
|
|
155f3b254a | ||
|
|
13e6c858d2 | ||
|
|
7fac48c44d | ||
|
|
9aa2ec191e | ||
|
|
13e12165eb | ||
|
|
ae37feebaa | ||
|
|
573bcba32f | ||
|
|
f4e5c56d39 | ||
|
|
4dc0dffc1f | ||
|
|
ceb329700d | ||
|
|
8cca2da48d | ||
|
|
e9bab4ebb7 | ||
|
|
3e7750e4ef | ||
|
|
184c1cbee5 | ||
|
|
c9e8625ea5 | ||
|
|
fd72615a63 | ||
|
|
f11a40e04d | ||
|
|
9522267dee | ||
|
|
f538d609b9 | ||
|
|
3edc699277 | ||
|
|
19562c7281 | ||
|
|
5a104c1778 | ||
|
|
09afc5b76a | ||
|
|
6129c5ac23 | ||
|
|
a552d13933 | ||
|
|
c4fa3b1eee | ||
|
|
795c708a61 | ||
|
|
92721c65e4 | ||
|
|
b8470f7dfa | ||
|
|
fd02ddedb7 | ||
|
|
f3beec141c | ||
|
|
8910b6d68b | ||
|
|
2a4c4576cc | ||
|
|
4e8fd693a6 | ||
|
|
7184642e68 | ||
|
|
10f4a461bd | ||
|
|
bec7f35b17 | ||
|
|
2a6dc3f167 | ||
|
|
e9e36c4a9c | ||
|
|
0c49096733 | ||
|
|
e2aa21060b | ||
|
|
b15691207b | ||
|
|
ab3ca134a7 | ||
|
|
7f839f3fa0 | ||
|
|
c3e8eac4b9 | ||
|
|
04f808843d | ||
|
|
eea4a68993 | ||
|
|
bfba5b5237 | ||
|
|
e203a38966 | ||
|
|
a3b5609138 | ||
|
|
b3c131b753 | ||
|
|
a8ea5ad974 | ||
|
|
831184af2e | ||
|
|
1f51766dea | ||
|
|
f8f1536213 | ||
|
|
0accfbd3d1 | ||
|
|
ee462b24f7 | ||
|
|
17ee189db8 | ||
|
|
8389293b4b | ||
|
|
99e0c79b6c | ||
|
|
02cdbc4e56 | ||
|
|
d57f0490d2 | ||
|
|
9680347180 | ||
|
|
7fbdc32f91 | ||
|
|
a69f54435d | ||
|
|
0d47433f80 | ||
|
|
d16a8e44f6 | ||
|
|
053a834d6d | ||
|
|
62c1ef852c | ||
|
|
b5234c55e8 | ||
|
|
472ebba8e9 | ||
|
|
d427cf66b0 | ||
|
|
9fe3d8077f | ||
|
|
2f12ef7bdd | ||
|
|
b6ad661e39 | ||
|
|
6355e0d75a | ||
|
|
29016056c2 | ||
|
|
23fe71e5cc | ||
|
|
80e4edd084 | ||
|
|
3d0bb6ad3c | ||
|
|
a8937d0698 | ||
|
|
b97997b7a0 | ||
|
|
0003c6c5d5 | ||
|
|
13b9d64bd0 | ||
|
|
c68511b223 | ||
|
|
c6ef61222e | ||
|
|
8309353784 | ||
|
|
3f65975ac2 | ||
|
|
4f6443e6e3 | ||
|
|
d517b3b9d6 | ||
|
|
faf228fa6f | ||
|
|
eaf5e39458 | ||
|
|
8a2539d57b | ||
|
|
8c827b126f | ||
|
|
db1bfb0fb9 | ||
|
|
8ba7751b97 | ||
|
|
65b58c9d08 | ||
|
|
72aed5ff6e | ||
|
|
ad07469c35 | ||
|
|
6f0de03b22 | ||
|
|
1cec96d2b5 | ||
|
|
a9935dd0a7 | ||
|
|
65dfd6c1c4 | ||
|
|
65c24a324a | ||
|
|
78b0cc1517 | ||
|
|
d66d4e6402 | ||
|
|
7b11822664 | ||
|
|
8c5f27cc8a | ||
|
|
b51c8a9aed | ||
|
|
2c4302f57d | ||
|
|
bd0ac077f9 | ||
|
|
105b907392 | ||
|
|
b4fb684af4 | ||
|
|
20a1bac22e | ||
|
|
9a9229f67c | ||
|
|
a941c96c61 | ||
|
|
05a6e36589 | ||
|
|
50a5cf6fc3 | ||
|
|
6ef844157e | ||
|
|
72583ba879 | ||
|
|
e7d96b0cbb | ||
|
|
a7458494a3 | ||
|
|
747c1a9722 | ||
|
|
dac2fe17a6 | ||
|
|
fb8cf6c882 | ||
|
|
bcf69231a8 | ||
|
|
5352d87b82 | ||
|
|
c978f5abab | ||
|
|
636442bcd3 | ||
|
|
9e74f0f525 | ||
|
|
b7028ae27d | ||
|
|
4f3b05ede5 | ||
|
|
e6e6d3510d | ||
|
|
1d1b4acdca | ||
|
|
0e50f1952d | ||
|
|
8686e15446 | ||
|
|
5fee00a150 | ||
|
|
0ec9873940 | ||
|
|
0600737319 | ||
|
|
cf51a5bfd8 | ||
|
|
533d8fae25 | ||
|
|
16167b1b13 | ||
|
|
38f6c06292 | ||
|
|
4170864a74 | ||
|
|
3171540bf2 | ||
|
|
0ed4f6d7f9 | ||
|
|
637becaa59 | ||
|
|
fd5a0d23a8 | ||
|
|
22626ea03d | ||
|
|
2e34568f74 | ||
|
|
f42b2f3c75 | ||
|
|
f88b36cee7 | ||
|
|
4421f767e1 | ||
|
|
898b32ca5f | ||
|
|
7c2840c570 | ||
|
|
1c9c58d48a | ||
|
|
45a1c79808 | ||
|
|
19fbc753c5 | ||
|
|
c8c424750b | ||
|
|
3d769a5e18 | ||
|
|
1b7d2f15b0 | ||
|
|
58cf299097 | ||
|
|
af8fe227e0 | ||
|
|
3160ec2455 | ||
|
|
04fd8b7182 | ||
|
|
ee90d11557 | ||
|
|
b312816804 | ||
|
|
39647cb811 | ||
|
|
e64cd66977 | ||
|
|
754c586fb1 | ||
|
|
d5a2b22819 | ||
|
|
b20bcce5fa | ||
|
|
aba82c7120 | ||
|
|
e0beeef2a3 | ||
|
|
c8a9d3f4a0 | ||
|
|
9f7c713712 | ||
|
|
f89196041b | ||
|
|
efa2d94239 | ||
|
|
f53f483dce | ||
|
|
ebcf0a90b5 | ||
|
|
2311b046e7 | ||
|
|
9544b6028f | ||
|
|
e04144b10f | ||
|
|
93f68a7fe2 | ||
|
|
947ea21ed1 | ||
|
|
d20d40c19b | ||
|
|
bb349a3fe9 | ||
|
|
bb3ecf975b | ||
|
|
bbc693e5e6 | ||
|
|
cb29cf0155 | ||
|
|
0c8592fb0d | ||
|
|
66f067340d | ||
|
|
b0ec8e5483 | ||
|
|
aa4b31edfc | ||
|
|
3d627b57a4 | ||
|
|
6b1bec500c | ||
|
|
7444db7e21 | ||
|
|
8be92a9c7e | ||
|
|
7554f40c6a | ||
|
|
17ab321e44 | ||
|
|
ac345d4ef3 | ||
|
|
e2a56a316d | ||
|
|
9b79bf2b81 | ||
|
|
ee2a9bda90 | ||
|
|
da0c5fa8a6 | ||
|
|
e202ec6abe | ||
|
|
e87be79f42 | ||
|
|
6372ac474c | ||
|
|
c6b0f70c8e | ||
|
|
6a921ad394 | ||
|
|
4c8743517f | ||
|
|
74ec0fe956 | ||
|
|
041f717a01 | ||
|
|
67dbdb19d7 | ||
|
|
de49179dd2 | ||
|
|
2723d255fe | ||
|
|
4515b2538b | ||
|
|
88e3a22cf0 | ||
|
|
7ac63b81c0 | ||
|
|
234611b686 | ||
|
|
fdb2691430 | ||
|
|
d81700c30c | ||
|
|
e91c4c33a5 | ||
|
|
40bf54e94f | ||
|
|
4a50c36ab4 |
@@ -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
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2019 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
109
README.md
109
README.md
@@ -1,27 +1,41 @@
|
||||
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
|
||||
[](LICENSE) 
|
||||
### Enigma2 channel and satellite list editor for GNU/Linux.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118884719-8277e980-b8ff-11eb-8621-c8c4afd6181b.png" width="560"/>](https://user-images.githubusercontent.com/7511379/118884719-8277e980-b8ff-11eb-8621-c8c4afd6181b.png)
|
||||
|
||||
## 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.
|
||||
* Import function.
|
||||
* Backup function.
|
||||
## Main features of the program
|
||||
* Editing bouquets, channels, satellites.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118884747-8ad02480-b8ff-11eb-9104-8cf8fb6e785d.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118884747-8ad02480-b8ff-11eb-9104-8cf8fb6e785d.png)
|
||||
* Import function.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118526825-4dc23180-b749-11eb-8197-e9bbccbc3bdf.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118526825-4dc23180-b749-11eb-8197-e9bbccbc3bdf.png)
|
||||
* Backup function.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118528402-f58c2f00-b74a-11eb-9b84-edf220526e6e.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118528402-f58c2f00-b74a-11eb-9b84-edf220526e6e.png)
|
||||
* Support of picons.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118526864-5c104d80-b749-11eb-8497-6e8c78542ab1.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118526864-5c104d80-b749-11eb-8497-6e8c78542ab1.png)
|
||||
* Importing services, downloading picons and updating satellites from the Web.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118530243-1a81a180-b74d-11eb-8e01-aea904d954af.png" width="250"/>](https://user-images.githubusercontent.com/7511379/118530243-1a81a180-b74d-11eb-8e01-aea904d954af.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118526706-31be9000-b749-11eb-9956-c4bf2e13f968.png" width="292"/>](https://user-images.githubusercontent.com/7511379/118526706-31be9000-b749-11eb-9956-c4bf2e13f968.png)
|
||||
* Extended support of IPTV.
|
||||
* Support of picons.
|
||||
* Downloading of picons and updating of satellites (transponders) from the Internet.
|
||||
* 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:
|
||||
* **Ctrl + Insert** - copies the selected channels from the main list to the the bouquet beginning
|
||||
or inserts (creates) a new bouquet.
|
||||
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
|
||||
* **Ctrl + X** - only in bouquet list. **Ctrl + C** - only in services list.
|
||||
Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
* Assignment of EPG from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
* Preview (playback) of IPTV or other streams directly from the bouquet list.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118884891-b3f0b500-b8ff-11eb-8717-3588d6e089de.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118884891-b3f0b500-b8ff-11eb-8717-3588d6e089de.png)
|
||||
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118886284-66754780-b901-11eb-9068-29b5a607ccaf.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118886284-66754780-b901-11eb-9068-29b5a607ccaf.png)
|
||||
* Simple FTP client (experimental).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png)
|
||||
|
||||
#### Keyboard shortcuts
|
||||
* **Ctrl + X** - only in bouquet list.
|
||||
* **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 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.
|
||||
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
|
||||
@@ -29,33 +43,52 @@ 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.
|
||||
|
||||
### 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```
|
||||
* **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.
|
||||
|
||||
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
|
||||
|
||||
## Minimum requirements
|
||||
*Python >= 3.5.2, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
|
||||
|
||||
***Optional:** 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.
|
||||
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
|
||||
* ### macOS
|
||||
**This program can be run on macOS.** To work in this OS, you must use a [separate branch](https://github.com/DYefremov/DemonEditor/tree/experimental-mac). A ready-made package can be downloaded from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
**The functionality and performance of this version may be different from the Linux version!**
|
||||
|
||||
### Important:
|
||||
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!
|
||||
## Important
|
||||
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
|
||||
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2.
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
|
||||
|
||||
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.
|
||||
|
||||
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.
|
||||
#### Command line arguments:
|
||||
* **-l** - write logs to file.
|
||||
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
|
||||
|
||||
## License
|
||||
Licensed under the [MIT](LICENSE) license.
|
||||
|
||||
@@ -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
|
||||
@@ -6,28 +6,28 @@ from gi.repository import GLib
|
||||
|
||||
_LOG_FILE = "demon-editor.log"
|
||||
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
|
||||
_LOGGER_NAME = "main_logger"
|
||||
_USE_LOG = False
|
||||
_LOGGER_NAME = None
|
||||
|
||||
|
||||
def init_logger():
|
||||
global _USE_LOG
|
||||
_USE_LOG = True
|
||||
global _LOGGER_NAME
|
||||
_LOGGER_NAME = "main_logger"
|
||||
logging.Logger(_LOGGER_NAME)
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s %(message)s",
|
||||
datefmt=_DATE_FORMAT,
|
||||
handlers=[logging.FileHandler(_LOG_FILE),
|
||||
logging.StreamHandler()])
|
||||
handlers=[logging.FileHandler(_LOG_FILE), logging.StreamHandler()])
|
||||
log("Logging is enabled.", level=logging.INFO)
|
||||
|
||||
|
||||
def get_logger():
|
||||
return logging.getLogger(_LOGGER_NAME)
|
||||
|
||||
|
||||
def log(message, level=logging.ERROR):
|
||||
get_logger().log(level, message) if _USE_LOG else print(message)
|
||||
def log(message, level=logging.ERROR, debug=False, fmt_message="{}"):
|
||||
""" The main logging function. """
|
||||
logger = logging.getLogger(_LOGGER_NAME)
|
||||
if debug:
|
||||
from traceback import format_exc
|
||||
logger.log(level, fmt_message.format(format_exc()))
|
||||
else:
|
||||
logger.log(level, message)
|
||||
|
||||
|
||||
def run_idle(func):
|
||||
|
||||
@@ -5,24 +5,25 @@ 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
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, \
|
||||
build_opener, install_opener, Request
|
||||
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
|
||||
install_opener, Request)
|
||||
|
||||
from app.commons import log, run_task
|
||||
from app.settings import SettingsType
|
||||
|
||||
_BQ_FILES_LIST = ("tv", "radio", # enigma 2
|
||||
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
BQ_FILES_LIST = ("tv", "radio", # enigma 2
|
||||
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
|
||||
_DATA_FILES_LIST = ("lamedb", "lamedb5", "services.xml", "blacklist", "whitelist",)
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
|
||||
_SAT_XML_FILE = "satellites.xml"
|
||||
_WEBTV_XML_FILE = "webtv.xml"
|
||||
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
|
||||
WEB_TV_XML_FILE = ("webtv.xml",)
|
||||
PICONS_SUF = (".jpg", ".png")
|
||||
|
||||
|
||||
class DownloadType(Enum):
|
||||
@@ -34,18 +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"
|
||||
PLAY = "mediaplayerplay?file=4097:0:1:0:0:0:0:0:0:0:"
|
||||
TEST = None
|
||||
TOKEN = "session"
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
pass
|
||||
|
||||
@@ -54,55 +43,324 @@ class HttpApiException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def download_data(*, settings, download_type=DownloadType.ALL, callback=print):
|
||||
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
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
files = []
|
||||
# bouquets
|
||||
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
|
||||
ftp.cwd(settings.services_path)
|
||||
ftp.dir(files.append)
|
||||
file_list = _BQ_FILES_LIST + _DATA_FILES_LIST if download_type is DownloadType.ALL else _BQ_FILES_LIST
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith(file_list):
|
||||
name = name.split()[-1]
|
||||
download_file(ftp, name, save_path, callback)
|
||||
# satellites.xml and webtv
|
||||
if download_type in (DownloadType.ALL, DownloadType.SATELLITES, DownloadType.WEBTV):
|
||||
ftp.cwd(settings.satellites_xml_path)
|
||||
files.clear()
|
||||
ftp.dir(files.append)
|
||||
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
|
||||
ftp.download_files(save_path, file_list, callback)
|
||||
# *.xml and webtv
|
||||
if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
|
||||
if download_type in (DownloadType.ALL, DownloadType.WEBTV):
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if download_type in (DownloadType.ALL, DownloadType.SATELLITES) and name.endswith(_SAT_XML_FILE):
|
||||
download_file(ftp, _SAT_XML_FILE, save_path, callback)
|
||||
if download_type in (DownloadType.ALL, DownloadType.WEBTV) and name.endswith(_WEBTV_XML_FILE):
|
||||
download_file(ftp, _WEBTV_XML_FILE, save_path, callback)
|
||||
if download_type is DownloadType.PICONS:
|
||||
picons_path = settings.picons_local_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
|
||||
# epg.dat
|
||||
if download_type is DownloadType.EPG:
|
||||
stb_path = settings.services_path
|
||||
epg_options = settings.epg_options
|
||||
if epg_options:
|
||||
stb_path = epg_options.epg_dat_stb_path or stb_path
|
||||
save_path = epg_options.epg_dat_path or save_path
|
||||
stb_path = epg_options.get("epg_dat_stb_path", stb_path)
|
||||
save_path = epg_options.get("epg_dat_path", save_path)
|
||||
|
||||
ftp.cwd(stb_path)
|
||||
ftp.dir(files.append)
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith("epg.dat"):
|
||||
name = name.split()[-1]
|
||||
download_file(ftp, name, 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):
|
||||
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
|
||||
@@ -112,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:
|
||||
@@ -121,6 +379,8 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
message = "All user data will be reloaded!"
|
||||
elif download_type is DownloadType.SATELLITES:
|
||||
message = "Satellites.xml file will be updated!"
|
||||
elif download_type is DownloadType.PICONS:
|
||||
message = "Picons will be updated!"
|
||||
|
||||
params = urlencode({"text": message, "type": 2, "timeout": 5})
|
||||
ht.send((url + "message?{}".format(params), "Sending info message... "))
|
||||
@@ -130,46 +390,50 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
|
||||
time.sleep(2)
|
||||
else:
|
||||
# telnet
|
||||
tn = telnet(host=host,
|
||||
user=settings.telnet_user,
|
||||
password=settings.telnet_password,
|
||||
timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# terminate enigma or neutrino
|
||||
tn.send("init 4")
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# telnet
|
||||
tn = telnet(host=host,
|
||||
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, _SAT_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, _WEBTV_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, _SAT_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, _WEBTV_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)
|
||||
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."))
|
||||
@@ -186,65 +450,19 @@ 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)
|
||||
# ***************** Picons *******************#
|
||||
|
||||
def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
ftp.delete_picons(callback, settings.picons_path, files_filter)
|
||||
if done_callback:
|
||||
done_callback()
|
||||
|
||||
|
||||
def upload_files(ftp, data_path, file_list, callback):
|
||||
for file_name in os.listdir(data_path):
|
||||
if file_name == _SAT_XML_FILE or file_name == _WEBTV_XML_FILE:
|
||||
continue
|
||||
if file_name.endswith(file_list):
|
||||
send_file(file_name, data_path, ftp, callback)
|
||||
|
||||
|
||||
def remove_unused_bouquets(ftp, callback):
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith(("tv", "radio", "bouquets.xml", "ubouquets.xml")):
|
||||
name = name.split()[-1]
|
||||
callback("Deleting file: {}. Status: {}\n".format(name, ftp.delete(name)))
|
||||
|
||||
|
||||
def upload_xml(ftp, data_path, xml_path, xml_file, callback):
|
||||
""" Used for transfer satellites.xml or webtv.xml files """
|
||||
ftp.cwd(xml_path)
|
||||
send_file(xml_file, data_path, ftp, callback)
|
||||
|
||||
|
||||
def upload_picons(ftp, src, dest, callback):
|
||||
try:
|
||||
ftp.cwd(dest)
|
||||
except error_perm as e:
|
||||
if str(e).startswith("550"):
|
||||
ftp.mkd(dest) # if not exist
|
||||
ftp.cwd(dest)
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
picons_suf = (".jpg", ".png")
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith(picons_suf):
|
||||
name = name.split()[-1]
|
||||
ftp.delete(name)
|
||||
for file_name in os.listdir(src):
|
||||
if file_name.endswith(picons_suf):
|
||||
send_file(file_name, src, ftp, callback)
|
||||
|
||||
|
||||
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 """
|
||||
with open(path + file_name, "rb") as f:
|
||||
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
|
||||
def picons_filter_function(files_filter=None):
|
||||
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
|
||||
|
||||
|
||||
def http(user, password, url, callback, use_ssl=False):
|
||||
@@ -253,7 +471,7 @@ def http(user, password, url, callback, use_ssl=False):
|
||||
|
||||
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 ""))
|
||||
|
||||
|
||||
@@ -266,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"))
|
||||
@@ -287,43 +505,136 @@ def telnet(host, port=23, user="", password="", timeout=5):
|
||||
class HttpAPI:
|
||||
__MAX_WORKERS = 4
|
||||
|
||||
def __init__(self, settings):
|
||||
self._settings = settings
|
||||
self._shutdown = False
|
||||
self._session_id = 0
|
||||
self._base_url = None
|
||||
self._data = None
|
||||
self.init()
|
||||
class Request(Enum):
|
||||
ZAP = "zap?sRef="
|
||||
INFO = "about"
|
||||
SIGNAL = "signal"
|
||||
STREAM = "stream.m3u?ref="
|
||||
STREAM_TS = "ts.m3u?file="
|
||||
STREAM_CURRENT = "streamcurrent.m3u"
|
||||
CURRENT = "getcurrent"
|
||||
TEST = None
|
||||
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"
|
||||
# Recordings
|
||||
RECORDINGS = "movielist?dirname="
|
||||
REC_DIRS = "getlocations"
|
||||
REC_CURRENT = "getcurrlocation"
|
||||
# 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"
|
||||
|
||||
PARAM_REQUESTS = {Request.REMOTE,
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
Request.EPG,
|
||||
Request.TIMER,
|
||||
Request.RECORDINGS}
|
||||
|
||||
STREAM_REQUESTS = {Request.STREAM,
|
||||
Request.STREAM_CURRENT,
|
||||
Request.STREAM_TS}
|
||||
|
||||
def __init__(self, settings):
|
||||
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
|
||||
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
|
||||
|
||||
def send(self, req_type, ref, callback=print):
|
||||
self._settings = settings
|
||||
self._shutdown = False
|
||||
self._session_id = 0
|
||||
self._main_url = None
|
||||
self._base_url = None
|
||||
self._data = None
|
||||
self._is_owif = True
|
||||
self.init()
|
||||
|
||||
def send(self, req_type, ref, callback=print, ref_prefix=""):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
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 in self.STREAM_REQUESTS:
|
||||
url += urllib.parse.quote(ref)
|
||||
elif req_type is HttpRequestType.PLAY:
|
||||
url += urllib.parse.quote(ref).replace("%3A", "%253A")
|
||||
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 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.PARAM_REQUESTS:
|
||||
url += ref
|
||||
|
||||
future = self._executor.submit(get_response, req_type, url, self._data)
|
||||
future.add_done_callback(lambda f: callback(f.result()))
|
||||
def done_callback(f):
|
||||
callback(f.result())
|
||||
|
||||
future = self._executor.submit(get_response, req_type, url, data)
|
||||
future.add_done_callback(done_callback)
|
||||
|
||||
@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
|
||||
url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
|
||||
self._base_url = "{}/web/".format(url)
|
||||
init_auth(user, password, url, use_ssl)
|
||||
url = "{}/web/{}".format(url, HttpRequestType.TOKEN.value)
|
||||
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
|
||||
self._base_url = "{}/web/".format(self._main_url)
|
||||
init_auth(user, password, self._main_url, use_ssl)
|
||||
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
|
||||
s_id = get_session_id(user, password, url)
|
||||
if s_id != "0":
|
||||
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
|
||||
self.send(self.Request.INFO, None, self.init_callback)
|
||||
|
||||
def init_callback(self, info):
|
||||
if info:
|
||||
version = info.get("e2webifversion", "").upper()
|
||||
self._is_owif = "OWIF" in version
|
||||
version_info = "Web Interface version: {}".format(version) if version else ""
|
||||
log("HTTP API initialized... {}".format(version_info))
|
||||
|
||||
@property
|
||||
def is_owif(self):
|
||||
""" Returns true if the web interface is OpenWebif. """
|
||||
return self._is_owif
|
||||
|
||||
@run_task
|
||||
def close(self):
|
||||
self._shutdown = True
|
||||
@@ -333,19 +644,35 @@ 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:
|
||||
return f.read().decode("utf-8")
|
||||
elif req_type is HttpRequestType.CURRENT:
|
||||
if req_type in HttpAPI.STREAM_REQUESTS:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
elif req_type is HttpAPI.Request.GRUB:
|
||||
return {"img_data": f.read()}
|
||||
elif req_type is HttpAPI.Request.CURRENT:
|
||||
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
|
||||
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
|
||||
elif req_type is HttpAPI.Request.PLAYER_LIST:
|
||||
return [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
|
||||
elif req_type is HttpAPI.Request.EPG:
|
||||
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
|
||||
elif req_type is HttpAPI.Request.TIMER_LIST:
|
||||
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
|
||||
elif req_type is HttpAPI.Request.REC_DIRS:
|
||||
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
|
||||
elif req_type is HttpAPI.Request.RECORDINGS:
|
||||
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
|
||||
else:
|
||||
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
|
||||
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))
|
||||
@@ -372,11 +699,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")
|
||||
@@ -402,7 +729,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)
|
||||
|
||||
|
||||
@@ -1,8 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
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,15 +54,24 @@ 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)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_bouquets(path, bouquets, s_type):
|
||||
def write_bouquet(path, bq, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
write_enigma_bouquets(path, bouquets)
|
||||
writer = BouquetsWriter(path, None)
|
||||
writer.write_bouquet(path + "userbouquet.{}.{}".format(bq.name, bq.type), bq.name, bq.services)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
from .neutrino.bouquets import write_bouquet
|
||||
write_bouquet(path, bq)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
BouquetsWriter(path, bouquets, force_bq_names).write()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
write_neutrino_bouquets(path, bouquets)
|
||||
|
||||
|
||||
@@ -13,9 +13,12 @@ class BqServiceType(Enum):
|
||||
DEFAULT = "DEFAULT"
|
||||
IPTV = "IPTV"
|
||||
MARKER = "MARKER" # 64
|
||||
SPACE = "SPACE" # 832 [hidden marker]
|
||||
ALT = "ALT" # Service with alternatives
|
||||
|
||||
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden", "file"])
|
||||
Bouquet.__new__.__defaults__ = (None, BqServiceType.DEFAULT, [], None, None, None) # For Python3 < 3.7
|
||||
Bouquets = namedtuple("Bouquets", ["name", "type", "bouquets"])
|
||||
BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
|
||||
|
||||
@@ -32,6 +35,7 @@ class TrType(Enum):
|
||||
Satellite = "s"
|
||||
Terrestrial = "t"
|
||||
Cable = "c"
|
||||
ATSC = "a"
|
||||
|
||||
|
||||
class BqType(Enum):
|
||||
@@ -135,7 +139,7 @@ TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5
|
||||
GUARD_INTERVAL = {"0": "1/32", "1": "1/16", "2": "1/8", "3": "1/4", "4": "Auto", "5": "1/128", "6": "19/128",
|
||||
"7": "19/256"}
|
||||
|
||||
HIERARCHY = {"0": "None", "1": "1", "2": "2", "3": "4", "4": "Auto"}
|
||||
HIERARCHY = {"0": "None", "1": "1", "2": "2", "3": "4", "4": "Auto"}
|
||||
|
||||
T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto", "6": "6/7", "7": "8/9"}
|
||||
|
||||
@@ -144,11 +148,13 @@ T_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
|
||||
# Cable
|
||||
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
|
||||
|
||||
# ATSC
|
||||
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
|
||||
"7": "16VSB"}
|
||||
|
||||
# CAS
|
||||
CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax", "C:0baa": "Conax", "C:0602": "Irdeto",
|
||||
"C:0604": "Irdeto", "C:0606": "Irdeto", "C:0608": "Irdeto", "C:0622": "Irdeto", "C:0626": "Irdeto",
|
||||
"C:0664": "Irdeto", "C:0614": "Irdeto", "C:0692": "Irdeto", "C:1801": "Nagravision", "C:0500": "Viaccess",
|
||||
"C:0E00": "PowerVu", "C:4ae0": "DRE-Crypt", "C:4ae1": "DRE-Crypt", "C:7be1": "DRE-Crypt"}
|
||||
CAS = {"C:26": "BISS", "C:0B": "Conax", "C:06": "Irdeto", "C:18": "Nagravision", "C:05": "Viaccess", "C:01": "SECA",
|
||||
"C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard"}
|
||||
|
||||
# 'on' attribute 0070(hex) = 112(int) = ONID(ONID-TID on www.lyngsat.com)
|
||||
PROVIDER = {112: "HTB+", 253: "Tricolor TV"}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,66 +1,233 @@
|
||||
""" Module for parsing bouquets """
|
||||
import re
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with Enigma2 bouquets. """
|
||||
import re
|
||||
from collections import Counter
|
||||
from 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)
|
||||
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!
|
||||
"""
|
||||
_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]"
|
||||
|
||||
def write_bouquets(path, bouquets):
|
||||
srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
line = []
|
||||
pattern = re.compile("[^\\w_()]+")
|
||||
current_marker = [0]
|
||||
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 = 1
|
||||
self._space_index = 0
|
||||
self._alt_names = set()
|
||||
|
||||
for bqs in bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
def write(self):
|
||||
line = []
|
||||
pattern = re.compile("[^\\w_()]+")
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.name
|
||||
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
|
||||
bq_name = _DEFAULT_BOUQUET_NAME
|
||||
for bqs in self._bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
bq_file_names = {b.file for b in bqs.bouquets}
|
||||
count = 1
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.file
|
||||
if not bq_name:
|
||||
if self._force_bq_names:
|
||||
bq_name = re.sub(pattern, "_", bq.name)
|
||||
else:
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
while bq_name in bq_file_names:
|
||||
count += 1
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
|
||||
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
|
||||
|
||||
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)
|
||||
alt_name = srv.data_id
|
||||
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
|
||||
|
||||
if self._force_bq_names:
|
||||
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
|
||||
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
|
||||
|
||||
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)
|
||||
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, current_marker)
|
||||
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):
|
||||
bouquet = ["#NAME {}\n".format(name)]
|
||||
marker = "#SERVICE 1:64:{:X}:0: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:
|
||||
if srv.service_type == BqServiceType.IPTV.name:
|
||||
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
|
||||
elif srv.service_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]))
|
||||
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))
|
||||
__slots__ = ["_path"]
|
||||
|
||||
with open(path, "w", encoding="utf-8") as file:
|
||||
file.writelines(bouquet)
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
|
||||
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, b_name))
|
||||
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(":")
|
||||
data_len = len(srv_data)
|
||||
if data_len < 10:
|
||||
log("The bouquet [{}] service [{}] has the wrong data format: [{}]".format(bq_name, num, srv))
|
||||
continue
|
||||
|
||||
s_type = 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, alt_name, 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 data_len == 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):
|
||||
""" Creates bouquet channel id """
|
||||
""" Creates bouquet channel id. """
|
||||
data_type = srv.data_id
|
||||
if data_type and len(data_type) > 4:
|
||||
data_type = int(srv.data_id.split(":")[4])
|
||||
@@ -68,55 +235,5 @@ def to_bouquet_id(srv):
|
||||
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
|
||||
|
||||
|
||||
def get_bouquet(path, name, bq_type):
|
||||
""" Parsing services ids from bouquet file """
|
||||
with open(path + "userbouquet.{}.{}".format(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 ['']
|
||||
for ch in srvs[1:]:
|
||||
ch_data = ch.strip().split(":")
|
||||
if ch_data[1] == "64":
|
||||
marker_data = ch.split("#DESCRIPTION", 1)
|
||||
services.append(BouquetService(marker_data[1].strip(), BqServiceType.MARKER, ch, ch_data[2]))
|
||||
elif "http" in ch:
|
||||
stream_data = ch.split("#DESCRIPTION", 1)
|
||||
services.append(BouquetService(stream_data[-1].strip(":").strip(), BqServiceType.IPTV, ch, 0))
|
||||
else:
|
||||
fav_id = "{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6])
|
||||
name = None
|
||||
if len(ch_data) == 12:
|
||||
name, desc = str(ch_data[-1]).split("\n#DESCRIPTION")
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), 0))
|
||||
|
||||
return srvs[0].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].*")
|
||||
|
||||
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, services = get_bouquet(path, name.group(1), bq_type)
|
||||
bouquets[2].append(Bouquet(name=b_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
|
||||
|
||||
@@ -13,271 +13,303 @@ _END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
|
||||
|
||||
|
||||
def get_services(path, format_version):
|
||||
return parse(path, format_version)
|
||||
return LameDbReader(path, format_version).parse()
|
||||
|
||||
|
||||
def write_services(path, services, format_version=4):
|
||||
if format_version == 4:
|
||||
write_to_lamedb(path, services)
|
||||
elif format_version == 5:
|
||||
write_to_lamedb5(path, services)
|
||||
LameDbWriter(path, services, format_version).write()
|
||||
|
||||
|
||||
def write_to_lamedb(path, services):
|
||||
""" Writing lamedb file ver.4 """
|
||||
lines = [_HEADER.format(4), "\ntransponders\n"]
|
||||
tr_lines = []
|
||||
services_lines = ["end\nservices\n"]
|
||||
tr_set = set()
|
||||
class LameDbReader:
|
||||
""" Lamedb parser class.
|
||||
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
if tr_id not in tr_set:
|
||||
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
|
||||
tr_lines.append(transponder)
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
Reads and parses the Enigma2 lamedb[5] file.
|
||||
Supports versions 3, 4 and 5..
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt"]
|
||||
|
||||
tr_lines.sort()
|
||||
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)
|
||||
def __init__(self, path, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
|
||||
def parse(self):
|
||||
""" Parsing lamedb. """
|
||||
if self._fmt == 4:
|
||||
return self.parse_v4()
|
||||
elif self._fmt == 5:
|
||||
return self.parse_v5()
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
|
||||
def parse_v3(self, services, transponders):
|
||||
""" Parsing version 3. """
|
||||
for t in transponders:
|
||||
tr = transponders[t].lower()
|
||||
tr_type = tr[0:1]
|
||||
if tr_type == "c":
|
||||
tr += ":0:0:0"
|
||||
elif tr_type == "t" or tr_type == "a":
|
||||
tr += ":0:0"
|
||||
else:
|
||||
tr_data = tr.split(_SEP)
|
||||
len_data = len(tr_data)
|
||||
if len_data == 6:
|
||||
tr_data.append("0")
|
||||
elif len_data == 9:
|
||||
tr_data.insert(6, "0")
|
||||
tr_data.append("0")
|
||||
tr_data.append("2")
|
||||
|
||||
tr = _SEP.join(tr_data)
|
||||
|
||||
transponders[t] = tr
|
||||
|
||||
return self.parse_services(services, transponders)
|
||||
|
||||
def parse_v4(self):
|
||||
""" Parsing version 4. """
|
||||
with open(self._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:
|
||||
return self.get_services_list(data)
|
||||
|
||||
def parse_v5(self):
|
||||
""" Parsing version 5. """
|
||||
with open(self._path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
|
||||
lns = file.readlines()
|
||||
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
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:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
elif data_len == 2:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
elif line.startswith("t:"):
|
||||
data = line.split(",")
|
||||
len_data = len(data)
|
||||
if len_data > 1:
|
||||
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
|
||||
trs[tr] = srv
|
||||
else:
|
||||
log("Error while parsing transponder data [ver. 5] for line: {}".format(line))
|
||||
|
||||
return self.parse_services(srvs, trs)
|
||||
|
||||
def parse_services(self, services, transponders):
|
||||
""" Parsing services. """
|
||||
services_list = []
|
||||
blacklist = get_blacklist(self._path) if self._path else {}
|
||||
srvs = self.split(services, 3)
|
||||
if srvs[0][0] == "": # Remove first empty element.
|
||||
srvs.remove(srvs[0])
|
||||
|
||||
for srv in srvs:
|
||||
data_id = str(srv[0]).lower() # Lower is for lamedb ver.3.
|
||||
data = data_id.split(_SEP)
|
||||
sp = "0"
|
||||
tid = data[2]
|
||||
nid = data[3]
|
||||
# For lamedb ver.3
|
||||
is_v3 = False
|
||||
if len(tid) < 4:
|
||||
is_v3 = True
|
||||
tid = "{:0>4}".format(tid)
|
||||
data[2] = tid
|
||||
if len(nid) < 4:
|
||||
is_v3 = True
|
||||
nid = "{:0>4}".format(nid)
|
||||
data[3] = nid
|
||||
if is_v3:
|
||||
data[0] = "{:0>4}".format(data[0])
|
||||
data_id = _SEP.join(data)
|
||||
|
||||
srv_type = int(data[4])
|
||||
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
tid = tid.lstrip(sp).upper()
|
||||
nid = nid.lstrip(sp).upper()
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# 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 s_id in blacklist else None
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else ""
|
||||
|
||||
if transponder is not None:
|
||||
tr_type, sp, tr = str(transponder).partition(" ")
|
||||
tr_type = TrType(tr_type)
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
# Removing all non printable symbols!
|
||||
srv_name = "".join(c for c in srv[1] if c.isprintable())
|
||||
freq = tr[0]
|
||||
rate = tr[1]
|
||||
pol = None
|
||||
fec = None
|
||||
system = None
|
||||
pos = None
|
||||
|
||||
if tr_type is TrType.Satellite:
|
||||
pol = POLARIZATION.get(tr[2], None)
|
||||
fec = FEC.get(tr[3], None)
|
||||
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
|
||||
pos = tr[4]
|
||||
if tr_type is TrType.Terrestrial:
|
||||
system = T_SYSTEM.get(tr[9], None)
|
||||
pos = "T"
|
||||
fec = T_FEC.get(tr[3], None)
|
||||
elif tr_type is TrType.Cable:
|
||||
system = "DVB-C"
|
||||
pos = "C"
|
||||
fec = FEC_DEFAULT.get(tr[4])
|
||||
elif tr_type is TrType.ATSC:
|
||||
system = "ATSC"
|
||||
pos = "T"
|
||||
fec = FEC_DEFAULT.get("0")
|
||||
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
if tr_type is TrType.Satellite:
|
||||
pos = int(pos)
|
||||
pos = "{:0.1f}{}".format(abs(pos / 10), "W" if pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Parse error [parse_services]: {}".format(e))
|
||||
|
||||
s = Service(srv[2], tr_type.value, coded, srv_name, locked, hide, package, service_type, None,
|
||||
picon_id, data[0], freq, rate, pol, fec, system, pos, data_id, fav_id, transponder)
|
||||
|
||||
services_list.append(s)
|
||||
return services_list
|
||||
|
||||
def get_services_list(self, data):
|
||||
""" 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)
|
||||
|
||||
transponders, sep, services = services.partition("services") # 2 step
|
||||
services, sep, _ = services.partition("\nend") # 3 step
|
||||
|
||||
if match.group() == "/3/":
|
||||
return self.parse_v3(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
return self.parse_services(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
@staticmethod
|
||||
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])
|
||||
if tr_id not in tr_set:
|
||||
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
|
||||
tr_lines.append(transponder)
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append("end\n" + _END_LINE)
|
||||
|
||||
return lines
|
||||
|
||||
def parse_transponders(self, arg):
|
||||
""" Parsing transponders. """
|
||||
transponders = {}
|
||||
for ar in arg:
|
||||
tr = ar.replace("\n", "").split("\t")
|
||||
if len(tr) == 2:
|
||||
transponders[tr[0]] = tr[1]
|
||||
|
||||
return transponders
|
||||
|
||||
def split(self, itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
|
||||
|
||||
def write_to_lamedb5(path, services):
|
||||
""" Writing lamedb5 file """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
class LameDbWriter:
|
||||
""" Writes the Enigma2 lamedb[5] file.
|
||||
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
|
||||
# Removing empty packages
|
||||
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
|
||||
flags = ",".join(flags)
|
||||
flags = "," + flags if flags else ""
|
||||
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
|
||||
Version 4 will be used instead of version 3!
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt", "_services"]
|
||||
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
def __init__(self, path, services, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
self._services = services
|
||||
|
||||
with open(path + "lamedb5", "w") as file:
|
||||
file.writelines(lines)
|
||||
def write(self):
|
||||
if self._fmt == 4:
|
||||
# Writing lamedb file ver.4
|
||||
with open(self._path + _FILE_NAME, "w") as file:
|
||||
file.writelines(LameDbReader.get_services_lines(self._services))
|
||||
elif self._fmt == 5:
|
||||
self.write_to_lamedb5()
|
||||
|
||||
def write_to_lamedb5(self):
|
||||
""" Writing lamedb5 file. """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
|
||||
def parse(path, version=4):
|
||||
""" Parsing lamedb """
|
||||
if version == 4:
|
||||
return parse_v4(path)
|
||||
elif version == 5:
|
||||
return parse_v5(path)
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
for srv in self._services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
|
||||
# Removing empty packages
|
||||
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
|
||||
flags = ",".join(flags)
|
||||
flags = "," + flags if flags else ""
|
||||
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
|
||||
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
|
||||
def parse_v3(services, transponders, path):
|
||||
""" Parsing version 3 """
|
||||
for t in transponders:
|
||||
tr = transponders[t].lower()
|
||||
tr_type = tr[0:1]
|
||||
if tr_type == "c":
|
||||
tr += ":0:0:0"
|
||||
elif tr_type == "t":
|
||||
tr += ":0:0"
|
||||
else:
|
||||
tr_data = tr.split(_SEP)
|
||||
len_data = len(tr_data)
|
||||
if len_data == 6:
|
||||
tr_data.append("0")
|
||||
elif len_data == 9:
|
||||
tr_data.insert(6, "0")
|
||||
tr_data.append("0")
|
||||
tr_data.append("2")
|
||||
|
||||
tr = _SEP.join(tr_data)
|
||||
|
||||
transponders[t] = tr
|
||||
|
||||
return parse_services(services, transponders, path)
|
||||
|
||||
|
||||
def parse_v4(path):
|
||||
""" 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)
|
||||
|
||||
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 """
|
||||
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
|
||||
lns = file.readlines()
|
||||
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
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)
|
||||
srv_data[1] = srv_data[1].strip("\"")
|
||||
data_len = len(srv_data)
|
||||
if data_len == 3:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
elif data_len == 2:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
elif l.startswith("t:"):
|
||||
tr, srv = l.split(",")
|
||||
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
|
||||
|
||||
return parse_services(srvs, trs, path)
|
||||
|
||||
|
||||
def parse_transponders(arg):
|
||||
""" Parsing transponders """
|
||||
transponders = {}
|
||||
for ar in arg:
|
||||
tr = ar.replace("\n", "").split("\t")
|
||||
if len(tr) == 2:
|
||||
transponders[tr[0]] = tr[1]
|
||||
|
||||
return transponders
|
||||
|
||||
|
||||
def parse_services(services, transponders, path):
|
||||
""" Parsing services """
|
||||
services_list = []
|
||||
blacklist = str(get_blacklist(path))
|
||||
srvs = split(services, 3)
|
||||
if srvs[0][0] == "": # remove first empty element
|
||||
srvs.remove(srvs[0])
|
||||
|
||||
for srv in srvs:
|
||||
data_id = str(srv[0]).lower() # lower is for lamedb ver.3
|
||||
data = data_id.split(_SEP)
|
||||
sp = "0"
|
||||
tid = data[2]
|
||||
nid = data[3]
|
||||
# For lamedb ver.3
|
||||
is_v3 = False
|
||||
if len(tid) < 4:
|
||||
is_v3 = True
|
||||
tid = "{:0>4}".format(tid)
|
||||
data[2] = tid
|
||||
if len(nid) < 4:
|
||||
is_v3 = True
|
||||
nid = "{:0>4}".format(nid)
|
||||
data[3] = nid
|
||||
if is_v3:
|
||||
data[0] = "{:0>4}".format(data[0])
|
||||
data_id = _SEP.join(data)
|
||||
|
||||
srv_type = int(data[4])
|
||||
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
tid = tid.lstrip(sp).upper()
|
||||
nid = nid.lstrip(sp).upper()
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else ""
|
||||
|
||||
if transponder is not None:
|
||||
tr_type, sp, tr = str(transponder).partition(" ")
|
||||
tr_type = TrType(tr_type)
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
# removing all non printable symbols!
|
||||
srv_name = "".join(c for c in srv[1] if c.isprintable())
|
||||
pol = None
|
||||
fec = None
|
||||
system = None
|
||||
pos = None
|
||||
|
||||
if tr_type is TrType.Satellite:
|
||||
pol = POLARIZATION.get(tr[2], None)
|
||||
fec = FEC.get(tr[3], None)
|
||||
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
|
||||
pos = "{}.{}".format(tr[4][:-1], tr[4][-1:])
|
||||
if tr_type is TrType.Terrestrial:
|
||||
system = T_SYSTEM.get(tr[9], None)
|
||||
pos = "T"
|
||||
fec = T_FEC.get(tr[3], None)
|
||||
elif tr_type is TrType.Cable:
|
||||
system = "DVB-C"
|
||||
pos = "C"
|
||||
fec = FEC_DEFAULT.get(tr[4])
|
||||
|
||||
services_list.append(Service(flags_cas=srv[2],
|
||||
transponder_type=tr_type.value,
|
||||
coded=coded,
|
||||
service=srv_name,
|
||||
locked=locked,
|
||||
hide=hide,
|
||||
package=package,
|
||||
service_type=service_type,
|
||||
picon=None,
|
||||
picon_id=picon_id,
|
||||
ssid=data[0],
|
||||
freq=tr[0],
|
||||
rate=tr[1],
|
||||
pol=pol,
|
||||
fec=fec,
|
||||
system=system,
|
||||
pos=pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=transponder))
|
||||
return services_list
|
||||
|
||||
|
||||
def split(itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
with open(self._path + "lamedb5", "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
""" Module for IPTV and streams support """
|
||||
import re
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from urllib.parse import unquote, quote
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.settings import SettingsType
|
||||
from app.ui.uicommons import IPTV_ICON
|
||||
from .ecommons import BqServiceType, Service
|
||||
|
||||
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
|
||||
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
|
||||
@@ -18,33 +19,75 @@ class StreamType(Enum):
|
||||
NONE_TS = "4097"
|
||||
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, params=None):
|
||||
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
|
||||
s_aggr = aggr[: -3]
|
||||
services = []
|
||||
groups = set()
|
||||
counter = 0
|
||||
marker_counter = 1
|
||||
sid_counter = 1
|
||||
name = None
|
||||
picon = None
|
||||
p_id = "1_0_1_0_0_0_0_0_0_0.png"
|
||||
st = BqServiceType.IPTV.name
|
||||
params = params or [0, 0, 0, 0]
|
||||
|
||||
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()
|
||||
inf, sep, line = line.partition(" ")
|
||||
if not line:
|
||||
line = inf
|
||||
line, sep, name = line.rpartition(",")
|
||||
|
||||
data = re.split('"', line)
|
||||
size = len(data)
|
||||
if size < 3:
|
||||
continue
|
||||
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
|
||||
picon = d.get("tvg-logo", None)
|
||||
|
||||
grp_name = d.get("group-title", None)
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = line.strip("#EXTGRP:").strip()
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(counter, grp_name, grp_name)
|
||||
counter += 1
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif not line.startswith("#"):
|
||||
url = line.strip()
|
||||
fav_id = get_fav_id(url, name, s_type)
|
||||
if name and url:
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
|
||||
params[0] = sid_counter
|
||||
sid_counter += 1
|
||||
fav_id = get_fav_id(url, name, s_type, params)
|
||||
if all((name, url, fav_id)):
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
|
||||
services.append(srv)
|
||||
else:
|
||||
log("*.m3u* parse error ['{}']: name[{}], url[{}], fav id[{}]".format(path, name, url, fav_id))
|
||||
|
||||
return services
|
||||
|
||||
@@ -64,7 +107,7 @@ def export_to_m3u(path, bouquet, s_type):
|
||||
lines.append("#EXTINF:-1,{}\n".format(s.name))
|
||||
if current_grp:
|
||||
lines.append(current_grp)
|
||||
lines.append("{}\n".format(urllib.request.unquote(data.strip())))
|
||||
lines.append("{}\n".format(unquote(data.strip())))
|
||||
elif s_type is BqServiceType.MARKER:
|
||||
current_grp = "#EXTGRP:{}\n".format(s.name)
|
||||
|
||||
@@ -72,13 +115,13 @@ def export_to_m3u(path, bouquet, s_type):
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def get_fav_id(url, service_name, s_type):
|
||||
def get_fav_id(url, service_name, settings_type, params=None, stream_type=None, s_type=1):
|
||||
""" Returns fav id depending on the profile. """
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
url = urllib.request.quote(url)
|
||||
stream_type = StreamType.NONE_TS.value
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, service_name, service_name, None)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
if settings_type is SettingsType.ENIGMA_2:
|
||||
stream_type = stream_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, s_type, *params, quote(url), service_name, service_name, None)
|
||||
elif settings_type is SettingsType.NEUTRINO_MP:
|
||||
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
|
||||
|
||||
|
||||
|
||||
@@ -89,11 +89,8 @@ def parse_webtv(path, name, bq_type):
|
||||
group = group.value if group else group
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
|
||||
iconsrc_b, group)
|
||||
srv = BouquetService(name=title,
|
||||
type=BqServiceType.IPTV,
|
||||
data=fav_id,
|
||||
num=0)
|
||||
services.append(srv)
|
||||
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
|
||||
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
|
||||
bouquets[2].append(bouquet)
|
||||
|
||||
@@ -125,14 +122,15 @@ def write_bouquet(file, bouquet):
|
||||
root.appendChild(bq_elem)
|
||||
|
||||
for srv in bq.services:
|
||||
f_data = srv.flags_cas.split(":")
|
||||
tr_id, on, ssid = srv.fav_id.split(":")
|
||||
srv_elem = doc.createElement("S")
|
||||
srv_elem.setAttribute("i", ssid)
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
srv_elem.setAttribute("t", tr_id)
|
||||
srv_elem.setAttribute("on", on)
|
||||
srv_elem.setAttribute("s", srv.pos.replace(".", ""))
|
||||
srv_elem.setAttribute("frq", srv.freq[:-3])
|
||||
srv_elem.setAttribute("s", f_data[1])
|
||||
srv_elem.setAttribute("frq", srv.freq)
|
||||
srv_elem.setAttribute("l", "0") # temporary !!!
|
||||
bq_elem.appendChild(srv_elem)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from xml.dom.minidom import parse, Document
|
||||
|
||||
from app.commons import log
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
|
||||
|
||||
_FILE = "services.xml"
|
||||
@@ -28,7 +29,7 @@ def write_services(path, services):
|
||||
tr_atr = sat.split(":")
|
||||
sat_elem = doc.createElement("sat")
|
||||
sat_elem.setAttribute("name", tr_atr[0])
|
||||
sat_elem.setAttribute("position", tr_atr[1].replace(".", ""))
|
||||
sat_elem.setAttribute("position", tr_atr[1])
|
||||
sat_elem.setAttribute("diseqc", tr_atr[2])
|
||||
sat_elem.setAttribute("uncommited", tr_atr[3])
|
||||
root.appendChild(sat_elem)
|
||||
@@ -88,7 +89,6 @@ def parse_services(path):
|
||||
if elem.hasAttributes():
|
||||
sat_name = elem.attributes["name"].value
|
||||
sat_pos = elem.attributes["position"].value
|
||||
sat_pos = "{}.{}".format(sat_pos[:-1], sat_pos[-1:])
|
||||
diseqc = elem.attributes.get("diseqc")
|
||||
diseqc = diseqc.value if diseqc else diseqc
|
||||
uncommited = elem.attributes.get("uncommited")
|
||||
@@ -117,6 +117,15 @@ def parse_transponder(api, sat, sat_pos, services, tr_elem):
|
||||
|
||||
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
|
||||
tr_id = tr_id.lstrip("0")
|
||||
pol = POLARIZATION.get(pol)
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
sat_pos = int(sat_pos)
|
||||
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse_transponder]: {}".format(e))
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
@@ -141,27 +150,10 @@ def parse_transponder(api, sat, sat_pos, services, tr_elem):
|
||||
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv, st, = PROVIDER.get(int(on, 16)), SERVICE_TYPE.get(str(int(srv_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
srv = Service(flags_cas=sat,
|
||||
transponder_type=None,
|
||||
coded=None,
|
||||
service=name,
|
||||
locked=None,
|
||||
hide=None,
|
||||
package=PROVIDER.get(int(on, 16)),
|
||||
service_type=SERVICE_TYPE.get(str(int(srv_type, 16))),
|
||||
picon=None,
|
||||
picon_id=picon_id,
|
||||
ssid=ssid,
|
||||
freq=freq,
|
||||
rate=rate,
|
||||
pol=POLARIZATION.get(pol),
|
||||
fec=FEC.get(fec),
|
||||
system=SYSTEM.get(sys),
|
||||
pos=sat_pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=tr)
|
||||
srv = Service(sat, None, None, name, None, None, prv, st, None, picon_id, ssid, freq, rate, pol,
|
||||
FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
|
||||
services.append(srv)
|
||||
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ def write_satellites(satellites, data_path):
|
||||
transponder_child.setAttribute("frequency", tr.frequency)
|
||||
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
|
||||
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner))
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner) or "0")
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system) or "0")
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation) or "0")
|
||||
if tr.pls_mode:
|
||||
transponder_child.setAttribute("pls_mode", tr.pls_mode)
|
||||
if tr.pls_code:
|
||||
@@ -90,7 +90,6 @@ def parse_transponders(elem, sat_name):
|
||||
atr["is_id"].value if "is_id" in atr else None)
|
||||
except Exception as e:
|
||||
message = "Error: can't parse transponder for '{}' satellite! {}".format(sat_name, repr(e))
|
||||
print(message)
|
||||
log(message)
|
||||
else:
|
||||
transponders.append(tr)
|
||||
|
||||
431
app/settings.py
431
app/settings.py
@@ -2,14 +2,19 @@ import copy
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from textwrap import dedent
|
||||
|
||||
CONFIG_PATH = str(Path.home()) + "/.config/demon-editor/"
|
||||
HOME_PATH = str(Path.home())
|
||||
CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = "data/"
|
||||
DATA_PATH = HOME_PATH + "/DemonEditor/data/"
|
||||
|
||||
IS_DARWIN = sys.platform == "darwin"
|
||||
|
||||
|
||||
class Defaults(Enum):
|
||||
@@ -18,13 +23,30 @@ class Defaults(Enum):
|
||||
BACKUP_BEFORE_DOWNLOADING = True
|
||||
BACKUP_BEFORE_SAVE = True
|
||||
V5_SUPPORT = False
|
||||
FORCE_BQ_NAMES = False
|
||||
HTTP_API_SUPPORT = False
|
||||
ENABLE_YT_DL = False
|
||||
ENABLE_SEND_TO = False
|
||||
USE_COLORS = True
|
||||
NEW_COLOR = "rgb(255,230,204)"
|
||||
EXTRA_COLOR = "rgb(179,230,204)"
|
||||
TOOLTIP_LOGO_SIZE = 96
|
||||
LIST_PICON_SIZE = 32
|
||||
FAV_CLICK_MODE = 0
|
||||
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
|
||||
STREAM_LIB = "vlc"
|
||||
PROFILE_FOLDER_DEFAULT = False
|
||||
RECORDS_PATH = DATA_PATH + "records/"
|
||||
ACTIVATE_TRANSCODING = False
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV/device"
|
||||
|
||||
|
||||
def get_settings():
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
write_settings(get_default_settings())
|
||||
|
||||
with open(CONFIG_FILE, "r") as config_file:
|
||||
return json.load(config_file)
|
||||
|
||||
|
||||
def get_default_settings(profile_name="default"):
|
||||
@@ -42,14 +64,33 @@ def get_default_settings(profile_name="default"):
|
||||
"use_colors": Defaults.USE_COLORS.value,
|
||||
"new_color": Defaults.NEW_COLOR.value,
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDS_PATH.value
|
||||
}
|
||||
|
||||
|
||||
def set_local_paths(settings, profile_name):
|
||||
settings["data_local_path"] = "{}{}/".format(settings["data_local_path"], profile_name)
|
||||
settings["picons_local_path"] = "{}{}/".format(settings["picons_local_path"], profile_name)
|
||||
settings["backup_local_path"] = "{}{}/".format(settings["backup_local_path"], profile_name)
|
||||
def get_default_transcoding_presets():
|
||||
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
|
||||
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
|
||||
|
||||
|
||||
def write_settings(config):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_FILE, "w") as config_file:
|
||||
json.dump(config, config_file, indent=" ")
|
||||
|
||||
|
||||
def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False):
|
||||
settings["data_local_path"] = "{}{}/".format(data_path, profile_name)
|
||||
if use_profile_folder:
|
||||
settings["picons_local_path"] = "{}{}/{}/".format(data_path, profile_name, "picons")
|
||||
settings["backup_local_path"] = "{}{}/{}/".format(data_path, profile_name, "backup")
|
||||
else:
|
||||
settings["picons_local_path"] = "{}{}/{}/".format(data_path, "picons", profile_name)
|
||||
settings["backup_local_path"] = "{}{}/{}/".format(data_path, "backup", profile_name)
|
||||
|
||||
|
||||
class SettingsType(IntEnum):
|
||||
@@ -61,10 +102,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/",
|
||||
@@ -72,9 +113,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/",
|
||||
@@ -86,12 +128,26 @@ class SettingsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SettingsReadException(SettingsException):
|
||||
pass
|
||||
|
||||
|
||||
class PlayStreamsMode(IntEnum):
|
||||
""" Behavior mode when opening streams. """
|
||||
BUILT_IN = 0
|
||||
WINDOW = 1
|
||||
M3U = 2
|
||||
|
||||
|
||||
class Settings:
|
||||
__INSTANCE = None
|
||||
__VERSION = 1
|
||||
|
||||
def __init__(self, ext_settings=None):
|
||||
settings = ext_settings or get_settings()
|
||||
try:
|
||||
settings = ext_settings or get_settings()
|
||||
except PermissionError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
if self.__VERSION > settings.get("version", 0):
|
||||
raise SettingsException("Outdated version of the settings format!")
|
||||
@@ -99,7 +155,9 @@ class Settings:
|
||||
self._settings = settings
|
||||
self._current_profile = self._settings.get("default_profile", "default")
|
||||
self._profiles = self._settings.get("profiles", {"default": SettingsType.ENIGMA_2.get_default_settings()})
|
||||
self._cp_settings = self._profiles.get(self._current_profile) # Current profile settings
|
||||
self._cp_settings = self._profiles.get(self._current_profile, None) # Current profile settings
|
||||
if not self._cp_settings:
|
||||
raise SettingsException("Error reading settings [current profile].")
|
||||
|
||||
def __str__(self):
|
||||
return dedent(""" Current profile: {}
|
||||
@@ -123,7 +181,10 @@ class Settings:
|
||||
def reset(self, force_write=False):
|
||||
for k, v in self.setting_type.get_default_settings().items():
|
||||
self._cp_settings[k] = v
|
||||
set_local_paths(self._cp_settings, self._current_profile)
|
||||
|
||||
def_path = self.default_data_path
|
||||
def_path += "enigma2/" if self.setting_type is SettingsType.ENIGMA_2 else "neutrino/"
|
||||
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
|
||||
|
||||
if force_write:
|
||||
self.save()
|
||||
@@ -188,21 +249,7 @@ class Settings:
|
||||
def setting_type(self, s_type):
|
||||
self._cp_settings["setting_type"] = s_type.value
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
return self._settings.get("language", locale.getlocale()[0] or "en_US")
|
||||
|
||||
@language.setter
|
||||
def language(self, value):
|
||||
self._settings["language"] = value
|
||||
|
||||
@property
|
||||
def load_last_config(self):
|
||||
return self._settings.get("load_last_config", False)
|
||||
|
||||
@load_last_config.setter
|
||||
def load_last_config(self, value):
|
||||
self._settings["load_last_config"] = value
|
||||
# ******* Network ******** #
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
@@ -236,22 +283,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"))
|
||||
@@ -276,22 +307,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"))
|
||||
@@ -332,14 +347,6 @@ class Settings:
|
||||
def satellites_xml_path(self, value):
|
||||
self._cp_settings["satellites_xml_path"] = value
|
||||
|
||||
@property
|
||||
def data_local_path(self):
|
||||
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
|
||||
|
||||
@data_local_path.setter
|
||||
def data_local_path(self, value):
|
||||
self._cp_settings["data_local_path"] = value
|
||||
|
||||
@property
|
||||
def picons_path(self):
|
||||
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
|
||||
@@ -348,6 +355,32 @@ class Settings:
|
||||
def picons_path(self, value):
|
||||
self._cp_settings["picons_path"] = value
|
||||
|
||||
# ***** Local paths ***** #
|
||||
|
||||
@property
|
||||
def profile_folder_is_default(self):
|
||||
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT.value)
|
||||
|
||||
@profile_folder_is_default.setter
|
||||
def profile_folder_is_default(self, value):
|
||||
self._settings["profile_folder_is_default"] = value
|
||||
|
||||
@property
|
||||
def default_data_path(self):
|
||||
return self._settings.get("default_data_path", DATA_PATH)
|
||||
|
||||
@default_data_path.setter
|
||||
def default_data_path(self, value):
|
||||
self._settings["default_data_path"] = value
|
||||
|
||||
@property
|
||||
def data_local_path(self):
|
||||
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
|
||||
|
||||
@data_local_path.setter
|
||||
def data_local_path(self, value):
|
||||
self._cp_settings["data_local_path"] = value
|
||||
|
||||
@property
|
||||
def picons_local_path(self):
|
||||
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
|
||||
@@ -364,7 +397,68 @@ class Settings:
|
||||
def backup_local_path(self, value):
|
||||
self._cp_settings["backup_local_path"] = value
|
||||
|
||||
# ***** Program settings *****
|
||||
@property
|
||||
def records_path(self):
|
||||
return self._settings.get("records_path", Defaults.RECORDS_PATH.value)
|
||||
|
||||
@records_path.setter
|
||||
def records_path(self, value):
|
||||
self._settings["records_path"] = value
|
||||
|
||||
# ******** Streaming ********* #
|
||||
|
||||
@property
|
||||
def activate_transcoding(self):
|
||||
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING.value)
|
||||
|
||||
@activate_transcoding.setter
|
||||
def activate_transcoding(self, value):
|
||||
self._settings["activate_transcoding"] = value
|
||||
|
||||
@property
|
||||
def active_preset(self):
|
||||
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET.value)
|
||||
|
||||
@active_preset.setter
|
||||
def active_preset(self, value):
|
||||
self._settings["active_preset"] = value
|
||||
|
||||
@property
|
||||
def transcoding_presets(self):
|
||||
return self._settings.get("transcoding_presets", get_default_transcoding_presets())
|
||||
|
||||
@transcoding_presets.setter
|
||||
def transcoding_presets(self, value):
|
||||
self._settings["transcoding_presets"] = value
|
||||
|
||||
@property
|
||||
def play_streams_mode(self):
|
||||
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE.value))
|
||||
|
||||
@play_streams_mode.setter
|
||||
def play_streams_mode(self, value):
|
||||
self._settings["play_streams_mode"] = value
|
||||
|
||||
@property
|
||||
def stream_lib(self):
|
||||
return self._settings.get("stream_lib", Defaults.STREAM_LIB.value)
|
||||
|
||||
@stream_lib.setter
|
||||
def stream_lib(self, value):
|
||||
self._settings["stream_lib"] = value
|
||||
|
||||
# *********** EPG ************ #
|
||||
|
||||
@property
|
||||
def epg_options(self):
|
||||
""" Options used by the EPG dialog. """
|
||||
return self._cp_settings.get("epg_options", None)
|
||||
|
||||
@epg_options.setter
|
||||
def epg_options(self, value):
|
||||
self._cp_settings["epg_options"] = value
|
||||
|
||||
# ***** Program settings ***** #
|
||||
|
||||
@property
|
||||
def backup_before_save(self):
|
||||
@@ -390,6 +484,14 @@ class Settings:
|
||||
def v5_support(self, value):
|
||||
self._settings["v5_support"] = value
|
||||
|
||||
@property
|
||||
def force_bq_names(self):
|
||||
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
|
||||
|
||||
@force_bq_names.setter
|
||||
def force_bq_names(self, value):
|
||||
self._settings["force_bq_names"] = value
|
||||
|
||||
@property
|
||||
def http_api_support(self):
|
||||
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
|
||||
@@ -406,6 +508,14 @@ class Settings:
|
||||
def enable_yt_dl(self, value):
|
||||
self._settings["enable_yt_dl"] = value
|
||||
|
||||
@property
|
||||
def enable_yt_dl_update(self):
|
||||
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL.value)
|
||||
|
||||
@enable_yt_dl_update.setter
|
||||
def enable_yt_dl_update(self, value):
|
||||
self._settings["enable_yt_dl_update"] = value
|
||||
|
||||
@property
|
||||
def enable_send_to(self):
|
||||
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
|
||||
@@ -414,6 +524,74 @@ class Settings:
|
||||
def enable_send_to(self, value):
|
||||
self._settings["enable_send_to"] = value
|
||||
|
||||
@property
|
||||
def fav_click_mode(self):
|
||||
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
|
||||
|
||||
@fav_click_mode.setter
|
||||
def fav_click_mode(self, value):
|
||||
self._settings["fav_click_mode"] = value
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
return self._settings.get("language", locale.getlocale()[0] or "en_US")
|
||||
|
||||
@language.setter
|
||||
def language(self, value):
|
||||
self._settings["language"] = value
|
||||
|
||||
@property
|
||||
def load_last_config(self):
|
||||
return self._settings.get("load_last_config", False)
|
||||
|
||||
@load_last_config.setter
|
||||
def load_last_config(self, value):
|
||||
self._settings["load_last_config"] = value
|
||||
|
||||
@property
|
||||
def show_srv_hints(self):
|
||||
""" Show short info as hints in the main services list. """
|
||||
return self._settings.get("show_srv_hints", True)
|
||||
|
||||
@show_srv_hints.setter
|
||||
def show_srv_hints(self, value):
|
||||
self._settings["show_srv_hints"] = value
|
||||
|
||||
@property
|
||||
def show_bq_hints(self):
|
||||
""" Show detailed info as hints in the bouquet list. """
|
||||
return self._settings.get("show_bq_hints", True)
|
||||
|
||||
@show_bq_hints.setter
|
||||
def show_bq_hints(self, value):
|
||||
self._settings["show_bq_hints"] = value
|
||||
|
||||
# *********** Appearance *********** #
|
||||
|
||||
@property
|
||||
def list_font(self):
|
||||
return self._settings.get("list_font", "")
|
||||
|
||||
@list_font.setter
|
||||
def list_font(self, value):
|
||||
self._settings["list_font"] = value
|
||||
|
||||
@property
|
||||
def list_picon_size(self):
|
||||
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE.value)
|
||||
|
||||
@list_picon_size.setter
|
||||
def list_picon_size(self, value):
|
||||
self._settings["list_picon_size"] = value
|
||||
|
||||
@property
|
||||
def tooltip_logo_size(self):
|
||||
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE.value)
|
||||
|
||||
@tooltip_logo_size.setter
|
||||
def tooltip_logo_size(self, value):
|
||||
self._settings["tooltip_logo_size"] = value
|
||||
|
||||
@property
|
||||
def use_colors(self):
|
||||
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
|
||||
@@ -439,28 +617,105 @@ class Settings:
|
||||
self._settings["extra_color"] = value
|
||||
|
||||
@property
|
||||
def fav_click_mode(self):
|
||||
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
|
||||
def dark_mode(self):
|
||||
return self._settings.get("dark_mode", False)
|
||||
|
||||
@fav_click_mode.setter
|
||||
def fav_click_mode(self, value):
|
||||
self._settings["fav_click_mode"] = value
|
||||
@dark_mode.setter
|
||||
def dark_mode(self, value):
|
||||
self._settings["dark_mode"] = value
|
||||
|
||||
@property
|
||||
def alternate_layout(self):
|
||||
return self._settings.get("alternate_layout", IS_DARWIN)
|
||||
|
||||
def get_settings():
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist
|
||||
os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True)
|
||||
@alternate_layout.setter
|
||||
def alternate_layout(self, value):
|
||||
self._settings["alternate_layout"] = value
|
||||
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
write_settings(get_default_settings())
|
||||
@property
|
||||
def bq_details_first(self):
|
||||
return self._settings.get("bq_details_first", False)
|
||||
|
||||
with open(CONFIG_FILE, "r") as config_file:
|
||||
return json.load(config_file)
|
||||
@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)
|
||||
|
||||
def write_settings(config):
|
||||
with open(CONFIG_FILE, "w") as config_file:
|
||||
json.dump(config, config_file, indent=" ")
|
||||
@is_themes_support.setter
|
||||
def is_themes_support(self, value):
|
||||
self._settings["is_themes_support"] = value
|
||||
|
||||
@property
|
||||
def theme(self):
|
||||
return self._settings.get("theme", "Default")
|
||||
|
||||
@theme.setter
|
||||
def theme(self, value):
|
||||
self._settings["theme"] = value
|
||||
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def themes_path(self):
|
||||
return "{}/.themes/".format(HOME_PATH)
|
||||
|
||||
@property
|
||||
def icon_theme(self):
|
||||
return self._settings.get("icon_theme", "Adwaita")
|
||||
|
||||
@icon_theme.setter
|
||||
def icon_theme(self, value):
|
||||
self._settings["icon_theme"] = value
|
||||
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def icon_themes_path(self):
|
||||
return "{}/.icons/".format(HOME_PATH)
|
||||
|
||||
@property
|
||||
def is_darwin(self):
|
||||
return IS_DARWIN
|
||||
|
||||
# *********** Download dialog *********** #
|
||||
|
||||
@property
|
||||
def use_http(self):
|
||||
return self._settings.get("use_http", True)
|
||||
|
||||
@use_http.setter
|
||||
def use_http(self, value):
|
||||
self._settings["use_http"] = value
|
||||
|
||||
@property
|
||||
def remove_unused_bouquets(self):
|
||||
return self._settings.get("remove_unused_bouquets", True)
|
||||
|
||||
@remove_unused_bouquets.setter
|
||||
def remove_unused_bouquets(self, value):
|
||||
self._settings["remove_unused_bouquets"] = value
|
||||
|
||||
# **************** Debug **************** #
|
||||
|
||||
@property
|
||||
def debug_mode(self):
|
||||
return self._settings.get("debug_mode", False)
|
||||
|
||||
@debug_mode.setter
|
||||
def debug_mode(self, value):
|
||||
self._settings["debug_mode"] = value
|
||||
|
||||
# **************** Experimental **************** #
|
||||
|
||||
@property
|
||||
def is_enable_experimental(self):
|
||||
""" Allows experimental functionality. """
|
||||
return self._settings.get("enable_experimental", False)
|
||||
|
||||
@is_enable_experimental.setter
|
||||
def is_enable_experimental(self, value):
|
||||
self._settings["enable_experimental"] = value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,49 +1,413 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
|
||||
from app.commons import run_task, log
|
||||
from gi.repository import Gdk, Gtk
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
|
||||
|
||||
|
||||
class Player:
|
||||
class Player(ABC):
|
||||
""" Base player class. Also used as a factory. """
|
||||
|
||||
@abstractmethod
|
||||
def get_play_mode(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def play(self, mrl=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_playing(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
pass
|
||||
|
||||
def get_window_handle(self, widget):
|
||||
""" Returns the identifier [pointer] for the window.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if sys.platform == "linux":
|
||||
return widget.get_window().get_xid()
|
||||
else:
|
||||
is_darwin = sys.platform == "darwin"
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if is_darwin else "libgdk-3-0.dll")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
|
||||
get_pointer.restype = ctypes.c_void_p
|
||||
get_pointer.argtypes = [ctypes.c_void_p]
|
||||
|
||||
return get_pointer(gpointer)
|
||||
|
||||
def get_video_widget(self, widget):
|
||||
area = Gtk.DrawingArea(visible=True)
|
||||
area.connect("draw", self.on_drawing_area_draw)
|
||||
area.connect("motion-notify-event", self.on_mouse_motion)
|
||||
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
widget.add(area)
|
||||
|
||||
return area
|
||||
|
||||
def on_drawing_area_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
def on_mouse_motion(self, widget, event):
|
||||
display = widget.get_display()
|
||||
window = widget.get_window()
|
||||
cursor = Gdk.Cursor.new_from_name(display, "default")
|
||||
window.set_cursor(cursor)
|
||||
|
||||
self.hide_mouse_cursor(window, display)
|
||||
|
||||
@run_with_delay(3)
|
||||
def hide_mouse_cursor(self, window, display):
|
||||
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
|
||||
window.set_cursor(cursor)
|
||||
|
||||
@staticmethod
|
||||
def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
""" Factory method. We will not use a separate factory to return a specific implementation.
|
||||
|
||||
@param name: implementation name.
|
||||
@param mode: current player mode [Built-in or windowed].
|
||||
@param widget: parent of video widget.
|
||||
@param buf_cb: buffering callback.
|
||||
@param position_cb: time (position) callback.
|
||||
@param error_cb: error callback.
|
||||
@param playing_cb: playing state callback.
|
||||
|
||||
Throws a NameError if there is no implementation for the given name.
|
||||
"""
|
||||
if name == "mpv":
|
||||
return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
elif name == "gst":
|
||||
return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
elif name == "vlc":
|
||||
return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
else:
|
||||
raise NameError("There is no such [{}] implementation.".format(name))
|
||||
|
||||
|
||||
class MpvPlayer(Player):
|
||||
""" Simple wrapper for MPV media player.
|
||||
|
||||
Uses python-mvp [https://github.com/jaseg/python-mpv].
|
||||
"""
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
try:
|
||||
from app.tools import mpv
|
||||
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget), )),
|
||||
input_default_bindings=False,
|
||||
input_cursor=False,
|
||||
cursor_autohide="no")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No libmpv is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
|
||||
def on_open(event):
|
||||
log("Starting playback...")
|
||||
playing_cb()
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.END_FILE)
|
||||
def on_end(event):
|
||||
event = event.get("event", {})
|
||||
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
|
||||
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
|
||||
error_cb()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if not mrl:
|
||||
return
|
||||
|
||||
self._player.play(mrl)
|
||||
self._is_playing = True
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
self._player.stop()
|
||||
self._is_playing = True
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._player.terminate()
|
||||
self.__INSTANCE = None
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
|
||||
class GstPlayer(Player):
|
||||
""" Simple wrapper for GStreamer playbin. """
|
||||
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gst", "1.0")
|
||||
gi.require_version("GstVideo", "1.0")
|
||||
from gi.repository import Gst, GstVideo
|
||||
# Initialization of GStreamer.
|
||||
Gst.init(sys.argv)
|
||||
gtk_sink = Gst.ElementFactory.make("gtksink")
|
||||
if not gtk_sink:
|
||||
msg = "GStreamer error: gtksink plugin not installed!"
|
||||
log(msg)
|
||||
raise ImportError(msg)
|
||||
except (OSError, ValueError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No GStreamer is found. Check that it is installed!")
|
||||
else:
|
||||
self._error_cb = error_cb
|
||||
self._playing_cb = playing_cb
|
||||
|
||||
self.STATE = Gst.State
|
||||
self.STAT_RETURN = Gst.StateChangeReturn
|
||||
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
self._player = Gst.ElementFactory.make("playbin", "player")
|
||||
# Initialization of the playback widget.
|
||||
self._player.set_property("video-sink", gtk_sink)
|
||||
vid_widget = gtk_sink.get_property("widget")
|
||||
vid_widget.connect("motion-notify-event", self.on_mouse_motion)
|
||||
widget.add(vid_widget)
|
||||
vid_widget.show()
|
||||
|
||||
bus = self._player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect("message::error", self.on_error)
|
||||
bus.connect("message::state-changed", self.on_state_changed)
|
||||
bus.connect("message::eos", self.on_eos)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
def play(self, mrl=None):
|
||||
self._player.set_state(self.STATE.READY)
|
||||
if not mrl:
|
||||
return
|
||||
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
log("Setting the URL for playback: {}".format(mrl))
|
||||
ret = self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
if ret == self.STAT_RETURN.FAILURE:
|
||||
log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl))
|
||||
else:
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
log("Stop playback...")
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.set_state(self.STATE.PAUSED)
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._is_playing = False
|
||||
self._player.set_state(self.STATE.NULL)
|
||||
self.__INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def on_error(self, bus, msg):
|
||||
err, dbg = msg.parse_error()
|
||||
log(err)
|
||||
self._error_cb()
|
||||
|
||||
def on_state_changed(self, bus, msg):
|
||||
if not msg.src == self._player:
|
||||
# Not from the player.
|
||||
return
|
||||
|
||||
old_state, new_state, pending = msg.parse_state_changed()
|
||||
if new_state is self.STATE.PLAYING:
|
||||
log("Starting playback...")
|
||||
self._playing_cb()
|
||||
self.get_stream_info()
|
||||
|
||||
def on_eos(self, bus, msg):
|
||||
""" Called when an end-of-stream message appears. """
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def get_stream_info(self):
|
||||
log("Getting stream info...")
|
||||
nr_video = self._player.get_property("n-video")
|
||||
for i in range(nr_video):
|
||||
# Retrieve the stream's video tags.
|
||||
tags = self._player.emit("get-video-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("video-codec")
|
||||
log("Video codec: {}".format(cod or "unknown"))
|
||||
|
||||
nr_audio = self._player.get_property("n-audio")
|
||||
for i in range(nr_audio):
|
||||
# Retrieve the stream's video tags.
|
||||
tags = self._player.emit("get-audio-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("audio-codec")
|
||||
log("Audio codec: {}".format(cod or "unknown"))
|
||||
|
||||
|
||||
class VlcPlayer(Player):
|
||||
""" Simple wrapper for VLC media player.
|
||||
|
||||
Uses python-vlc [https://github.com/oaubert/python-vlc].
|
||||
"""
|
||||
|
||||
__VLC_INSTANCE = None
|
||||
|
||||
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
try:
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError
|
||||
else:
|
||||
self._is_playing = False
|
||||
|
||||
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
|
||||
self._player = vlc.Instance(args).media_player_new()
|
||||
vlc.libvlc_video_set_key_input(self._player, False)
|
||||
vlc.libvlc_video_set_mouse_input(self._player, False)
|
||||
except (OSError, AttributeError, NameError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No VLC is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
ev_mgr = self._player.event_manager()
|
||||
|
||||
if rewind_callback:
|
||||
if buf_cb:
|
||||
# TODO look other EventType options
|
||||
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
|
||||
lambda et, p: rewind_callback(p.get_media().get_duration()),
|
||||
lambda et, p: buf_cb(p.get_media().get_duration()),
|
||||
self._player)
|
||||
if position_callback:
|
||||
if position_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et, p: position_callback(p.get_time()),
|
||||
lambda et, p: position_cb(p.get_time()),
|
||||
self._player)
|
||||
|
||||
if error_callback:
|
||||
if error_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
|
||||
lambda et, p: error_callback(),
|
||||
lambda et, p: error_cb(),
|
||||
self._player)
|
||||
if playing_callback:
|
||||
if playing_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
|
||||
lambda et, p: playing_callback(),
|
||||
lambda et, p: playing_cb(),
|
||||
self._player)
|
||||
|
||||
self.init_video_widget(widget)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None):
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
if not cls.__VLC_INSTANCE:
|
||||
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return cls.__VLC_INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if mrl:
|
||||
@@ -69,29 +433,7 @@ class Player:
|
||||
self._is_playing = False
|
||||
self._player.stop()
|
||||
self._player.release()
|
||||
|
||||
def set_xwindow(self, xid):
|
||||
self._player.set_xwindow(xid)
|
||||
|
||||
def set_nso(self, widget):
|
||||
""" Used on MacOS to set NSObject.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
try:
|
||||
import ctypes
|
||||
g_dll = ctypes.CDLL("libgdk-3.0.dylib")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
get_nsview = g_dll.gdk_quartz_window_get_nsview
|
||||
get_nsview.restype, get_nsview.argtypes = ctypes.c_void_p, [ctypes.c_void_p]
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
# Get the C void* pointer to the window
|
||||
pointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
self._player.set_nsobject(get_nsview(pointer))
|
||||
self.__VLC_INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_mrl(mrl)
|
||||
@@ -99,8 +441,80 @@ class Player:
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_full_screen(self, full):
|
||||
self._player.set_fullscreen(full)
|
||||
def init_video_widget(self, widget):
|
||||
video_widget = self.get_video_widget(widget)
|
||||
if sys.platform == "linux":
|
||||
self._player.set_xwindow(video_widget.get_window().get_xid())
|
||||
elif sys.platform == "darwin":
|
||||
self._player.set_nsobject(self.get_window_handle(video_widget))
|
||||
else:
|
||||
self._player.set_hwnd(self.get_window_handle(video_widget))
|
||||
|
||||
|
||||
class Recorder:
|
||||
__VLC_REC_INSTANCE = None
|
||||
|
||||
_CMD = "sout=#std{{access=file,mux=ts,dst={}.ts}}"
|
||||
_TR_CMD = "sout=#transcode{{{}}}:file{{mux=mp4,dst={}.mp4}}"
|
||||
|
||||
def __init__(self, settings):
|
||||
try:
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError
|
||||
else:
|
||||
self._settings = settings
|
||||
self._is_record = False
|
||||
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
|
||||
self._recorder = vlc.Instance(args).media_player_new()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, settings):
|
||||
if not cls.__VLC_REC_INSTANCE:
|
||||
cls.__VLC_REC_INSTANCE = Recorder(settings)
|
||||
return cls.__VLC_REC_INSTANCE
|
||||
|
||||
@run_task
|
||||
def record(self, url, name):
|
||||
if self._recorder:
|
||||
self._recorder.stop()
|
||||
|
||||
path = self._settings.records_path
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
d_now = datetime.now().strftime(_DATE_FORMAT)
|
||||
path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_"))
|
||||
cmd = self.get_transcoding_cmd(path) if self._settings.activate_transcoding else self._CMD.format(path)
|
||||
media = self._recorder.get_instance().media_new(url, cmd)
|
||||
media.get_mrl()
|
||||
|
||||
self._recorder.set_media(media)
|
||||
self._is_record = True
|
||||
self._recorder.play()
|
||||
log("Record started {}".format(d_now))
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
self._recorder.stop()
|
||||
self._is_record = False
|
||||
log("Recording stopped.")
|
||||
|
||||
def is_record(self):
|
||||
return self._is_record
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
if self._recorder:
|
||||
self._recorder.stop()
|
||||
self._recorder.release()
|
||||
self._is_record = False
|
||||
log("Recording stopped. Releasing...")
|
||||
|
||||
def get_transcoding_cmd(self, path):
|
||||
presets = self._settings.transcoding_presets
|
||||
prs = presets.get(self._settings.active_preset)
|
||||
return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
1941
app/tools/mpv.py
Normal file
1941
app/tools/mpv.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,230 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from app.commons import run_task
|
||||
import requests
|
||||
|
||||
from app.commons import run_task, log
|
||||
from app.settings import SettingsType
|
||||
from .satellites import _HEADERS
|
||||
|
||||
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
|
||||
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
|
||||
|
||||
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid", "single", "selected"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid"])
|
||||
|
||||
|
||||
class PiconsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PiconsCzDownloader:
|
||||
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
|
||||
|
||||
_PERM_URL = "https://picon.cz/download/7337"
|
||||
_BASE_URL = "https://picon.cz/download/"
|
||||
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
|
||||
_HEADER = {"User-Agent": "DemonEditor/1.0.10", "Referer": ""}
|
||||
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
|
||||
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
|
||||
|
||||
def __init__(self, picon_ids=set(), appender=log):
|
||||
self._perm_links = {}
|
||||
self._providers = {}
|
||||
self._provider_logos = {}
|
||||
self._picon_ids = picon_ids
|
||||
self._appender = appender
|
||||
|
||||
def init(self):
|
||||
""" Initializes dict with values: download_id -> perm link and provider data. """
|
||||
if self._perm_links:
|
||||
return
|
||||
|
||||
self._HEADER["Referer"] = self._PERM_URL
|
||||
|
||||
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
logo_map = self.get_logos_map()
|
||||
name_map = self.get_name_map()
|
||||
|
||||
for line in request.iter_lines():
|
||||
l_id, perm_link = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
|
||||
self._perm_links[str(l_id)] = str(perm_link)
|
||||
data = re.match(self._LINK_PATTERN, perm_link)
|
||||
if data:
|
||||
sat_pos = data.group(3)
|
||||
# Logo url.
|
||||
logo = logo_map.get(data.group(2), None)
|
||||
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
|
||||
logo_url = "{}{}/{}.png".format(self._BASE_LOGO_URL, logo, l_name) if logo else None
|
||||
|
||||
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
|
||||
if sat_pos in self._providers:
|
||||
self._providers[sat_pos].append(prv)
|
||||
else:
|
||||
self._providers[sat_pos] = [prv]
|
||||
else:
|
||||
log("{} [get permalinks] error: {}".format(self.__class__.__name__, request.reason))
|
||||
raise PiconsError(request.reason)
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return self._providers
|
||||
|
||||
def get_sat_providers(self, url):
|
||||
return self._providers.get(url, [])
|
||||
|
||||
def download(self, provider, picons_path, picon_ids=None):
|
||||
self._HEADER["Referer"] = provider.url
|
||||
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
dest = "{}{}.7z".format(picons_path, provider.on_id)
|
||||
self._appender("Downloading: {}\n".format(provider.url))
|
||||
with open(dest, mode="bw") as f:
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
f.write(data)
|
||||
self._appender("Extracting: {}\n".format(provider.on_id))
|
||||
self.extract(dest, picons_path, picon_ids)
|
||||
else:
|
||||
log("{} [download] error: {}".format(self.__class__.__name__, request.reason))
|
||||
|
||||
def extract(self, src, dest, picon_ids=None):
|
||||
""" Extracts 7z archives. """
|
||||
# TODO: think about https://github.com/miurahr/py7zr
|
||||
exe = "7zr"
|
||||
if not os.path.isfile("/usr/bin/7zr"):
|
||||
raise PiconsError("7-zip [7zr] archiver not found!")
|
||||
|
||||
cmd = [exe, "l", src]
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if err:
|
||||
log("{} [extract] error: {}".format(self.__class__.__name__, err))
|
||||
raise PiconsError(err)
|
||||
except OSError as e:
|
||||
log("{} [extract] error: {}".format(self.__class__.__name__, e))
|
||||
raise PiconsError(e)
|
||||
|
||||
is_filter = bool(picon_ids)
|
||||
ids = picon_ids or self._picon_ids
|
||||
to_extract = []
|
||||
|
||||
for o in re.finditer(self._FILE_PATTERN, out):
|
||||
p_id = o.group(1).decode("utf-8", errors="ignore")
|
||||
if p_id in ids:
|
||||
to_extract.append(p_id)
|
||||
|
||||
if is_filter and not to_extract:
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
raise PiconsError("No matching picons found!")
|
||||
|
||||
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
|
||||
cmd.extend(to_extract)
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if err:
|
||||
log("{} [extract] error: {}".format(self.__class__.__name__, err))
|
||||
raise PiconsError(err)
|
||||
else:
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
raise PiconsError(e)
|
||||
|
||||
def get_logo_data(self, url):
|
||||
""" Returns the logo data if present. """
|
||||
return self._provider_logos.get(url, None)
|
||||
|
||||
def get_provider_logo(self, url):
|
||||
""" Retrieves package logo. """
|
||||
# Getting package logo.
|
||||
logo = self._provider_logos.get(url, None)
|
||||
if logo:
|
||||
return logo
|
||||
|
||||
try:
|
||||
with requests.get(url=url, stream=True) as logo_request:
|
||||
if logo_request.reason == "OK":
|
||||
data = logo_request.content
|
||||
self._provider_logos[url] = data
|
||||
return data
|
||||
else:
|
||||
log("Downloading package logo error: {}".format(logo_request.reason))
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log("{} error [get provider logo]: {}".format(self.__class__.__name__, e))
|
||||
|
||||
def get_logos_map(self):
|
||||
return {"piconblack": "b50",
|
||||
"picontransparent": "t50",
|
||||
"piconwhite": "w50",
|
||||
"piconmirrorglass": "mr100",
|
||||
"piconnoName": "n100",
|
||||
"piconsrhd": "srhd100",
|
||||
"piconfreezeframe": "ff220",
|
||||
"piconfreezewhite": "fw100",
|
||||
"piconpoolrainbow": "r100",
|
||||
"piconsimpleblack": "s220",
|
||||
"piconjustblack": "jb220",
|
||||
"picondirtypaper": "dp220",
|
||||
"picongray": "g400",
|
||||
"piconmonochrom": "m220",
|
||||
"picontransparentwhite": "tw100",
|
||||
"picontransparentdark": "td220",
|
||||
"piconoled": "o96",
|
||||
"piconblack80": "b50",
|
||||
"piconblack3d": "b50"
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
return {"antiksat": "ANTIK",
|
||||
"digiczsk": "DIGI",
|
||||
"DTTitaly": "picon_trs-it",
|
||||
"dvbtCZSK": "picon_trs",
|
||||
"PolandDTT": "picon_trs-pl",
|
||||
"freeSAT": "UPC DIRECT",
|
||||
"orangesat": "ORANGE TV",
|
||||
"skylink": "M7 GROUP",
|
||||
}
|
||||
|
||||
|
||||
class PiconsParser(HTMLParser):
|
||||
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
def __init__(self, entities=False, separator=' ', single=None):
|
||||
|
||||
@@ -33,9 +240,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,32 +253,29 @@ 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)
|
||||
|
||||
if self._single and ln == 4 and row[0].startswith("../../logo/"):
|
||||
self.picons.append(Picon(row[0].strip("../"), "0", "0"))
|
||||
if self._single and ln == 4 and row[0].startswith("/logo/"):
|
||||
self.picons.append(Picon(row[0].strip(), "0"))
|
||||
else:
|
||||
if 9 < ln < 13:
|
||||
if ln > 8:
|
||||
url = None
|
||||
if row[0].startswith("../logo/"):
|
||||
url = row[0]
|
||||
elif row[1].startswith("../logo/"):
|
||||
url = row[1]
|
||||
if row[2].startswith("/logo/"):
|
||||
url = row[2]
|
||||
|
||||
ssid = row[-4]
|
||||
if url and len(ssid) > 2:
|
||||
self.picons.append(Picon(url, ssid, row[-3]))
|
||||
if url and row[0].isdigit():
|
||||
self.picons.append(Picon(url, row[0]))
|
||||
|
||||
self._current_row = []
|
||||
|
||||
@@ -79,34 +283,47 @@ class PiconsParser(HTMLParser):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
|
||||
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")
|
||||
pos = int("".join(c for c in pos if c.isdigit()))
|
||||
# For negative (West) positions 3600 - numeric position value!!!
|
||||
if neg_pos:
|
||||
pos = 3600 - pos
|
||||
parser = PiconsParser(single=single)
|
||||
parser.reset()
|
||||
parser.feed(f.read())
|
||||
picons = parser.picons
|
||||
if picons:
|
||||
os.makedirs(picons_path, exist_ok=True)
|
||||
for p in picons:
|
||||
try:
|
||||
if single:
|
||||
on_id, freq = on_id.strip().split("::")
|
||||
namespace = "{:X}{:X}".format(int(pos), int(freq))
|
||||
else:
|
||||
namespace = "{:X}0000".format(int(pos))
|
||||
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
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)
|
||||
def parse(provider, picons_path, picon_ids, s_type=SettingsType.ENIGMA_2):
|
||||
""" Returns tuple(url, picon file name) list. """
|
||||
req = requests.get(provider.url, timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.text
|
||||
else:
|
||||
log("Provider picons downloading error: {} {}".format(provider.url, req.reason))
|
||||
return
|
||||
|
||||
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
|
||||
neg_pos = pos.endswith("W")
|
||||
pos = int("".join(c for c in pos if c.isdigit()))
|
||||
# For negative (West) positions 3600 - numeric position value!!!
|
||||
if neg_pos:
|
||||
pos = 3600 - pos
|
||||
|
||||
parser = PiconsParser(single=provider.single)
|
||||
parser.reset()
|
||||
parser.feed(logo_data)
|
||||
picons = parser.picons
|
||||
picons_data = []
|
||||
|
||||
if picons:
|
||||
for p in picons:
|
||||
try:
|
||||
if single:
|
||||
on_id, freq = on_id.strip().split("::")
|
||||
namespace = "{:X}{:X}".format(int(pos), int(freq))
|
||||
else:
|
||||
namespace = "{:X}0000".format(int(pos))
|
||||
|
||||
if single and not ssid.isdigit():
|
||||
ssid = "".join(c for c in ssid if c.isdigit()) or "0"
|
||||
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
picons_data.append(("{}{}".format(PiconsParser._BASE_URL, p.ref), p_name))
|
||||
except (TypeError, ValueError) as e:
|
||||
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
|
||||
log(msg)
|
||||
|
||||
return picons_data
|
||||
|
||||
@staticmethod
|
||||
def format(ssid, on_id, namespace, picon_ids, s_type):
|
||||
@@ -125,10 +342,8 @@ class ProviderParser(HTMLParser):
|
||||
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
|
||||
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
|
||||
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
|
||||
_DOMAIN = "https://www.lyngsat.com"
|
||||
_TV_DOMAIN = _DOMAIN + "/tvchannels/"
|
||||
_RADIO_DOMAIN = _DOMAIN + "/radiochannels/"
|
||||
_PKG_DOMAIN = _DOMAIN + "/packages/"
|
||||
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/", "/logo/"}
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
def __init__(self, entities=False, separator=' '):
|
||||
|
||||
@@ -156,26 +371,17 @@ class ProviderParser(HTMLParser):
|
||||
if tag == 'tr':
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
if attrs[0][1].startswith("logo/"):
|
||||
if attrs[0][1].startswith("/logo/"):
|
||||
self._current_row.append(attrs[0][1])
|
||||
if tag == "a":
|
||||
url = attrs[0][1]
|
||||
if url.startswith((self._PKG_DOMAIN, self._TV_DOMAIN, self._RADIO_DOMAIN)):
|
||||
if any(d in url for d in self._DOMAINS):
|
||||
self._current_row.append(url)
|
||||
if tag == "font" and len(attrs) == 1:
|
||||
atr = attrs[0]
|
||||
if len(atr) == 2 and atr[1] == "darkgreen":
|
||||
self._is_onid_tid = True
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
if self._is_onid_tid:
|
||||
m = self._ONID_TID_PATTERN.match(data)
|
||||
if m:
|
||||
self._on_id, tid = m.group().split("-")
|
||||
self._is_onid_tid = False
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
@@ -188,41 +394,49 @@ class ProviderParser(HTMLParser):
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == 'tr':
|
||||
r = self._current_row
|
||||
row = self._current_row
|
||||
# Satellite position
|
||||
if not self._positon:
|
||||
pos = re.findall(self._POSITION_PATTERN, str(r))
|
||||
pos = re.findall(self._POSITION_PATTERN, str(row))
|
||||
if pos:
|
||||
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
|
||||
|
||||
len_row = len(r)
|
||||
len_row = len(row)
|
||||
if len_row > 2:
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(r[1])
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[0])
|
||||
if m:
|
||||
self._freq = m.group().split()[0]
|
||||
|
||||
if len_row == 12:
|
||||
if len_row > 12:
|
||||
# Providers
|
||||
name = r[5]
|
||||
name = row[5]
|
||||
self._prv_names.add(name)
|
||||
m = self._ONID_TID_PATTERN.match(str(r[-2]))
|
||||
m = self._ONID_TID_PATTERN.match(str(row[-5]))
|
||||
if m:
|
||||
on_id, tid = m.group().split("-")
|
||||
if on_id not in self._ids:
|
||||
r[-2] = on_id
|
||||
self._on_id = on_id
|
||||
row[-2] = on_id
|
||||
self._ids.add(on_id)
|
||||
r[0] = self._positon
|
||||
row[0] = self._positon
|
||||
if name + on_id not in self._prv_names:
|
||||
self._prv_names.add(name + on_id)
|
||||
self.rows.append(Provider(logo=r[2], name=name, pos=self._positon, url=r[6], on_id=on_id,
|
||||
logo_data = None
|
||||
if row[2].startswith("/logo/"):
|
||||
req = requests.get(self._BASE_URL + row[2], timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.content
|
||||
else:
|
||||
log("Downloading provider logo error: {}".format(req.reason))
|
||||
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[6], on_id=on_id,
|
||||
ssid=None, single=False, selected=True))
|
||||
elif 6 < len_row < 10:
|
||||
elif 6 < len_row < 12:
|
||||
# Single services
|
||||
name, url, ssid = None, None, None
|
||||
if r[0].startswith("http"):
|
||||
name, url, ssid = r[1], r[0], r[4]
|
||||
elif r[1].startswith("http"):
|
||||
name, url, ssid = r[2], r[1], r[5]
|
||||
if row[0].startswith("http"):
|
||||
name, url, ssid = row[1], row[0], row[0]
|
||||
elif row[1].startswith("http"):
|
||||
name, url, ssid = row[2], row[1], row[0]
|
||||
|
||||
if name and url:
|
||||
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
|
||||
@@ -238,14 +452,51 @@ class ProviderParser(HTMLParser):
|
||||
super().reset()
|
||||
|
||||
|
||||
def parse_providers(open_path):
|
||||
def parse_providers(url):
|
||||
""" Returns a list of providers sorted by logo [single channels after providers]. """
|
||||
parser = ProviderParser()
|
||||
parser.reset()
|
||||
|
||||
with open(open_path, encoding="utf-8", errors="replace") as f:
|
||||
parser.feed(f.read())
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
if request.status_code == 200:
|
||||
parser.feed(request.text)
|
||||
else:
|
||||
log("Parse providers error [{}]: {}".format(url, request.reason))
|
||||
|
||||
return parser.rows
|
||||
def srt(p):
|
||||
if p.logo is None:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
providers = parser.rows
|
||||
providers.sort(key=srt)
|
||||
|
||||
return providers
|
||||
|
||||
|
||||
def download_picon(src_url, dest_path, callback):
|
||||
""" Downloads and saves the picon to file. """
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
|
||||
if callback:
|
||||
callback("Downloading: {}.\n".format(os.path.basename(dest_path)))
|
||||
|
||||
req = requests.get(src_url, timeout=timeout, stream=True)
|
||||
if req.status_code != 200:
|
||||
err_msg = err_msg.format(src_url, req.reason)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
else:
|
||||
try:
|
||||
with open(dest_path, "wb") as f:
|
||||
for chunk in req:
|
||||
f.write(chunk)
|
||||
except OSError as e:
|
||||
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
|
||||
|
||||
@run_task
|
||||
|
||||
@@ -1,31 +1,83 @@
|
||||
""" 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 (Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0"}
|
||||
|
||||
|
||||
class SatelliteSource(Enum):
|
||||
FLYSAT = ("https://www.flysat.com/satlist.php",)
|
||||
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
|
||||
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
|
||||
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
|
||||
|
||||
@staticmethod
|
||||
def get_sources(src):
|
||||
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"}
|
||||
POS_PAT = re.compile(r".*?(\d+\.\d°?[EW]).*")
|
||||
|
||||
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
|
||||
|
||||
@@ -42,12 +94,14 @@ 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])
|
||||
for atr in attrs:
|
||||
if atr[0] == "href":
|
||||
self._current_row.append(atr[1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
@@ -55,16 +109,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 +134,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,33 +152,27 @@ 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")
|
||||
base_url = "https://www.lyngsat.com/"
|
||||
sats = []
|
||||
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], row[1], False)) # coupled [all in one] satellites
|
||||
sats.append((row[4], current_pos, row[5], row[3], False))
|
||||
if r_len == 8: # for a very limited number of satellites
|
||||
data = list(filter(None, row))
|
||||
urls = set()
|
||||
sat_type = ""
|
||||
for d in data:
|
||||
url = re.match(extra_pattern, d)
|
||||
if url:
|
||||
urls.add(url.group(0))
|
||||
if d in ("C", "Ku", "CKu"):
|
||||
sat_type = d
|
||||
current_pos = self.parse_position(data[1])
|
||||
for url in urls:
|
||||
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
|
||||
sats.append((name, current_pos, sat_type, url, False))
|
||||
elif r_len == 5:
|
||||
sats.append((row[2], current_pos, row[3], row[1], False))
|
||||
cur_pos = "0"
|
||||
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
|
||||
if not row[0]:
|
||||
row = row[1:]
|
||||
|
||||
pos = self.parse_position(row[1])
|
||||
if not self.POS_PAT.match(pos):
|
||||
if len(row) == 4 and row[0].endswith(".html"):
|
||||
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
|
||||
continue
|
||||
|
||||
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
|
||||
cur_pos = pos
|
||||
return sats
|
||||
elif source is SatelliteSource.KINGOFSAT:
|
||||
def get_sat(r):
|
||||
return r[3], self.parse_position(r[1]), None, r[0], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
|
||||
|
||||
def get_satellite(self, sat):
|
||||
pos = sat[1]
|
||||
@@ -144,16 +192,29 @@ class SatellitesParser(HTMLParser):
|
||||
def get_transponders(self, sat_url):
|
||||
""" 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)
|
||||
reason = request.reason
|
||||
trs = []
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
self.get_transponders_for_fly_sat(trs)
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
self.get_transponders_for_lyng_sat(trs)
|
||||
|
||||
url = sat_url
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
url = "https://www.flysat.com/" + sat_url
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
url = "https://en.kingofsat.net/" + sat_url
|
||||
|
||||
try:
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log("Getting transponders error: {}".format(e))
|
||||
else:
|
||||
if request.status_code == 200:
|
||||
self.feed(request.text)
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
self.get_transponders_for_fly_sat(trs)
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
self.get_transponders_for_lyng_sat(trs)
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self.get_transponders_for_king_of_sat(trs)
|
||||
else:
|
||||
log("SatellitesParser [get transponders] error: {} {}".format(url, request.reason))
|
||||
|
||||
return sorted(trs, key=lambda x: int(x.frequency))
|
||||
|
||||
@@ -209,42 +270,235 @@ class SatellitesParser(HTMLParser):
|
||||
trs.extend(n_trs)
|
||||
|
||||
def get_transponders_for_lyng_sat(self, trs):
|
||||
""" Parsing transponders for LyngSat """
|
||||
""" Parsing transponders for LyngSat. """
|
||||
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
|
||||
sr_fec_pattern = re.compile("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
|
||||
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
|
||||
re.IGNORECASE)
|
||||
sr_fec_pattern = re.compile(r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s*(?:T2-MI\s+PLP\s+(\d+))?.*")
|
||||
zeros = "000"
|
||||
pls_modes = {v: k for k, v in PLS_MODE.items()}
|
||||
pls_mode, pls_code, pls_id = None, None, None
|
||||
|
||||
for r in filter(lambda x: len(x) > 8, self._rows):
|
||||
for frq in r[1], r[2], r[3]:
|
||||
for row in filter(lambda x: len(x) > 8, self._rows):
|
||||
for frq in row[1], row[2], row[3]:
|
||||
freq = re.match(frq_pol_pattern, frq)
|
||||
if freq:
|
||||
break
|
||||
if not freq:
|
||||
continue
|
||||
|
||||
frq, pol = freq.group(1), freq.group(2)
|
||||
sr_fec = re.match(sr_fec_pattern, r[-3])
|
||||
srf = " ".join(row[3:5])
|
||||
sr_fec = re.search(sr_fec_pattern, srf)
|
||||
if not sr_fec:
|
||||
continue
|
||||
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
|
||||
|
||||
sys, mod, sr, fec = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3), sr_fec.group(4)
|
||||
mod = mod.strip() if mod else "Auto"
|
||||
|
||||
res = re.match(sys_pattern, r[-4])
|
||||
if not res:
|
||||
continue
|
||||
|
||||
sys = res.group(1)
|
||||
pls_mode = res.group(3)
|
||||
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
|
||||
pls_code = res.group(4)
|
||||
pls_id = res.group(6)
|
||||
pls_id = sr_fec.group(5)
|
||||
|
||||
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
def get_transponders_for_king_of_sat(self, trs):
|
||||
""" Getting transponders for KingOfSat source.
|
||||
|
||||
Since the *.ini file contains incomplete information, it is not used.
|
||||
"""
|
||||
zeros = "000"
|
||||
pat = re.compile(
|
||||
r"(\d+).00\s+([RLHV])\s+(DVB-S[2]?)\s+(?:T2-MI, PLP (\d+)\s+)?(.*PSK).*?(?:Stream\s+(\d+))?\s+(\d+)\s+(\d+/\d+)$")
|
||||
|
||||
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
|
||||
res = pat.search(" ".join((row[0], row[2], row[3], row[8], row[9], row[10])))
|
||||
if res:
|
||||
freq, sr, pol, fec, sys = res.group(1), res.group(7), res.group(2), res.group(8), res.group(3)
|
||||
mod, pls_id, pls_code = res.group(5), res.group(4), res.group(6)
|
||||
|
||||
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, pls_code, pls_id)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
|
||||
class ServicesParser(HTMLParser):
|
||||
""" Services parser for LYNGSAT source. """
|
||||
|
||||
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "MPEG-2/SD": "1", "SD": "1", "MPEG-4 SD": "22", "MPEG-4/SD": "22",
|
||||
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
|
||||
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
|
||||
"HEVC UHD": "31", "HEVC UHD 4K": "31"}
|
||||
self._TR_PAT = re.compile(
|
||||
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
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[0] for row in
|
||||
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].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"
|
||||
pos_found = False
|
||||
tr = None
|
||||
# Transponder
|
||||
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
|
||||
if not pos_found:
|
||||
pos_tr = re.match(self._POS_PAT, r[0].text)
|
||||
if not pos_tr:
|
||||
continue
|
||||
|
||||
if not sat_position:
|
||||
pos = int(SatellitesParser.get_position(
|
||||
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
|
||||
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
text = " ".join(c.text for c in r[1:])
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
if td.group(5):
|
||||
log("Detected T2-MI transponder!")
|
||||
continue
|
||||
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
|
||||
nid, tid = td.group(8), td.group(9)
|
||||
|
||||
neg_pos = False # POS = W
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
|
||||
inv = 2 # Default
|
||||
fec = get_key_by_value(FEC, _fec)
|
||||
sys = get_key_by_value(SYSTEM, sys)
|
||||
tr_flag = 1
|
||||
mod = get_key_by_value(MODULATION, mod)
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
|
||||
nid, tid = int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
if not tr:
|
||||
msg = "ServicesParser error [get transponder services]: {}"
|
||||
er = "Transponder [{}] not found or its type [T2-MI, etc] not supported yet.".format(freq)
|
||||
log(msg.format(er))
|
||||
return []
|
||||
|
||||
# Services
|
||||
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
|
||||
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)))
|
||||
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
except ValueError as e:
|
||||
log("ServicesParser error [get transponder services]: {}".format(e))
|
||||
|
||||
return services
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
246
app/tools/yt.py
246
app/tools/yt.py
@@ -1,16 +1,21 @@
|
||||
""" Module for working with YouTube service """
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
import shutil
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from json import JSONDecodeError
|
||||
from urllib.request import Request
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import unquote
|
||||
from urllib.request import Request, urlopen, urlretrieve
|
||||
|
||||
from app.commons import log
|
||||
from app.ui.uicommons import show_notification
|
||||
|
||||
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
|
||||
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{23,})?.*")
|
||||
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*")
|
||||
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0",
|
||||
"DNT": "1",
|
||||
@@ -20,9 +25,35 @@ Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
|
||||
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
|
||||
|
||||
|
||||
class YouTubeException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class YouTube:
|
||||
""" Helper class for working with YouTube service. """
|
||||
|
||||
_YT_INSTANCE = None
|
||||
_VIDEO_INFO_LINK = "https://youtube.com/get_video_info?video_id={}&hl=en"
|
||||
|
||||
VIDEO_LINK = "https://www.youtube.com/watch?v={}"
|
||||
|
||||
def __init__(self, settings, callback):
|
||||
self._settings = settings
|
||||
self._yt_dl = None
|
||||
self._callback = callback
|
||||
|
||||
if self._settings.enable_yt_dl:
|
||||
try:
|
||||
self._yt_dl = YouTubeDL.get_instance(self._settings, callback=self._callback)
|
||||
except YouTubeException:
|
||||
pass # NOP
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, settings, callback=log):
|
||||
if not cls._YT_INSTANCE:
|
||||
cls._YT_INSTANCE = YouTube(settings, callback)
|
||||
return cls._YT_INSTANCE
|
||||
|
||||
@staticmethod
|
||||
def is_yt_video_link(url):
|
||||
return re.match(_YT_VIDEO_PATTERN, url)
|
||||
@@ -41,17 +72,29 @@ class YouTube:
|
||||
if yt:
|
||||
return yt.group(1)
|
||||
|
||||
@staticmethod
|
||||
def get_yt_link(video_id):
|
||||
""" Getting link to YouTube video by id.
|
||||
def get_yt_link(self, video_id, url=None, skip_errors=False):
|
||||
""" Getting link to YouTube video by id or URL.
|
||||
|
||||
returns tuple from the video links dict and title
|
||||
Returns tuple from the video links dict and title.
|
||||
"""
|
||||
req = Request("https://youtube.com/get_video_info?video_id={}&hl=en".format(video_id), headers=_HEADERS)
|
||||
if self._settings.enable_yt_dl and url:
|
||||
if not self._yt_dl:
|
||||
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
|
||||
return self._yt_dl.get_yt_link(url, skip_errors)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=2) as resp:
|
||||
data = urllib.request.unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
|
||||
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(urllib.request.unquote, data))}
|
||||
return self.get_yt_link_by_id(video_id)
|
||||
|
||||
@staticmethod
|
||||
def get_yt_link_by_id(video_id):
|
||||
""" Getting link to YouTube video by id.
|
||||
|
||||
Returns tuple from the video links dict and title.
|
||||
"""
|
||||
req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS)
|
||||
|
||||
with urlopen(req, timeout=2) as resp:
|
||||
data = unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
|
||||
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(unquote, data))}
|
||||
player_resp = out.get("player_response", None)
|
||||
|
||||
if player_resp:
|
||||
@@ -67,7 +110,7 @@ class YouTube:
|
||||
|
||||
if fmts:
|
||||
urls = {Quality[i["itag"]]: i["url"] for i in
|
||||
filter(lambda i: i.get("itag", -1) in Quality, fmts)}
|
||||
filter(lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
|
||||
|
||||
if urls and title:
|
||||
return urls, title.replace("+", " ")
|
||||
@@ -76,7 +119,7 @@ class YouTube:
|
||||
if stream_map:
|
||||
s_map = {k: v for k, sep, v in (str(d).partition("=") for d in stream_map.split("&"))}
|
||||
url, title = s_map.get("url", None), out.get("title", None)
|
||||
url, title = urllib.request.unquote(url) if url else "", title.replace("+", " ") if title else ""
|
||||
url, title = unquote(url) if url else "", title.replace("+", " ") if title else ""
|
||||
if url and title:
|
||||
return {Quality[0]: url}, title.replace("+", " ")
|
||||
|
||||
@@ -86,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. """
|
||||
@@ -96,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":
|
||||
@@ -104,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:
|
||||
@@ -121,7 +183,8 @@ class PlayListParser(HTMLParser):
|
||||
|
||||
ct = resp.get("contents", None)
|
||||
if ct:
|
||||
for d in [(d["title"]["simpleText"], d["videoId"]) for d in flat("playlistVideoRenderer", ct)]:
|
||||
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
|
||||
|
||||
@@ -144,13 +207,162 @@ class PlayListParser(HTMLParser):
|
||||
"""
|
||||
request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS)
|
||||
|
||||
with urllib.request.urlopen(request, timeout=2) as resp:
|
||||
with urlopen(request, timeout=2) as resp:
|
||||
data = gzip.decompress(resp.read()).decode("utf-8")
|
||||
parser = PlayListParser()
|
||||
parser.feed(data)
|
||||
return parser.header, parser.playlist
|
||||
|
||||
|
||||
class YouTubeDL:
|
||||
""" Utility class [experimental] for working with youtube-dl.
|
||||
|
||||
[https://github.com/ytdl-org/youtube-dl]
|
||||
"""
|
||||
|
||||
_DL_INSTANCE = None
|
||||
_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.
|
||||
|
||||
def __init__(self, settings, callback):
|
||||
self._path = settings.default_data_path + "tools/"
|
||||
self._update = settings.enable_yt_dl_update
|
||||
self._supported = {"22", "18"}
|
||||
self._dl = None
|
||||
self._callback = callback
|
||||
self._download_exception = None
|
||||
self._is_update_process = False
|
||||
|
||||
self.init()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, settings, callback=print):
|
||||
if not cls._DL_INSTANCE:
|
||||
cls._DL_INSTANCE = YouTubeDL(settings, callback)
|
||||
return cls._DL_INSTANCE
|
||||
|
||||
def init(self):
|
||||
if not os.path.isfile(self._path + "youtube_dl/version.py"):
|
||||
self.get_latest_release()
|
||||
|
||||
if self._path not in sys.path:
|
||||
sys.path.append(self._path)
|
||||
|
||||
self.init_dl()
|
||||
|
||||
def init_dl(self):
|
||||
try:
|
||||
import youtube_dl
|
||||
except ModuleNotFoundError as e:
|
||||
log("YouTubeDLHelper error: {}".format(str(e)))
|
||||
raise YouTubeException(e)
|
||||
except ImportError as e:
|
||||
log("YouTubeDLHelper error: {}".format(str(e)))
|
||||
else:
|
||||
if self._update:
|
||||
if hasattr(youtube_dl.version, "__version__"):
|
||||
l_ver = self.get_last_release_id()
|
||||
cur_ver = youtube_dl.version.__version__
|
||||
if l_ver and youtube_dl.version.__version__ < l_ver:
|
||||
msg = "youtube-dl has new release!\nCurrent: {}. Last: {}.".format(cur_ver, l_ver)
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
self.get_latest_release()
|
||||
|
||||
self._DownloadError = youtube_dl.utils.DownloadError
|
||||
self._dl = youtube_dl.YoutubeDL(self._OPTIONS)
|
||||
msg = "youtube-dl initialized..."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
|
||||
@staticmethod
|
||||
def get_last_release_id():
|
||||
""" Getting last release id. """
|
||||
url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
|
||||
try:
|
||||
with urlopen(url, timeout=10) as resp:
|
||||
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
|
||||
except URLError as e:
|
||||
log("YouTubeDLHelper error [get last release id]: {}".format(e))
|
||||
|
||||
def get_latest_release(self):
|
||||
try:
|
||||
self._is_update_process = True
|
||||
log("Getting the last youtube-dl release...")
|
||||
|
||||
with urlopen(YouTubeDL._LATEST_RELEASE_URL, timeout=10) as resp:
|
||||
r = json.loads(resp.read().decode("utf-8"))
|
||||
zip_url = r.get("zipball_url", None)
|
||||
if zip_url:
|
||||
zip_file = self._path + "yt.zip"
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
f_name, headers = urlretrieve(zip_url, filename=zip_file)
|
||||
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(f_name) as arch:
|
||||
|
||||
if os.path.isdir(self._path):
|
||||
shutil.rmtree(self._path)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
|
||||
for info in arch.infolist():
|
||||
pref, sep, f = info.filename.partition("/youtube_dl/")
|
||||
if sep:
|
||||
arch.extract(info.filename)
|
||||
shutil.move(info.filename, "{}{}{}".format(self._path, sep, f))
|
||||
shutil.rmtree(pref)
|
||||
msg = "Getting the last youtube-dl release is done!"
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
return True
|
||||
except URLError as e:
|
||||
log("YouTubeDLHelper error: {}".format(e))
|
||||
raise YouTubeException(e)
|
||||
finally:
|
||||
self._is_update_process = False
|
||||
|
||||
def get_yt_link(self, url, skip_errors=False):
|
||||
""" Returns tuple from the video links [dict] and title. """
|
||||
if self._is_update_process:
|
||||
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:
|
||||
return self._dl.extract_info(url, download=False)
|
||||
except URLError as e:
|
||||
log(str(e))
|
||||
raise YouTubeException(e)
|
||||
except self._DownloadError as e:
|
||||
log(str(e))
|
||||
if not skip_errors:
|
||||
raise YouTubeException(e)
|
||||
|
||||
def update_options(self, options):
|
||||
self._dl.params.update(options)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._dl.params
|
||||
|
||||
|
||||
def flat(key, d):
|
||||
for k, v in d.items():
|
||||
if k == key:
|
||||
|
||||
@@ -8,7 +8,7 @@ from enum import Enum
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SettingsType
|
||||
from app.ui.dialogs import show_dialog, DialogType
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey
|
||||
|
||||
@@ -30,10 +30,7 @@ class BackupDialog:
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_release": self.on_key_release}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain("demon-editor")
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "backup_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "backup_dialog.glade", handlers)
|
||||
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
@@ -59,14 +56,13 @@ class BackupDialog:
|
||||
def show(self):
|
||||
self._dialog_window.show()
|
||||
|
||||
@run_idle
|
||||
def init_data(self):
|
||||
try:
|
||||
files = os.listdir(self._backup_path)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
for file in filter(lambda x: x.endswith(".zip"), files):
|
||||
if os.path.isdir(self._backup_path):
|
||||
for file in filter(lambda x: x.endswith(".zip"), os.listdir(self._backup_path)):
|
||||
self._model.append((file.rstrip(".zip"), False))
|
||||
else:
|
||||
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
|
||||
|
||||
def on_restore_bouquets(self, item):
|
||||
self.restore(RestoreType.BOUQUETS)
|
||||
@@ -129,6 +125,8 @@ class BackupDialog:
|
||||
append_text_to_tview(name + "\n", self._text_view)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self._text_view.get_buffer().set_text("")
|
||||
|
||||
def restore(self, restore_type):
|
||||
model, paths = self._main_view.get_selection().get_selected_rows()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dmitriy Yefremov
|
||||
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
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
@@ -144,6 +144,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
@@ -153,6 +154,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="stock">gtk-dialog-info</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
|
||||
1952
app/ui/control.glade
Normal file
1952
app/ui/control.glade
Normal file
File diff suppressed because it is too large
Load Diff
826
app/ui/control.py
Normal file
826
app/ui/control.py
Normal file
@@ -0,0 +1,826 @@
|
||||
""" 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 show_dialog, DialogType, get_message, get_builder
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI, UtfFTP
|
||||
from ..eparser.ecommons import BqServiceType
|
||||
|
||||
|
||||
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"
|
||||
RECORDINGS = "recordings"
|
||||
|
||||
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 or {}
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._title or ""
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return self._desc or ""
|
||||
|
||||
@property
|
||||
def time_header(self):
|
||||
return self._time_header or ""
|
||||
|
||||
class TimerRow(Gtk.ListBoxRow):
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "timer_row.glade"
|
||||
|
||||
def __init__(self, timer, **properties):
|
||||
super().__init__(**properties)
|
||||
|
||||
self._timer = timer
|
||||
|
||||
builder = get_builder(self._UI_PATH, None, use_str=True)
|
||||
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
|
||||
|
||||
class RecordingsRow(Gtk.ListBoxRow):
|
||||
def __init__(self, movie: dict, **properties):
|
||||
super().__init__(**properties)
|
||||
|
||||
self._movie = movie
|
||||
h_box = Gtk.HBox()
|
||||
h_box.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self._service = movie.get("e2servicename")
|
||||
service_label = Gtk.Label()
|
||||
service_label.set_markup("<b>{}</b>".format(self._service))
|
||||
|
||||
self._title = movie.get("e2title", "")
|
||||
title_label = Gtk.Label(self._title)
|
||||
|
||||
self._desc = movie.get("e2description", "")
|
||||
description = Gtk.Label()
|
||||
description.set_markup("<i>{}</i>".format(self._desc))
|
||||
description.set_line_wrap(True)
|
||||
description.set_max_width_chars(25)
|
||||
|
||||
start_time = datetime.fromtimestamp(int(movie.get("e2time", "0")))
|
||||
start_time_label = Gtk.Label()
|
||||
start_time_label.set_margin_top(5)
|
||||
start_time_label.set_markup("<b>{}</b>".format(start_time.strftime("%A, %H:%M")))
|
||||
|
||||
time_label = Gtk.Label()
|
||||
time_label.set_margin_top(5)
|
||||
time_label.set_markup("<b>{}</b>".format(movie.get("e2length", "0")))
|
||||
|
||||
info_box = Gtk.HBox()
|
||||
info_box.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
info_box.set_spacing(10)
|
||||
info_box.pack_start(start_time_label, False, True, 5)
|
||||
info_box.pack_end(time_label, False, True, 5)
|
||||
|
||||
h_box.add(service_label)
|
||||
h_box.add(title_label)
|
||||
h_box.add(description)
|
||||
h_box.add(info_box)
|
||||
sep = Gtk.Separator()
|
||||
sep.set_margin_top(5)
|
||||
h_box.add(sep)
|
||||
h_box.set_spacing(5)
|
||||
|
||||
self.set_tooltip_text(movie.get("e2filename", ""))
|
||||
self.add(h_box)
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def movie(self):
|
||||
return self._movie
|
||||
|
||||
@property
|
||||
def service(self):
|
||||
return self._service or ""
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._title or ""
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return self._desc or ""
|
||||
|
||||
@property
|
||||
def file(self):
|
||||
return self._movie.get("e2filename", "")
|
||||
|
||||
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,
|
||||
"on_recordings_press": self.on_recordings_press,
|
||||
"on_recording_filter_changed": self.on_recording_filter_changed,
|
||||
"on_recordings_dir_changed": self.on_recordings_dir_changed}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", 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()
|
||||
# Recordings.
|
||||
self._recordings_list_box = builder.get_object("recordings_list_box")
|
||||
self._recordings_list_box.set_filter_func(self.recording_filter_function)
|
||||
self._recordings_filter_entry = builder.get_object("recordings_filter_entry")
|
||||
self._recordings_dir_box = builder.get_object("recordings_dir_box")
|
||||
|
||||
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)
|
||||
# Recordings
|
||||
app.set_action("on_recording_remove", self.on_recording_remove)
|
||||
|
||||
@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 self.Tool.RECORDINGS:
|
||||
self.update_recordings_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, quote(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):
|
||||
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(quote(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 = quote(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:
|
||||
if service.service_type == BqServiceType.ALT.name:
|
||||
msg = "Alternative service.\n\n {}".format(get_message("Not implemented yet!"))
|
||||
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=msg)
|
||||
context.finish(False, False, time)
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
# *********************** Recordings *************************** #
|
||||
|
||||
def on_recordings_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._http_api.send(HttpAPI.Request.STREAM_TS,
|
||||
row.movie.get("e2filename", ""),
|
||||
self.on_play_recording)
|
||||
|
||||
def on_recording_filter_changed(self, entry):
|
||||
self._recordings_list_box.invalidate_filter()
|
||||
|
||||
def recording_filter_function(self, row):
|
||||
txt = self._recordings_filter_entry.get_text().upper()
|
||||
return any((not txt, txt in row.service.upper(), txt in row.title.upper(), txt in row.desc.upper()))
|
||||
|
||||
def on_recording_remove(self, action, value=None):
|
||||
""" Removes recordings via FTP. """
|
||||
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
rows = self._recordings_list_box.get_selected_rows()
|
||||
if rows:
|
||||
settings = self._app._settings
|
||||
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
for r in rows:
|
||||
resp = ftp.delete_file(r.file)
|
||||
if resp.startswith("2"):
|
||||
GLib.idle_add(self._recordings_list_box.remove, r)
|
||||
else:
|
||||
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=resp)
|
||||
break
|
||||
|
||||
def on_recordings_dir_changed(self, box: Gtk.ComboBoxText):
|
||||
self._http_api.send(HttpAPI.Request.RECORDINGS, quote(box.get_active_id()), self.update_recordings_data)
|
||||
|
||||
def update_recordings_list(self):
|
||||
if not len(self._recordings_dir_box.get_model()):
|
||||
self._http_api.send(HttpAPI.Request.REC_CURRENT, "", self.update_current_rec_dir)
|
||||
|
||||
def update_current_rec_dir(self, current):
|
||||
cur = current.get("e2location", None)
|
||||
if cur:
|
||||
self._recordings_dir_box.append(cur, cur)
|
||||
self._http_api.send(HttpAPI.Request.REC_DIRS, "", self.update_rec_dirs)
|
||||
|
||||
def update_rec_dirs(self, dirs):
|
||||
for d in dirs.get("rec_dirs", []):
|
||||
self._recordings_dir_box.append(d, d)
|
||||
|
||||
@run_idle
|
||||
def update_recordings_data(self, recordings):
|
||||
list(map(self._recordings_list_box.remove, (r for r in self._recordings_list_box)))
|
||||
list(map(lambda r: self._recordings_list_box.add(self.RecordingsRow(r)), recordings.get("recordings", [])))
|
||||
|
||||
def on_play_recording(self, m3u):
|
||||
url = self._app.get_url_from_m3u(m3u)
|
||||
if url:
|
||||
self._app.play(url)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -26,12 +26,12 @@ THE SOFTWARE.
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -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">0.4.7 Pre-alpha</property>
|
||||
<property name="copyright">2018-2020 Dmitriy Yefremov
|
||||
<property name="version">1.0.10 Beta</property>
|
||||
<property name="copyright">2018-2021 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">Это приложение распространяется без каких-либо гарантий.
|
||||
Подробнее в <a href="http://opensource.org/licenses/mit-license.php">The MIT License (MIT)</a>.</property>
|
||||
<property name="authors">Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="translator_credits" translatable="yes">translator-credits</property>
|
||||
<property name="artists">Program logo: <a href="http://ihad.tv"> mfgeg</a></property>
|
||||
<property name="artists">Program logo: <a href="http://ihad.tv">mfgeg</a></property>
|
||||
<property name="logo_icon_name">demon-editor</property>
|
||||
<property name="wrap_license">True</property>
|
||||
<property name="license_type">mit-x11</property>
|
||||
@@ -108,6 +108,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
@@ -175,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">
|
||||
@@ -194,6 +196,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkLabel" id="wait_dialog_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="label" translatable="yes">Loading data...</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -209,10 +213,10 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
</style>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
@@ -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
|
||||
@@ -52,12 +53,17 @@ class WaitDialog:
|
||||
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
|
||||
self._dialog = dialog
|
||||
self._dialog.set_transient_for(transient)
|
||||
if text is not None:
|
||||
builder.get_object("wait_dialog_label").set_text(text)
|
||||
self._label = builder.get_object("wait_dialog_label")
|
||||
self._default_text = text or self._label.get_text()
|
||||
|
||||
def show(self):
|
||||
def show(self, text=None):
|
||||
self.set_text(text)
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def set_text(self, text):
|
||||
self._label.set_text(get_message(text or self._default_text))
|
||||
|
||||
@run_idle
|
||||
def hide(self):
|
||||
self._dialog.hide()
|
||||
@@ -67,49 +73,56 @@ 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:
|
||||
return get_message_dialog(transient, DialogType.QUESTION, Gtk.ButtonsType.OK_CANCEL, text or "Are you sure?")
|
||||
action = action_type if action_type else Gtk.ButtonsType.OK_CANCEL
|
||||
return get_message_dialog(transient, DialogType.QUESTION, action, text or "Are you sure?")
|
||||
elif dialog_type is DialogType.ABOUT:
|
||||
return get_about_dialog(transient)
|
||||
|
||||
|
||||
def get_chooser_dialog(transient, settings, pattern, name):
|
||||
def get_chooser_dialog(transient, settings, name, patterns, title=None):
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.add_pattern(pattern)
|
||||
file_filter.set_name(name)
|
||||
for p in patterns:
|
||||
file_filter.add_pattern(p)
|
||||
|
||||
return show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
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
|
||||
@@ -172,5 +185,26 @@ def get_dialogs_string(path):
|
||||
return "".join(f)
|
||||
|
||||
|
||||
def get_builder(path, handlers=None, use_str=False, objects=None):
|
||||
""" Creates and returns a Gtk.Builder instance. """
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
|
||||
if use_str:
|
||||
if objects:
|
||||
builder.add_objects_from_string(get_dialogs_string(path).format(use_header=IS_GNOME_SESSION), objects)
|
||||
else:
|
||||
builder.add_from_string(get_dialogs_string(path).format(use_header=IS_GNOME_SESSION))
|
||||
else:
|
||||
if objects:
|
||||
builder.add_objects_from_file(path, objects)
|
||||
else:
|
||||
builder.add_from_file(path)
|
||||
|
||||
builder.connect_signals(handlers or {})
|
||||
|
||||
return builder
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -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>
|
||||
@@ -241,25 +241,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has_frame">False</property>
|
||||
<property name="has_entry">True</property>
|
||||
<signal name="changed" handler="on_profile_changed" swapped="no"/>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="has_tooltip">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="has_frame">False</property>
|
||||
<property name="max_width_chars">9</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -280,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>
|
||||
@@ -373,6 +358,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="receives_default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="on_remove_unused_bouquets_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -403,6 +389,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Use http to reload data in the receiver.</property>
|
||||
<property name="active">True</property>
|
||||
<signal name="state-set" handler="on_use_http_state_set" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -437,198 +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>
|
||||
</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>
|
||||
@@ -724,9 +519,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -2,13 +2,13 @@ import os
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.connections import download_data, DownloadType, upload_data
|
||||
from app.settings import SettingsType
|
||||
from app.ui.backup import backup_data, restore_data
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.settings_dialog import show_settings_dialog
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .dialogs import show_dialog, DialogType, get_message, get_builder
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
@@ -21,16 +21,14 @@ 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,
|
||||
"on_remove_unused_bouquets_toggled": self.on_remove_unused_bouquets_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
|
||||
|
||||
self._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")
|
||||
@@ -44,12 +42,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")
|
||||
@@ -64,15 +56,14 @@ class DownloadDialog:
|
||||
self.update_profiles()
|
||||
self.init_ui_settings()
|
||||
|
||||
@run_idle
|
||||
def init_ui_settings(self):
|
||||
self._host_entry.set_text(self._settings.host)
|
||||
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)
|
||||
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)
|
||||
|
||||
def update_profiles(self):
|
||||
self._profile_combo_box.remove_all()
|
||||
@@ -96,32 +87,12 @@ class DownloadDialog:
|
||||
elif self._satellites_radio_button.get_active():
|
||||
download_type = DownloadType.SATELLITES
|
||||
elif self._webtv_radio_button.get_active():
|
||||
download_type = DownloadType.WEB_TV
|
||||
download_type = DownloadType.WEBTV
|
||||
return download_type
|
||||
|
||||
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:
|
||||
@@ -130,25 +101,27 @@ 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:
|
||||
self._settings.current_profile = active
|
||||
self._profile_combo_box.set_active_id(active)
|
||||
self._s_type = self._settings.setting_type
|
||||
self.init_ui_settings()
|
||||
|
||||
def on_use_http_state_set(self, button, state):
|
||||
self._settings.use_http = state
|
||||
|
||||
def on_remove_unused_bouquets_toggled(self, button):
|
||||
self._settings.remove_unused_bouquets = button.get_active()
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@run_task
|
||||
def download(self, download, d_type):
|
||||
""" Download/upload data from/to receiver """
|
||||
self._expander.set_expanded(True)
|
||||
GLib.idle_add(self._expander.set_expanded, True)
|
||||
self.clear_output()
|
||||
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
|
||||
|
||||
@@ -170,8 +143,9 @@ class DownloadDialog:
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO),
|
||||
use_http=self._use_http_switch.get_active())
|
||||
except Exception as e:
|
||||
message = str(getattr(e, "message", str(e)))
|
||||
self.show_info_message(message, Gtk.MessageType.ERROR)
|
||||
msg = "Downloading data error: {}"
|
||||
log(msg.format(e), debug=self._settings.debug_mode, fmt_message=msg)
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
if all((download, backup, data_path)):
|
||||
restore_data(backup_src, data_path)
|
||||
else:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 Dmitriy Yefremov
|
||||
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
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="bouquet_list_store">
|
||||
<columns>
|
||||
@@ -653,6 +653,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="stock">gtk-spell-check</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -9,13 +9,13 @@ from urllib.error import HTTPError, URLError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.commons import run_idle, run_task
|
||||
from app.connections import download_data, DownloadType
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.tools.epg import EPG, ChannelsParser
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
|
||||
from .main_helper import on_popup_menu, update_entry_data
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey
|
||||
|
||||
|
||||
class RefsSource(Enum):
|
||||
@@ -67,10 +67,7 @@ class EpgDialog:
|
||||
self._show_tooltips = True
|
||||
self._download_xml_is_active = False
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "epg_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "epg_dialog.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("epg_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -486,7 +483,7 @@ class EpgDialog:
|
||||
epg_dat_path = self._settings.data_local_path + "epg/"
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
default_epg_data_stb_path = "/etc/enigma2"
|
||||
epg_options = self._settings.get("epg_options")
|
||||
epg_options = self._settings.epg_options
|
||||
if epg_options:
|
||||
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
|
||||
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
|
||||
@@ -506,15 +503,14 @@ class EpgDialog:
|
||||
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
|
||||
|
||||
def on_options_save(self, item=None):
|
||||
epg_options = {"xml_source": self._xml_radiobutton.get_active(),
|
||||
"use_web_source": self._use_web_source_switch.get_active(),
|
||||
"local_path_to_xml": self._xml_chooser_button.get_filename(),
|
||||
"url_to_xml": self._url_to_xml_entry.get_text(),
|
||||
"enable_filtering": self._enable_filtering_switch.get_active(),
|
||||
"epg_dat_path": self._epg_dat_path_entry.get_text(),
|
||||
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
|
||||
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
|
||||
self._settings.add("epg_options", epg_options)
|
||||
self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
|
||||
"use_web_source": self._use_web_source_switch.get_active(),
|
||||
"local_path_to_xml": self._xml_chooser_button.get_filename(),
|
||||
"url_to_xml": self._url_to_xml_entry.get_text(),
|
||||
"enable_filtering": self._enable_filtering_switch.get_active(),
|
||||
"epg_dat_path": self._epg_dat_path_entry.get_text(),
|
||||
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
|
||||
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
@@ -540,6 +536,7 @@ class EpgDialog:
|
||||
|
||||
# ***************** Downloads *********************#
|
||||
|
||||
@run_task
|
||||
def download_epg_from_stb(self):
|
||||
""" Download the epg.dat file via ftp from the receiver. """
|
||||
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
|
||||
|
||||
657
app/ui/ftp.glade
Normal file
657
app/ui/ftp.glade
Normal file
@@ -0,0 +1,657 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="bookmarks_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name url -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="file_create_folder_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="file_list_store">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name extra -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="ftp_create_folder_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="ftp_list_store">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name attr -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name extra -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkFrame" id="main_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_ftp_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="ftp_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="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="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="paned">
|
||||
<property name="width_request">320</property>
|
||||
<property name="height_request">240</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_bpx">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_info_box">
|
||||
<property name="visible">True</property>
|
||||
<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="label">FTP:</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">75</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="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">85</property>
|
||||
<property name="title" translatable="yes">Attr.</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_attr_column_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="pc_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="label" translatable="yes">PC:</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<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">75</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>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">list-remove</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image_2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">list-remove</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="rename_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">gtk-edit</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="ftp_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_create_folder_menu_item">
|
||||
<property name="label" translatable="yes">Create folder</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">ftp_create_folder_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="F7" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_edit_menu_item">
|
||||
<property name="label" translatable="yes">Edit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">rename_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_edit" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="F2" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_remove_menu_item">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="rename_image_2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">gtk-edit</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="file_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_create_folder_menu_item">
|
||||
<property name="label" translatable="yes">Create folder</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">file_create_folder_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="F7" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_edit_menu_item">
|
||||
<property name="label" translatable="yes">Edit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">rename_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_edit" object="file_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="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>
|
||||
572
app/ui/ftp.py
Normal file
572
app/ui/ftp.py
Normal file
@@ -0,0 +1,572 @@
|
||||
""" 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, get_builder
|
||||
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 = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
|
||||
|
||||
self.add(builder.get_object("main_frame"))
|
||||
self._ftp_info_label = builder.get_object("ftp_info_label")
|
||||
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
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 Dmitriy Yefremov
|
||||
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
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
@@ -119,6 +119,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
@@ -128,6 +129,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="stock">gtk-dialog-info</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
|
||||
|
||||
|
||||
def import_bouquet(transient, model, path, settings, services, appender):
|
||||
def import_bouquet(transient, model, path, settings, services, appender, file_path=None):
|
||||
""" Import of single bouquet """
|
||||
itr = model.get_iter(path)
|
||||
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
|
||||
@@ -30,7 +29,7 @@ def import_bouquet(transient, model, path, settings, services, appender):
|
||||
elif bq_type is BqType.WEBTV:
|
||||
f_pattern = "webtv.xml"
|
||||
|
||||
file_path = get_chooser_dialog(transient, settings, f_pattern, "bouquet files")
|
||||
file_path = file_path or get_chooser_dialog(transient, settings, "bouquet files", (f_pattern,))
|
||||
if file_path == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
@@ -63,7 +62,7 @@ def import_bouquet(transient, model, path, settings, services, appender):
|
||||
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
|
||||
|
||||
@@ -81,10 +80,7 @@ class ImportDialog:
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_press": self.on_key_press}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain("demon-editor")
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "import_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "import_dialog.glade", handlers)
|
||||
|
||||
self._bq_services = {}
|
||||
self._services = {}
|
||||
@@ -119,16 +115,25 @@ 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:
|
||||
self._main_model.append((bq.name, bq.type, True))
|
||||
self._bq_services[(bq.name, bq.type)] = bq.services
|
||||
# Note! Getting default format ver. 4
|
||||
services = get_services(path, self._profile, 4 if self._profile is SettingsType.ENIGMA_2 else 0)
|
||||
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
|
||||
elif self._profile is SettingsType.NEUTRINO_MP:
|
||||
services = get_services(path, self._profile, 0)
|
||||
else:
|
||||
self.show_info_message("Setting format not supported!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
for srv in services:
|
||||
self._services[srv.fav_id] = srv
|
||||
except FileNotFoundError as e:
|
||||
log("Import error [init data]: {}".format(e))
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_import(self, item):
|
||||
@@ -139,9 +144,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 +164,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()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkDialog" id="search_unavailable_streams_dialog">
|
||||
<property name="use-header-bar">1</property>
|
||||
@@ -48,9 +48,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="decorated">False</property>
|
||||
<property name="gravity">center</property>
|
||||
<signal name="response" handler="on_response" swapped="no"/>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="search_unavailable_dialog_box">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -222,6 +219,12 @@ Author: Dmitriy Yefremov
|
||||
<row>
|
||||
<col id="0">none-REC2</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">eServiceUri</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">eServiceHLS</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkDialog" id="iptv_dialog">
|
||||
@@ -267,9 +270,6 @@ Author: Dmitriy Yefremov
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="iptv_dialog_box">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -733,8 +733,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="gravity">center</property>
|
||||
<signal name="response" handler="on_response" swapped="no"/>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="close_config_list_button">
|
||||
<property name="label">gtk-close</property>
|
||||
<object class="GtkButton" id="cancel_config_list_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
@@ -743,15 +743,24 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="list_configuration_apply_button">
|
||||
<property name="label">gtk-apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
<object class="GtkButton" id="list_configuration_apply_button">
|
||||
<property name="label">gtk-apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="list_configuration_ok_button">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="iptv_list_configuration_dialog_box">
|
||||
@@ -774,7 +783,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<object class="GtkBox" id="reset_list_to_default_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
@@ -824,10 +833,12 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="reset_to_default_lists_switch">
|
||||
<object class="GtkButton" id="reset_list_to_default_butto">
|
||||
<property name="label" translatable="yes">Reset to default</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<signal name="state-set" handler="on_reset_to_default" object="start_values_frame" swapped="no"/>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_reset_to_default" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -836,20 +847,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="reset_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="label" translatable="yes">Reset to default</property>
|
||||
<property name="xalign">1</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -1218,7 +1215,9 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">close_config_list_button</action-widget>
|
||||
<action-widget response="-6">cancel_config_list_button</action-widget>
|
||||
<action-widget response="-10">list_configuration_apply_button</action-widget>
|
||||
<action-widget response="-5">list_configuration_ok_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
|
||||
497
app/ui/iptv.py
497
app/ui/iptv.py
@@ -1,23 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import concurrent.futures
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from functools import lru_cache
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, unquote, quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from gi.repository import GLib
|
||||
import requests
|
||||
from gi.repository import GLib, Gio, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
|
||||
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
|
||||
parse_m3u)
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import YouTube, PlayListParser
|
||||
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
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
|
||||
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
@@ -40,28 +68,16 @@ def get_stream_type(box):
|
||||
return StreamType.NONE_TS.value
|
||||
elif active == 2:
|
||||
return StreamType.NONE_REC_1.value
|
||||
return StreamType.NONE_REC_2.value
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_yt_icon(icon_name, size=24):
|
||||
""" Getting YouTube icon. If the icon is not found in the icon themes, the "Info" icon is returned by default! """
|
||||
default_theme = Gtk.IconTheme.get_default()
|
||||
if default_theme.has_icon(icon_name):
|
||||
return default_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
theme = Gtk.IconTheme.new()
|
||||
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
|
||||
theme.set_custom_theme(theme_name)
|
||||
if theme.has_icon(icon_name):
|
||||
return theme.load_icon(icon_name, size, 0)
|
||||
|
||||
return default_theme.load_icon("info", size, 0)
|
||||
elif active == 3:
|
||||
return StreamType.NONE_REC_2.value
|
||||
elif active == 4:
|
||||
return StreamType.E_SERVICE_URI.value
|
||||
return StreamType.E_SERVICE_HLS.value
|
||||
|
||||
|
||||
class IptvDialog:
|
||||
|
||||
def __init__(self, transient, view, services, bouquet, profile=SettingsType.ENIGMA_2, action=Action.ADD):
|
||||
def __init__(self, transient, view, services, bouquet, settings, action=Action.ADD):
|
||||
handlers = {"on_response": self.on_response,
|
||||
"on_entry_changed": self.on_entry_changed,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
@@ -70,17 +86,16 @@ class IptvDialog:
|
||||
"on_yt_quality_changed": self.on_yt_quality_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
self._action = action
|
||||
self._profile = profile
|
||||
self._s_type = settings.setting_type
|
||||
self._settings = settings
|
||||
self._bouquet = bouquet
|
||||
self._services = services
|
||||
self._yt_links = None
|
||||
self._yt_dl = None
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
|
||||
|
||||
self._dialog = builder.get_object("iptv_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -109,7 +124,7 @@ class IptvDialog:
|
||||
for el in self._digit_elems:
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
if profile is SettingsType.NEUTRINO_MP:
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
|
||||
builder.get_object("iptv_type_label").set_visible(False)
|
||||
builder.get_object("reference_entry").set_visible(False)
|
||||
@@ -122,8 +137,8 @@ class IptvDialog:
|
||||
if self._action is Action.ADD:
|
||||
self._save_button.set_visible(False)
|
||||
self._add_button.set_visible(True)
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
self._update_reference_entry()
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self.update_reference_entry()
|
||||
self._stream_type_combobox.set_active(1)
|
||||
elif self._action is Action.EDIT:
|
||||
self._current_srv = get_base_model(self._model)[self._paths][:]
|
||||
@@ -133,7 +148,7 @@ class IptvDialog:
|
||||
self._dialog.run()
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_save(self, item):
|
||||
@@ -144,16 +159,16 @@ class IptvDialog:
|
||||
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
self.save_enigma2_data() if self._profile is SettingsType.ENIGMA_2 else self.save_neutrino_data()
|
||||
self.save_enigma2_data() if self._s_type is SettingsType.ENIGMA_2 else self.save_neutrino_data()
|
||||
self._dialog.destroy()
|
||||
|
||||
def init_data(self, srv):
|
||||
name, fav_id = srv[2], srv[7]
|
||||
self._name_entry.set_text(name)
|
||||
self.init_enigma2_data(fav_id) if self._profile is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id)
|
||||
self.init_enigma2_data(fav_id) if self._s_type is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id)
|
||||
|
||||
def init_enigma2_data(self, fav_id):
|
||||
data, sep, desc = fav_id.partition("#DESCRIPTION")
|
||||
@@ -173,6 +188,10 @@ class IptvDialog:
|
||||
self._stream_type_combobox.set_active(2)
|
||||
elif stream_type is StreamType.NONE_REC_2:
|
||||
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)
|
||||
|
||||
@@ -181,16 +200,17 @@ class IptvDialog:
|
||||
self._tr_id_entry.set_text(str(int(data[4], 16)))
|
||||
self._net_id_entry.set_text(str(int(data[5], 16)))
|
||||
self._namespace_entry.set_text(str(int(data[6], 16)))
|
||||
self._url_entry.set_text(urllib.request.unquote(data[10].strip()))
|
||||
self._update_reference_entry()
|
||||
self._url_entry.set_text(unquote(data[10].strip()))
|
||||
self.update_reference_entry()
|
||||
|
||||
def init_neutrino_data(self, fav_id):
|
||||
data = fav_id.split("::")
|
||||
self._url_entry.set_text(data[0])
|
||||
self._description_entry.set_text(data[1])
|
||||
|
||||
def _update_reference_entry(self):
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
def update_reference_entry(self):
|
||||
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
|
||||
self.on_url_changed(self._url_entry)
|
||||
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
|
||||
self._srv_type_entry.get_text(),
|
||||
int(self._sid_entry.get_text()),
|
||||
@@ -206,12 +226,14 @@ class IptvDialog:
|
||||
entry.set_name(_DIGIT_ENTRY_NAME)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
self._update_reference_entry()
|
||||
self.update_reference_entry()
|
||||
|
||||
def on_url_changed(self, entry):
|
||||
url_str = entry.get_text()
|
||||
url = urlparse(url_str)
|
||||
entry.set_name("GtkEntry" if all([url.scheme, url.netloc, url.path]) else _DIGIT_ENTRY_NAME)
|
||||
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)
|
||||
if yt_id:
|
||||
@@ -229,10 +251,21 @@ class IptvDialog:
|
||||
|
||||
def set_yt_url(self, entry, video_id):
|
||||
try:
|
||||
links, title = YouTube.get_yt_link(video_id)
|
||||
if not self._yt_dl:
|
||||
def callback(message, error=True):
|
||||
msg_type = Gtk.MessageType.ERROR if error else Gtk.MessageType.INFO
|
||||
self.show_info_message(message, msg_type)
|
||||
|
||||
self._yt_dl = YouTube.get_instance(self._settings, callback=callback)
|
||||
yield True
|
||||
links, title = self._yt_dl.get_yt_link(video_id, entry.get_text())
|
||||
yield True
|
||||
except urllib.error.URLError as e:
|
||||
self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR)
|
||||
return
|
||||
except YouTubeException as e:
|
||||
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
|
||||
return
|
||||
else:
|
||||
if self._action is Action.ADD:
|
||||
self._name_entry.set_text(title)
|
||||
@@ -250,7 +283,9 @@ class IptvDialog:
|
||||
yield True
|
||||
|
||||
def on_stream_type_changed(self, item):
|
||||
self._update_reference_entry()
|
||||
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()
|
||||
|
||||
def on_yt_quality_changed(self, box):
|
||||
model = box.get_model()
|
||||
@@ -266,7 +301,7 @@ class IptvDialog:
|
||||
int(self._tr_id_entry.get_text()),
|
||||
int(self._net_id_entry.get_text()),
|
||||
int(self._namespace_entry.get_text()),
|
||||
urllib.request.quote(self._url_entry.get_text()),
|
||||
quote(self._url_entry.get_text()),
|
||||
name, name)
|
||||
self.update_bouquet_data(name, fav_id)
|
||||
|
||||
@@ -309,13 +344,11 @@ class IptvDialog:
|
||||
|
||||
class SearchUnavailableDialog:
|
||||
|
||||
def __init__(self, transient, model, fav_bouquet, iptv_rows, profile):
|
||||
def __init__(self, transient, model, fav_bouquet, iptv_rows, s_type):
|
||||
handlers = {"on_response": self.on_response}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade", ("search_unavailable_streams_dialog",))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "iptv.glade", handlers,
|
||||
objects=("search_unavailable_streams_dialog",))
|
||||
|
||||
self._dialog = builder.get_object("search_unavailable_streams_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -323,7 +356,7 @@ class SearchUnavailableDialog:
|
||||
self._counter_label = builder.get_object("streams_rows_counter_label")
|
||||
self._level_bar = builder.get_object("unavailable_streams_level_bar")
|
||||
self._bouquet = fav_bouquet
|
||||
self._profile = profile
|
||||
self._s_type = s_type
|
||||
self._iptv_rows = iptv_rows
|
||||
self._counter = -1
|
||||
self._max_rows = len(self._iptv_rows)
|
||||
@@ -351,7 +384,7 @@ class SearchUnavailableDialog:
|
||||
if not self._download_task:
|
||||
return
|
||||
try:
|
||||
req = Request(get_iptv_url(row, self._profile))
|
||||
req = Request(get_iptv_url(row, self._s_type))
|
||||
self.update_bar()
|
||||
urlopen(req, timeout=2)
|
||||
except HTTPError as e:
|
||||
@@ -391,9 +424,10 @@ class SearchUnavailableDialog:
|
||||
self._dialog.destroy()
|
||||
|
||||
|
||||
class IptvListConfigurationDialog:
|
||||
class IptvListDialog:
|
||||
""" Base class for working with iptv lists. """
|
||||
|
||||
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, profile):
|
||||
def __init__(self, transient, s_type):
|
||||
handlers = {"on_apply": self.on_apply,
|
||||
"on_response": self.on_response,
|
||||
"on_stream_type_default_togged": self.on_stream_type_default_togged,
|
||||
@@ -407,20 +441,15 @@ class IptvListConfigurationDialog:
|
||||
"on_entry_changed": self.on_entry_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("iptv_list_configuration_dialog", "stream_type_liststore"))
|
||||
builder.connect_signals(handlers)
|
||||
self._s_type = s_type
|
||||
|
||||
self._rows = iptv_rows
|
||||
self._services = services
|
||||
self._bouquet = bouquet
|
||||
self._fav_model = fav_model
|
||||
self._profile = profile
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("iptv_list_configuration_dialog", "stream_type_liststore"))
|
||||
|
||||
self._dialog = builder.get_object("iptv_list_configuration_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._data_box = builder.get_object("iptv_list_data_box")
|
||||
self._start_values_grid = builder.get_object("start_values_grid")
|
||||
self._info_bar = builder.get_object("list_configuration_info_bar")
|
||||
self._reference_label = builder.get_object("reference_label")
|
||||
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
|
||||
@@ -435,22 +464,28 @@ class IptvListConfigurationDialog:
|
||||
self._list_tid_entry = builder.get_object("list_tid_entry")
|
||||
self._list_nid_entry = builder.get_object("list_nid_entry")
|
||||
self._list_namespace_entry = builder.get_object("list_namespace_entry")
|
||||
self._reset_to_default_switch = builder.get_object("reset_to_default_lists_switch")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._apply_button = builder.get_object("list_configuration_apply_button")
|
||||
self._cancel_button = builder.get_object("cancel_config_list_button")
|
||||
self._ok_button = builder.get_object("list_configuration_ok_button")
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._default_elems = (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
|
||||
self._tid_check_button, self._nid_check_button, self._namespace_check_button)
|
||||
self._digit_elems = (self._list_srv_type_entry, self._list_sid_entry, self._list_tid_entry,
|
||||
self._list_nid_entry, self._list_namespace_entry)
|
||||
for el in self._digit_elems:
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
self._dialog.destroy()
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return True
|
||||
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_stream_type_changed(self, box):
|
||||
self.update_reference()
|
||||
@@ -486,34 +521,65 @@ class IptvListConfigurationDialog:
|
||||
self._list_namespace_entry.set_sensitive(not button.get_active())
|
||||
|
||||
@run_idle
|
||||
def on_reset_to_default(self, item, active):
|
||||
item.set_sensitive(not active)
|
||||
def on_reset_to_default(self, item):
|
||||
self._stream_type_combobox.set_active(1)
|
||||
self._list_srv_type_entry.set_text("1")
|
||||
for el in (self._list_sid_entry, self._list_nid_entry, self._list_tid_entry, self._list_namespace_entry):
|
||||
for el in self._digit_elems[1:]:
|
||||
el.set_text("0")
|
||||
for el in (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
|
||||
self._tid_check_button, self._nid_check_button, self._namespace_check_button):
|
||||
for el in self._default_elems:
|
||||
el.set_active(True)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_apply(self, item):
|
||||
pass
|
||||
|
||||
@run_idle
|
||||
def update_reference(self):
|
||||
if is_data_correct(self._digit_elems):
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
self._reference_label.set_text(
|
||||
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
if _PATTERN.search(entry.get_text()):
|
||||
entry.set_name(_DIGIT_ENTRY_NAME)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
self.update_reference()
|
||||
|
||||
def is_default_values(self):
|
||||
return any(el.get_text() == "0" for el in self._digit_elems[2:])
|
||||
|
||||
def is_all_data_default(self):
|
||||
return all(el.get_active() for el in self._default_elems)
|
||||
|
||||
|
||||
class IptvListConfigurationDialog(IptvListDialog):
|
||||
|
||||
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
|
||||
super().__init__(transient, s_type)
|
||||
|
||||
self._rows = iptv_rows
|
||||
self._bouquet = bouquet
|
||||
self._fav_model = fav_model
|
||||
self._services = services
|
||||
|
||||
@run_idle
|
||||
def on_apply(self, item):
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
reset = self._reset_to_default_switch.get_active()
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
type_default = self._type_check_button.get_active()
|
||||
tid_default = self._tid_check_button.get_active()
|
||||
sid_auto = self._sid_auto_check_button.get_active()
|
||||
nid_default = self._nid_check_button.get_active()
|
||||
namespace_default = self._namespace_check_button.get_active()
|
||||
|
||||
stream_type = StreamType.NONE_TS.value if reset else get_stream_type(self._stream_type_combobox)
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
|
||||
tid = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
|
||||
nid = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
|
||||
@@ -524,7 +590,7 @@ class IptvListConfigurationDialog:
|
||||
data, sep, desc = fav_id.partition("http")
|
||||
data = data.split(":")
|
||||
|
||||
if reset:
|
||||
if self.is_all_data_default():
|
||||
data[2], data[3], data[4], data[5], data[6] = "10000"
|
||||
else:
|
||||
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
|
||||
@@ -543,23 +609,217 @@ class IptvListConfigurationDialog:
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
|
||||
@run_idle
|
||||
def update_reference(self):
|
||||
if is_data_correct(self._digit_elems):
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
self._reference_label.set_text(
|
||||
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
if _PATTERN.search(entry.get_text()):
|
||||
entry.set_name(_DIGIT_ENTRY_NAME)
|
||||
class M3uImportDialog(IptvListDialog):
|
||||
""" Import dialog for *.m3u* playlists. """
|
||||
|
||||
def __init__(self, transient, s_type, m3_path, app):
|
||||
super().__init__(transient, s_type)
|
||||
|
||||
self._app = app
|
||||
self._picons = app._picons
|
||||
self._pic_path = app._settings.picons_local_path
|
||||
self._services = None
|
||||
self._url_count = 0
|
||||
self._errors_count = 0
|
||||
self._max_count = 0
|
||||
self._is_download = False
|
||||
self._cancellable = Gio.Cancellable()
|
||||
self._dialog.set_title(get_message("Playlist import"))
|
||||
self._dialog.connect("delete-event", self.on_close)
|
||||
self._apply_button.set_label(get_message("Import"))
|
||||
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
|
||||
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
|
||||
# Progress
|
||||
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
|
||||
self._spinner = Gtk.Spinner(active=False)
|
||||
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
|
||||
load_label = Gtk.Label(label=get_message("Loading data..."))
|
||||
self._spinner.bind_property("active", self._spinner, "visible")
|
||||
self._spinner.bind_property("visible", load_label, "visible")
|
||||
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
|
||||
|
||||
progress_box = Gtk.HBox(visible=True, spacing=2)
|
||||
progress_box.add(self._progress_bar)
|
||||
progress_box.pack_end(self._spinner, False, False, 0)
|
||||
progress_box.pack_start(load_label, False, False, 0)
|
||||
# Picons
|
||||
self._picons_switch = Gtk.Switch(visible=True)
|
||||
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=2)
|
||||
self._picon_box.pack_end(self._picons_switch, False, False, 0)
|
||||
self._picon_box.pack_end(Gtk.Label(visible=True, label=get_message("Download picons")), False, False, 0)
|
||||
# Extra box
|
||||
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
|
||||
extra_box.set_center_widget(progress_box)
|
||||
extra_box.pack_start(self._info_label, False, False, 5)
|
||||
extra_box.pack_end(self._picon_box, True, True, 5)
|
||||
|
||||
frame = Gtk.Frame(visible=True)
|
||||
frame.add(extra_box)
|
||||
self._data_box.add(frame)
|
||||
|
||||
self.get_m3u(m3_path, s_type)
|
||||
|
||||
@run_task
|
||||
def get_m3u(self, path, s_type):
|
||||
try:
|
||||
GLib.idle_add(self._spinner.set_property, "active", True)
|
||||
self._services = parse_m3u(path, s_type)
|
||||
for s in self._services:
|
||||
if s.picon:
|
||||
GLib.idle_add(self._picon_box.set_sensitive, True)
|
||||
break
|
||||
finally:
|
||||
msg = "{} {}.".format(get_message("Streams detected:"), len(self._services) if self._services else 0)
|
||||
GLib.idle_add(self._info_label.set_text, msg)
|
||||
GLib.idle_add(self._spinner.set_property, "active", False)
|
||||
|
||||
def on_apply(self, item):
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
picons = {}
|
||||
services = self._services
|
||||
|
||||
if not self.is_all_data_default():
|
||||
services = []
|
||||
params = [int(el.get_text()) for el in self._digit_elems]
|
||||
s_type = params[0]
|
||||
params = params[1:]
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
|
||||
for i, s in enumerate(self._services, start=params[0]):
|
||||
# Skipping markers.
|
||||
if not s.data_id:
|
||||
services.append(s)
|
||||
continue
|
||||
|
||||
params[0] = i
|
||||
picon_id = "{}_0_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".format(stream_type, s_type, *params)
|
||||
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, stream_type, s_type)
|
||||
if s.picon:
|
||||
picons[s.picon] = picon_id
|
||||
|
||||
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
|
||||
|
||||
if self._picons_switch.get_active():
|
||||
if self.is_default_values():
|
||||
show_dialog(DialogType.ERROR, self._dialog,
|
||||
"Set values for TID, NID and Namespace for correct naming of the picons!")
|
||||
return
|
||||
|
||||
self.download_picons(picons)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
self.update_reference()
|
||||
GLib.idle_add(self._ok_button.set_visible, True)
|
||||
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
self._app.append_imported_services(services)
|
||||
|
||||
@run_task
|
||||
def download_picons(self, picons):
|
||||
self._is_download = True
|
||||
os.makedirs(os.path.dirname(self._pic_path), exist_ok=True)
|
||||
GLib.idle_add(self._apply_button.set_sensitive, False)
|
||||
GLib.idle_add(self._progress_bar.set_visible, True)
|
||||
|
||||
self._errors_count = 0
|
||||
self._url_count = len(picons)
|
||||
self._max_count = self._url_count
|
||||
self._cancellable.reset()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {executor.submit(self.download_picon, p, picons.get(p, None)): p for p in filter(None, picons)}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_download and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
|
||||
self.update_progress(self._url_count)
|
||||
self.on_done()
|
||||
|
||||
def download_picon(self, url, pic_data):
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
|
||||
req = requests.get(url, timeout=timeout)
|
||||
if req.status_code != 200:
|
||||
log(err_msg.format(url, req.reason))
|
||||
self.update_progress(1)
|
||||
else:
|
||||
self.on_picon_load_done(req.content, pic_data)
|
||||
|
||||
@run_idle
|
||||
def on_picon_load_done(self, data, user_data):
|
||||
try:
|
||||
self._info_label.set_text("Processing: {}".format(user_data))
|
||||
f = Gio.MemoryInputStream.new_from_data(data)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 220, 132, False, self._cancellable)
|
||||
path = "{}{}".format(self._pic_path, user_data)
|
||||
pixbuf.savev(path, "png", [], [])
|
||||
self._picons[user_data] = get_picon_pixbuf(path)
|
||||
except GLib.GError as e:
|
||||
self.update_progress(1)
|
||||
if e.code != Gio.IOErrorEnum.CANCELLED:
|
||||
log("Loading picon [{}] data error: {}".format(user_data, e))
|
||||
else:
|
||||
self.update_progress()
|
||||
|
||||
@run_idle
|
||||
def update_progress(self, error=0):
|
||||
self._errors_count += error
|
||||
self._url_count -= 1
|
||||
frac = 1 - self._url_count / self._max_count
|
||||
self._progress_bar.set_fraction(frac)
|
||||
|
||||
@run_idle
|
||||
def on_done(self):
|
||||
self._progress_bar.set_visible(False)
|
||||
self._progress_bar.set_fraction(0.0)
|
||||
self._apply_button.set_sensitive(True)
|
||||
self._info_label.set_text("Errors: {}.".format(self._errors_count))
|
||||
self._is_download = False
|
||||
|
||||
gen = self.update_fav_model()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update_fav_model(self):
|
||||
services = self._app._services
|
||||
picons = self._app._picons
|
||||
model = self._app.fav_view.get_model()
|
||||
for r in model:
|
||||
s = services.get(r[Column.FAV_ID], None)
|
||||
if s:
|
||||
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
|
||||
yield True
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self._ok_button.set_visible(True)
|
||||
yield True
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return True
|
||||
|
||||
if response == Gtk.ResponseType.CANCEL and not self._is_download or not self.on_close():
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_close(self, window=None, event=None):
|
||||
if self._is_download:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
|
||||
self._is_download = False
|
||||
self._cancellable.cancel()
|
||||
return False
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class YtListImportDialog:
|
||||
def __init__(self, transient, profile, appender):
|
||||
def __init__(self, transient, settings, appender):
|
||||
handlers = {"on_import": self.on_import,
|
||||
"on_receive": self.on_receive,
|
||||
"on_yt_url_entry_changed": self.on_url_entry_changed,
|
||||
@@ -571,12 +831,17 @@ class YtListImportDialog:
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_close": self.on_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
|
||||
"yt_popup_menu", "remove_selection_image"))
|
||||
builder.connect_signals(handlers)
|
||||
self.appender = appender
|
||||
self._s_type = settings.setting_type
|
||||
self._download_task = False
|
||||
self._yt_list_id = None
|
||||
self._yt_list_title = None
|
||||
self._settings = settings
|
||||
self._yt = None
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
|
||||
"yt_popup_menu", "remove_selection_image"))
|
||||
|
||||
self._dialog = builder.get_object("yt_import_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -601,12 +866,6 @@ class YtListImportDialog:
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
self.appender = appender
|
||||
self._profile = profile
|
||||
self._download_task = False
|
||||
self._yt_list_id = None
|
||||
self._yt_list_title = None
|
||||
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
|
||||
@@ -620,7 +879,11 @@ class YtListImportDialog:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
||||
done_links = {}
|
||||
rows = list(filter(lambda r: r[2], self._model))
|
||||
futures = {executor.submit(YouTube.get_yt_link, r[1]): r for r in rows}
|
||||
if not self._yt:
|
||||
self._yt = YouTube.get_instance(self._settings)
|
||||
|
||||
futures = {executor.submit(self._yt.get_yt_link, r[1], YouTube.VIDEO_LINK.format(r[1]),
|
||||
True): r for r in rows}
|
||||
size = len(futures)
|
||||
counter = 0
|
||||
|
||||
@@ -632,6 +895,8 @@ class YtListImportDialog:
|
||||
done_links[futures[future]] = future.result()
|
||||
counter += 1
|
||||
self.update_progress_bar(counter / size)
|
||||
except YouTubeException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
except Exception as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
@@ -654,7 +919,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
|
||||
@@ -665,8 +932,8 @@ class YtListImportDialog:
|
||||
self.update_active_elements(True)
|
||||
|
||||
def update_links(self, links):
|
||||
for l in links:
|
||||
yield self._model.append((l[0], l[1], True, None))
|
||||
for link in links:
|
||||
yield self._model.append((link[0], link[1], True, None))
|
||||
|
||||
size = len(self._model)
|
||||
self._yt_count_label.set_text(str(size))
|
||||
@@ -686,11 +953,11 @@ class YtListImportDialog:
|
||||
|
||||
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
|
||||
for link in links:
|
||||
lnk, title = link
|
||||
lnk, title = link or (None, None)
|
||||
if not lnk:
|
||||
continue
|
||||
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
|
||||
fav_id = get_fav_id(ln, title, self._profile)
|
||||
fav_id = get_fav_id(ln, title, self._s_type)
|
||||
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
|
||||
srvs.append(srv)
|
||||
self.appender(srvs)
|
||||
|
||||
BIN
app/ui/lang/be/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/be/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/ui/lang/pl/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/pl/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/ui/lang/tr/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/tr/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
""" This is helper module for ui """
|
||||
""" Helper module for the ui. """
|
||||
import os
|
||||
import shutil
|
||||
import urllib.request
|
||||
from urllib.parse import unquote
|
||||
|
||||
from gi.repository import GdkPixbuf, GLib
|
||||
|
||||
@@ -16,23 +16,28 @@ from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON,
|
||||
|
||||
# ***************** Markers *******************#
|
||||
|
||||
def insert_marker(view, bouquets, selected_bouquet, services, parent_window):
|
||||
def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_type=BqServiceType.MARKER):
|
||||
"""" Inserts marker into bouquet services list. """
|
||||
response = show_dialog(DialogType.INPUT, parent_window)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
fav_id, text = "1:832:D:0:0:0:0:0:0:0:\n", None
|
||||
|
||||
if not response.strip():
|
||||
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
|
||||
return
|
||||
if m_type is BqServiceType.MARKER:
|
||||
response = show_dialog(DialogType.INPUT, parent_window)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
|
||||
s_type = BqServiceType.MARKER.name
|
||||
if not response.strip():
|
||||
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
|
||||
return
|
||||
|
||||
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
|
||||
text = response
|
||||
|
||||
s_type = m_type.name
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
marker = (None, None, response, None, None, s_type, None, fav_id, None, None, None)
|
||||
marker = (None, None, text, None, None, s_type, None, fav_id, None, None, None)
|
||||
itr = model.insert_before(model.get_iter(paths[0]), marker) if paths else model.insert(0, marker)
|
||||
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
|
||||
services[fav_id] = Service(None, None, None, response, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
|
||||
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
|
||||
|
||||
|
||||
# ***************** Movement *******************#
|
||||
@@ -41,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):
|
||||
@@ -156,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()):
|
||||
@@ -208,6 +217,7 @@ def set_flags(flag, services_view, fav_view, services, blacklist):
|
||||
if not paths:
|
||||
return
|
||||
|
||||
paths = get_base_paths(paths, model)
|
||||
model = get_base_model(model)
|
||||
|
||||
if flag is Flag.HIDE:
|
||||
@@ -236,13 +246,14 @@ def set_lock(blacklist, services, model, paths, target, services_model):
|
||||
locked = has_locked_hide(model, paths, col_num)
|
||||
|
||||
ids = []
|
||||
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
|
||||
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
|
||||
srv = services.get(fav_id, None)
|
||||
if srv:
|
||||
bq_id = to_bouquet_id(srv)
|
||||
if srv and srv.service_type not in skip_type:
|
||||
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
|
||||
if not bq_id:
|
||||
continue
|
||||
blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
|
||||
@@ -342,12 +353,12 @@ def scroll_to(index, view, paths=None):
|
||||
|
||||
# ***************** Picons *********************#
|
||||
|
||||
def update_picons_data(path, picons):
|
||||
def update_picons_data(path, picons, size=32):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
for file in os.listdir(path):
|
||||
pf = get_picon_pixbuf(path + file)
|
||||
pf = get_picon_pixbuf(path + file, size)
|
||||
if pf:
|
||||
picons[file] = pf
|
||||
|
||||
@@ -362,59 +373,79 @@ def append_picons(picons, model):
|
||||
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
|
||||
def assign_picon(target, srv_view, fav_view, transient, picons, settings, services):
|
||||
def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
|
||||
""" Assigning picons and returns picons files list. """
|
||||
view = srv_view if target is ViewTarget.SERVICES else fav_view
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
picons_files = []
|
||||
|
||||
response = get_chooser_dialog(transient, settings, "*.png", "png files")
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
if not src_path:
|
||||
src_path = get_chooser_dialog(transient, settings, "*.png files", ("*.png",))
|
||||
if src_path == Gtk.ResponseType.CANCEL:
|
||||
return picons_files
|
||||
|
||||
if not str(response).endswith(".png"):
|
||||
if not str(src_path).endswith(".png") or not os.path.isfile(src_path):
|
||||
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
|
||||
return
|
||||
return picons_files
|
||||
|
||||
picon_pos = Column.SRV_PICON
|
||||
model = get_base_model(model)
|
||||
itr = model.get_iter(paths)
|
||||
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
|
||||
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
|
||||
p_pos = Column.SRV_PICON
|
||||
col_num = Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID
|
||||
itrs = [model.get_iter(p) for p in paths]
|
||||
|
||||
if picon_id:
|
||||
if os.path.isfile(response):
|
||||
picons_path = settings.picons_local_path
|
||||
if target is ViewTarget.SERVICES:
|
||||
f_model = model.get_model()
|
||||
itrs = [f_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
|
||||
model = get_base_model(model)
|
||||
|
||||
for itr in itrs:
|
||||
fav_id = model.get_value(itr, col_num)
|
||||
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
|
||||
|
||||
if picon_id:
|
||||
picons_path = dst_path or settings.picons_local_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
picon_file = picons_path + picon_id
|
||||
shutil.copy(response, picon_file)
|
||||
picon = get_picon_pixbuf(picon_file)
|
||||
picons[picon_id] = picon
|
||||
model.set_value(itr, picon_pos, picon)
|
||||
if target is ViewTarget.SERVICES:
|
||||
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, picon_pos)
|
||||
try:
|
||||
shutil.copy(src_path, picon_file)
|
||||
except shutil.SameFileError:
|
||||
pass # NOP
|
||||
else:
|
||||
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, picon_pos)
|
||||
picons_files.append(picon_file)
|
||||
picon = get_picon_pixbuf(picon_file)
|
||||
picons[picon_id] = picon
|
||||
model.set_value(itr, p_pos, picon)
|
||||
if target is ViewTarget.SERVICES:
|
||||
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
|
||||
else:
|
||||
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
|
||||
|
||||
return picons_files
|
||||
|
||||
|
||||
def set_picon(fav_id, model, picon, fav_id_pos, picon_pos):
|
||||
for row in model:
|
||||
if row[fav_id_pos] == fav_id:
|
||||
row[picon_pos] = picon
|
||||
break
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def remove_picon(target, srv_view, fav_view, picons, settings):
|
||||
view = srv_view if target is ViewTarget.SERVICES else fav_view
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
model = get_base_model(model)
|
||||
|
||||
fav_ids = []
|
||||
picon_ids = []
|
||||
picon_pos = Column.SRV_PICON # picon position is equal for services and fav
|
||||
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
itrs = [model.get_iter(p) for p in paths]
|
||||
|
||||
if target is ViewTarget.SERVICES:
|
||||
f_model = model.get_model()
|
||||
itrs = [f_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
|
||||
model = get_base_model(model)
|
||||
|
||||
for itr in itrs:
|
||||
model.set_value(itr, picon_pos, None)
|
||||
if target is ViewTarget.SERVICES:
|
||||
fav_ids.append(model.get_value(itr, Column.SRV_FAV_ID))
|
||||
@@ -426,8 +457,10 @@ def remove_picon(target, srv_view, fav_view, picons, settings):
|
||||
else:
|
||||
fav_ids.append(fav_id)
|
||||
|
||||
fav_id_column = Column.FAV_ID if target is ViewTarget.SERVICES else Column.SRV_FAV_ID
|
||||
|
||||
def remove(md, path, it):
|
||||
if md.get_value(it, Column.FAV_ID if target is ViewTarget.SERVICES else Column.SRV_FAV_ID) in fav_ids:
|
||||
if md.get_value(it, fav_id_column) in fav_ids:
|
||||
md.set_value(it, picon_pos, None)
|
||||
if target is ViewTarget.FAV:
|
||||
picon_ids.append(md.get_value(it, Column.SRV_PICON_ID))
|
||||
@@ -488,16 +521,16 @@ def is_only_one_item_selected(paths, transient):
|
||||
return True
|
||||
|
||||
|
||||
def get_picon_pixbuf(path):
|
||||
def get_picon_pixbuf(path, size=32):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True)
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width=size, height=size, preserve_aspect_ratio=True)
|
||||
except GLib.GError as e:
|
||||
pass
|
||||
|
||||
|
||||
# ***************** Bouquets *********************#
|
||||
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
|
||||
""" Auto-generate and append list of bouquets """
|
||||
fav_id_index = Column.SRV_FAV_ID
|
||||
index = Column.SRV_TYPE
|
||||
@@ -512,8 +545,6 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback)
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
service = Service(*model[paths][:Column.SRV_TOOLTIP])
|
||||
if service.service_type not in tv_types:
|
||||
bq_type = BqType.RADIO.value
|
||||
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
|
||||
[service.package if gen_type is BqGenType.PACKAGE else
|
||||
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
|
||||
@@ -558,8 +589,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
|
||||
@@ -567,16 +598,32 @@ def update_entry_data(entry, dialog, settings):
|
||||
|
||||
|
||||
def get_base_model(model):
|
||||
""" Returns base tree model if has wrappers ("TreeModelSort" and "TreeModelFilter") """
|
||||
""" Returns base tree model if has wrappers [TreeModelSort, TreeModelFilter]. """
|
||||
if type(model) is Gtk.TreeModelSort:
|
||||
return model.get_model().get_model()
|
||||
return model
|
||||
|
||||
|
||||
def get_base_itrs(itrs, model):
|
||||
""" Returns base iters from wrapper models. """
|
||||
if type(model) is Gtk.TreeModelSort:
|
||||
filter_model = model.get_model()
|
||||
return [filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
|
||||
return itrs
|
||||
|
||||
|
||||
def get_base_paths(paths, model):
|
||||
""" Returns base paths from wrapper models. """
|
||||
if type(model) is Gtk.TreeModelSort:
|
||||
filter_model = model.get_model()
|
||||
return [filter_model.convert_path_to_child_path(model.convert_path_to_child_path(p)) for p in paths]
|
||||
return paths
|
||||
|
||||
|
||||
def get_model_data(view):
|
||||
""" Returns model name and base model from the given view """
|
||||
model = get_base_model(view.get_model())
|
||||
model_name = model.get_name()
|
||||
model_name = model.get_name() if model else ""
|
||||
return model_name, model
|
||||
|
||||
|
||||
@@ -595,7 +642,7 @@ def get_iptv_url(row, s_type):
|
||||
data = list(filter(lambda x: "http" in x, data))
|
||||
if data:
|
||||
url = data[0]
|
||||
return urllib.request.unquote(url) if s_type is SettingsType.ENIGMA_2 else url
|
||||
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
|
||||
|
||||
|
||||
def on_popup_menu(menu, event):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,377 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from gi.repository import GLib, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.connections import upload_data, DownloadType
|
||||
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
|
||||
from app.settings import SettingsType
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, TV_ICON
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .main_helper import update_entry_data, append_text_to_tview, scroll_to, on_popup_menu
|
||||
|
||||
|
||||
class PiconsDialog:
|
||||
def __init__(self, transient, settings, picon_ids, sat_positions):
|
||||
self._picon_ids = picon_ids
|
||||
self._sat_positions = sat_positions
|
||||
self._TMP_DIR = tempfile.gettempdir() + "/"
|
||||
self._BASE_URL = "www.lyngsat.com/packages/"
|
||||
self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
|
||||
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
|
||||
self._current_process = None
|
||||
self._terminate = False
|
||||
|
||||
handlers = {"on_receive": self.on_receive,
|
||||
"on_load_providers": self.on_load_providers,
|
||||
"on_cancel": self.on_cancel,
|
||||
"on_close": self.on_close,
|
||||
"on_send": self.on_send,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_picons_dir_open": self.on_picons_dir_open,
|
||||
"on_selected_toggled": self.on_selected_toggled,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
"on_position_edited": self.on_position_edited,
|
||||
"on_notebook_switch_page": self.on_notebook_switch_page,
|
||||
"on_convert": self.on_convert,
|
||||
"on_satellites_view_realize": self.on_satellites_view_realize,
|
||||
"on_satellite_selection": self.on_satellite_selection,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_popup_menu": on_popup_menu}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "picons_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
self._dialog = builder.get_object("picons_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._providers_tree_view = builder.get_object("providers_tree_view")
|
||||
self._satellites_tree_view = builder.get_object("satellites_tree_view")
|
||||
self._expander = builder.get_object("expander")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._ip_entry = builder.get_object("ip_entry")
|
||||
self._picons_entry = builder.get_object("picons_entry")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._picons_dir_entry = builder.get_object("picons_dir_entry")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._load_providers_button = builder.get_object("load_providers_button")
|
||||
self._receive_button = builder.get_object("receive_button")
|
||||
self._convert_button = builder.get_object("convert_button")
|
||||
self._enigma2_path_button = builder.get_object("enigma2_path_button")
|
||||
self._save_to_button = builder.get_object("save_to_button")
|
||||
self._send_button = builder.get_object("send_button")
|
||||
self._cancel_button = builder.get_object("cancel_button")
|
||||
self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
|
||||
self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
|
||||
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
|
||||
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
|
||||
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
|
||||
self._satellite_label = builder.get_object("satellite_label")
|
||||
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
|
||||
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
|
||||
self._cancel_button.bind_property("visible", builder.get_object("header_download_box"), "visible", 4)
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
self._ip_entry.set_text(self._settings.host)
|
||||
self._picons_entry.set_text(self._settings.picons_path)
|
||||
self._picons_path = self._settings.picons_local_path
|
||||
self._picons_dir_entry.set_text(self._picons_path)
|
||||
|
||||
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
|
||||
message = get_message("To automatically set the identifiers for picons,\n"
|
||||
"first load the required services list into the main application window.")
|
||||
self.show_info_message(message, Gtk.MessageType.WARNING)
|
||||
self._satellite_label.show()
|
||||
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
|
||||
def on_satellites_view_realize(self, view):
|
||||
self.get_satellites(view)
|
||||
|
||||
@run_task
|
||||
def get_satellites(self, view):
|
||||
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
|
||||
if not sats:
|
||||
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
|
||||
gen = self.append_satellites(view.get_model(), sats)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def append_satellites(self, model, sats):
|
||||
try:
|
||||
for sat in sats:
|
||||
pos = sat[1]
|
||||
name, pos = "{} ({})".format(sat[0], pos), "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
|
||||
|
||||
if not self._terminate and model:
|
||||
if pos in self._sat_positions:
|
||||
yield model.append((name, sat[3], pos))
|
||||
finally:
|
||||
self._satellite_label.show()
|
||||
|
||||
def on_satellite_selection(self, view, path, column):
|
||||
model = view.get_model()
|
||||
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
|
||||
|
||||
@run_idle
|
||||
def on_load_providers(self, item):
|
||||
self._expander.set_expanded(True)
|
||||
self.on_info_bar_close()
|
||||
self._cancel_button.show()
|
||||
url = self._url_entry.get_text()
|
||||
|
||||
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.clear()
|
||||
self.append_providers(url, model)
|
||||
|
||||
@run_task
|
||||
def append_providers(self, url, model):
|
||||
self._current_process.wait()
|
||||
try:
|
||||
self._terminate = False
|
||||
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
|
||||
except FileNotFoundError:
|
||||
pass # NOP
|
||||
else:
|
||||
if providers:
|
||||
for p in providers:
|
||||
if self._terminate:
|
||||
return
|
||||
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
|
||||
self.update_receive_button_state()
|
||||
finally:
|
||||
GLib.idle_add(self._cancel_button.hide)
|
||||
self._terminate = False
|
||||
|
||||
def get_pixbuf(self, img_url):
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
|
||||
width=48, height=48, preserve_aspect_ratio=True)
|
||||
|
||||
def on_receive(self, item):
|
||||
self._cancel_button.show()
|
||||
self.start_download()
|
||||
|
||||
@run_task
|
||||
def start_download(self):
|
||||
if self._current_process.poll() is None:
|
||||
self.show_dialog("The task is already running!", DialogType.ERROR)
|
||||
return
|
||||
|
||||
self._terminate = False
|
||||
self._expander.set_expanded(True)
|
||||
|
||||
providers = self.get_selected_providers()
|
||||
for prv in providers:
|
||||
if not self._POS_PATTERN.match(prv[2]):
|
||||
self.show_info_message(
|
||||
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
|
||||
scroll_to(prv.path, self._providers_tree_view)
|
||||
return
|
||||
|
||||
try:
|
||||
for prv in providers:
|
||||
if self._terminate:
|
||||
return
|
||||
self.process_provider(Provider(*prv))
|
||||
|
||||
if self._resize_no_radio_button.get_active():
|
||||
self.resize(self._picons_path)
|
||||
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
finally:
|
||||
GLib.idle_add(self._cancel_button.hide)
|
||||
self._terminate = False
|
||||
|
||||
def process_provider(self, prv):
|
||||
url = prv.url
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
|
||||
self._current_process.wait()
|
||||
path = self._TMP_DIR + (url[url.find("//") + 2:] if prv.single else self._BASE_URL + url[url.rfind("/") + 1:])
|
||||
PiconsParser.parse(path, self._picons_path, self._TMP_DIR, prv, self._picon_ids, self.get_picons_format())
|
||||
|
||||
def write_to_buffer(self, fd, condition):
|
||||
if condition == GLib.IO_IN:
|
||||
char = fd.read(1)
|
||||
self.append_output(char)
|
||||
return True
|
||||
return False
|
||||
|
||||
@run_idle
|
||||
def append_output(self, char):
|
||||
append_text_to_tview(char, self._text_view)
|
||||
|
||||
def resize(self, path):
|
||||
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
|
||||
command = "mogrify -resize {}! *.png".format(
|
||||
"320x240" if self._resize_220_132_radio_button.get_active() else "100x60").split()
|
||||
try:
|
||||
self._current_process = subprocess.Popen(command, universal_newlines=True, cwd=path)
|
||||
self._current_process.wait()
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message("Conversion error. " + str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_cancel(self, item=None):
|
||||
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return True
|
||||
|
||||
self.terminate_task()
|
||||
|
||||
@run_task
|
||||
def terminate_task(self):
|
||||
self._terminate = True
|
||||
|
||||
if self._current_process:
|
||||
self._current_process.terminate()
|
||||
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
|
||||
|
||||
def on_close(self, window, event):
|
||||
if self.on_cancel():
|
||||
return True
|
||||
|
||||
self.clean_data()
|
||||
GLib.idle_add(self._dialog.destroy)
|
||||
|
||||
@run_task
|
||||
def clean_data(self):
|
||||
path = self._TMP_DIR + "www.lyngsat.com"
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
def on_send(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self.upload_picons()
|
||||
|
||||
@run_task
|
||||
def upload_picons(self):
|
||||
if self.is_task_running():
|
||||
self.show_dialog("The task is already running!", DialogType.ERROR)
|
||||
return
|
||||
|
||||
try:
|
||||
GLib.idle_add(self._expander.set_expanded, True)
|
||||
upload_data(settings=self._settings,
|
||||
download_type=DownloadType.PICONS,
|
||||
callback=self.append_output,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
|
||||
except OSError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
|
||||
def on_picons_dir_open(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, settings=self._settings)
|
||||
|
||||
@run_idle
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.set_value(model.get_iter(path), 7, not toggle.get_active())
|
||||
self.update_receive_button_state()
|
||||
|
||||
def on_select_all(self, view):
|
||||
self.update_selection(view, True)
|
||||
|
||||
def on_unselect_all(self, view):
|
||||
self.update_selection(view, False)
|
||||
|
||||
def update_selection(self, view, select):
|
||||
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
|
||||
self.update_receive_button_state()
|
||||
|
||||
def on_url_changed(self, entry):
|
||||
suit = self._PATTERN.search(entry.get_text())
|
||||
entry.set_name("GtkEntry" if suit else "digit-entry")
|
||||
self._load_providers_button.set_sensitive(suit if suit else False)
|
||||
|
||||
def on_position_edited(self, render, path, value):
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.set_value(model.get_iter(path), 2, value)
|
||||
|
||||
@run_idle
|
||||
def on_notebook_switch_page(self, nb, box, tab_num):
|
||||
self._load_providers_button.set_visible(not tab_num)
|
||||
self._receive_button.set_visible(not tab_num)
|
||||
self._convert_button.set_visible(tab_num)
|
||||
self._send_button.set_visible(not tab_num)
|
||||
|
||||
@run_idle
|
||||
def on_convert(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
picons_path = self._enigma2_path_button.get_filename()
|
||||
save_path = self._save_to_button.get_filename()
|
||||
if not picons_path or not save_path:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
|
||||
return
|
||||
|
||||
self._expander.set_expanded(True)
|
||||
convert_to(src_path=picons_path,
|
||||
dest_path=save_path,
|
||||
s_type=SettingsType.ENIGMA_2,
|
||||
callback=self.append_output,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
|
||||
|
||||
@run_idle
|
||||
def update_receive_button_state(self):
|
||||
try:
|
||||
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
|
||||
except TypeError:
|
||||
pass # NOP
|
||||
|
||||
def get_selected_providers(self):
|
||||
""" returns selected providers """
|
||||
return [r for r in self._providers_tree_view.get_model() if r[7]]
|
||||
|
||||
@run_idle
|
||||
def show_dialog(self, message, dialog_type):
|
||||
show_dialog(dialog_type, self._dialog, message)
|
||||
|
||||
def get_picons_format(self):
|
||||
picon_format = SettingsType.ENIGMA_2
|
||||
|
||||
if self._neutrino_mp_radio_button.get_active():
|
||||
picon_format = SettingsType.NEUTRINO_MP
|
||||
|
||||
return picon_format
|
||||
|
||||
def is_task_running(self):
|
||||
return self._current_process and self._current_process.poll() is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1807
app/ui/picons_manager.glade
Normal file
1807
app/ui/picons_manager.glade
Normal file
File diff suppressed because it is too large
Load Diff
1009
app/ui/picons_manager.py
Normal file
1009
app/ui/picons_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 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
|
||||
|
||||
-->
|
||||
<!--
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -55,9 +28,10 @@ Author: Dmitriy Yefremov
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="fec_store">
|
||||
<columns>
|
||||
@@ -426,12 +400,12 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Satellites edit tool</property>
|
||||
<property name="spacing">1</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="left_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="file_header_menu_button">
|
||||
<property name="visible">True</property>
|
||||
@@ -541,11 +515,14 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="satellite_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">20</property>
|
||||
<property name="min_width">250</property>
|
||||
<property name="title" translatable="yes">Satellite</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="satellite_cellrenderertext"/>
|
||||
<object class="GtkCellRendererText" id="satellite_cellrenderertext">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
@@ -558,6 +535,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Freq</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="frequency_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -572,6 +550,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Rate</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sat_rate_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -586,6 +565,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Pol</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sat_pol_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -600,6 +580,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">FEC</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="set_fec_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -614,6 +595,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">System</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sys_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -628,6 +610,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Mod</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="mod_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -727,6 +710,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="title" translatable="yes">Satellite</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
@@ -1312,21 +1296,35 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkTreeModelSort" id="update_sat_list_model_sort">
|
||||
<property name="model">update_sat_list_model_filter</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="update_source_store">
|
||||
<object class="GtkListStore" id="update_service_store">
|
||||
<columns>
|
||||
<!-- column-name source -->
|
||||
<!-- 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>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">FlySat</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">LyngSat</col>
|
||||
</row>
|
||||
</data>
|
||||
</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>
|
||||
@@ -1336,11 +1334,10 @@ 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>
|
||||
<property name="spacing">2</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="satellites_update_left_header_box">
|
||||
@@ -1363,22 +1360,20 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="source_combo_box">
|
||||
<object class="GtkComboBoxText" id="source_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">update_source_store</property>
|
||||
<property name="active">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="source_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<items>
|
||||
<item id="FLYSAT" translatable="yes">FlySat</item>
|
||||
<item id="LYNGSAT" translatable="yes">LyngSat</item>
|
||||
<item id="KINGOFSAT" translatable="yes">KingOfSat</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1396,6 +1391,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>
|
||||
@@ -1409,9 +1405,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="satellites_update_right_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</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>
|
||||
@@ -1432,13 +1429,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>
|
||||
@@ -1479,6 +1477,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="stock">gtk-select-all</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1501,6 +1500,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="stock">gtk-find</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1593,11 +1593,14 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1720,97 +1723,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>
|
||||
@@ -1827,7 +2023,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>
|
||||
@@ -1839,6 +2034,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
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 .search import SearchProvider
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION
|
||||
from .dialogs import show_dialog, DialogType, get_dialogs_string
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
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, MOVE_KEYS, KeyboardKey, MOD_MASK
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
|
||||
|
||||
@@ -44,12 +44,9 @@ class SatellitesDialog:
|
||||
"on_resize": self.on_resize,
|
||||
"on_quit": self.on_quit}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
|
||||
("satellites_editor_window", "satellites_tree_store", "popup_menu",
|
||||
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("satellites_editor_window", "satellites_tree_store", "popup_menu",
|
||||
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
|
||||
|
||||
self._window = builder.get_object("satellites_editor_window")
|
||||
self._window.set_transient_for(transient)
|
||||
@@ -84,27 +81,17 @@ class SatellitesDialog:
|
||||
|
||||
@run_idle
|
||||
def on_open(self, model):
|
||||
response = self.get_file_dialog_response(Gtk.FileChooserAction.OPEN)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
response = get_chooser_dialog(self._window, self._settings, "satellites.xml", ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
if not str(response).endswith("satellites.xml"):
|
||||
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
|
||||
return
|
||||
|
||||
self._data_path = response
|
||||
self.load_satellites_list(model)
|
||||
|
||||
def get_file_dialog_response(self, action: Gtk.FileChooserAction):
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.add_pattern("satellites.xml")
|
||||
file_filter.set_name("satellites.xml")
|
||||
response = show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
transient=self._window,
|
||||
settings=self._settings,
|
||||
action_type=action,
|
||||
file_filter=file_filter)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def on_row_activated(view, path, column):
|
||||
if view.row_expanded(path):
|
||||
@@ -124,7 +111,7 @@ class SatellitesDialog:
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_remove(view)
|
||||
@@ -263,13 +250,22 @@ class SatellitesDialog:
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def on_remove(view):
|
||||
@run_idle
|
||||
def on_remove(self, view):
|
||||
""" Removal of selected satellites and transponders.
|
||||
|
||||
The satellites are removed first! Then transponders.
|
||||
"""
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
|
||||
for itr in [model.get_iter(path) for path in paths]:
|
||||
model.remove(itr)
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
satellites = list(filter(model.iter_has_child, itrs))
|
||||
if len(satellites):
|
||||
# Removing selected satellites.
|
||||
list(map(model.remove, satellites))
|
||||
else:
|
||||
# Removing selected transponders.
|
||||
list(map(model.remove, itrs))
|
||||
|
||||
@run_idle
|
||||
def on_save(self, view):
|
||||
@@ -289,7 +285,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):
|
||||
@@ -316,13 +312,8 @@ class TransponderDialog:
|
||||
def __init__(self, transient, transponder: Transponder = None):
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
|
||||
"pls_mode_store"))
|
||||
builder.connect_signals(handlers)
|
||||
objects = ("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True, objects=objects)
|
||||
|
||||
self._dialog = builder.get_object("transponder_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -336,7 +327,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")
|
||||
@@ -401,10 +392,7 @@ class SatelliteDialog:
|
||||
""" Shows dialog for adding or edit satellite """
|
||||
|
||||
def __init__(self, transient, satellite: Satellite = None):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
|
||||
self._dialog = builder.get_object("satellite_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -438,16 +426,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,
|
||||
@@ -460,26 +449,33 @@ class SatellitesUpdateDialog:
|
||||
"on_search_up": self.on_search_up,
|
||||
"on_quit": self.on_quit}
|
||||
|
||||
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",
|
||||
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 = get_builder(UI_RESOURCES_PATH + "satellites_dialog.glade", handlers,
|
||||
objects=("satellites_update_window", "update_source_store", "update_sat_list_store",
|
||||
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
|
||||
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
|
||||
"remove_selection_image"))
|
||||
builder.connect_signals(handlers)
|
||||
"remove_selection_image", "update_transponder_store", "update_service_store"))
|
||||
|
||||
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")
|
||||
@@ -495,21 +491,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()
|
||||
@@ -518,10 +524,16 @@ class SatellitesUpdateDialog:
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
|
||||
sat_src = SatelliteSource.FLYSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
elif src == 2:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
if sats:
|
||||
callback(sats)
|
||||
self._download_task = False
|
||||
self.is_download = False
|
||||
|
||||
@run_idle
|
||||
def append_satellites(self, sats):
|
||||
@@ -530,70 +542,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):
|
||||
@@ -606,11 +564,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)))
|
||||
@@ -635,7 +597,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
|
||||
|
||||
@@ -646,7 +608,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)
|
||||
@@ -679,10 +641,299 @@ 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)
|
||||
self._source_box.remove(0)
|
||||
self._source_box.remove(1)
|
||||
self._source_box.set_active(0)
|
||||
# Transponder view popup menu
|
||||
tr_popup_menu = Gtk.Menu()
|
||||
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
|
||||
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 LameDbReader
|
||||
# Used for double checking!
|
||||
reader = LameDbReader(path=None)
|
||||
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
|
||||
except ValueError as e:
|
||||
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
|
||||
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):
|
||||
|
||||
@@ -31,6 +31,8 @@ class SearchProvider:
|
||||
if self._max_indexes > 0:
|
||||
self.on_search_down()
|
||||
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def scroll_to(self, index):
|
||||
view, path = self._paths[index]
|
||||
view.scroll_to_cell(path, None)
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="fec_list_store">
|
||||
<columns>
|
||||
<!-- column-name fec -->
|
||||
@@ -9,40 +40,53 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">1/2</col>
|
||||
<col id="0">1/2</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">2/3</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/4</col>
|
||||
<col id="0">3/4</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">5/6</col>
|
||||
<col id="0">5/6</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">7/8</col>
|
||||
<col id="0">7/8</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">8/9</col>
|
||||
<col id="0">8/9</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/5</col>
|
||||
<col id="0">3/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">4/5</col>
|
||||
<col id="0">4/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">6/7</col>
|
||||
<col id="0">6/7</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">9/10</col>
|
||||
<col id="0">9/10</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkComboBox" id="rate_lp_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">fec_list_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rate_lp_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="invertion_list_store">
|
||||
<columns>
|
||||
<!-- column-name invertion -->
|
||||
@@ -50,13 +94,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Off</col>
|
||||
<col id="0">Off</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">On</col>
|
||||
<col id="0">On</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -67,19 +111,19 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">QPSK</col>
|
||||
<col id="0">QPSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">8PSK</col>
|
||||
<col id="0">8PSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">16APSK</col>
|
||||
<col id="0">16APSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">32APSK</col>
|
||||
<col id="0">32APSK</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -90,13 +134,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Off</col>
|
||||
<col id="0">Off</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">On</col>
|
||||
<col id="0">On</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -107,13 +151,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Root</col>
|
||||
<col id="0">Root</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Gold</col>
|
||||
<col id="0">Gold</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Combo</col>
|
||||
<col id="0">Combo</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -124,16 +168,16 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">H</col>
|
||||
<col id="0">H</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">V</col>
|
||||
<col id="0">V</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">R</col>
|
||||
<col id="0">R</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">L</col>
|
||||
<col id="0">L</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -144,21 +188,20 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">35%</col>
|
||||
<col id="0">35%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">25%</col>
|
||||
<col id="0">25%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">20%</col>
|
||||
<col id="0">20%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkAdjustment" id="sat_pos_adjustment">
|
||||
<property name="lower">-180</property>
|
||||
<property name="upper">180</property>
|
||||
<property name="step_increment">0.10000000000000001</property>
|
||||
<property name="page_increment">10</property>
|
||||
@@ -210,26 +253,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">DVB-S</col>
|
||||
<col id="0">DVB-S</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">DVB-S2</col>
|
||||
<col id="0">DVB-S2</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkComboBox" id="rate_lp_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">fec_list_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rate_lp_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkDialog" id="service_details_dialog">
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="can_focus">False</property>
|
||||
@@ -255,6 +285,7 @@
|
||||
<property name="tooltip_text" translatable="yes">Cancel</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
@@ -314,7 +345,7 @@
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="srv_grid">
|
||||
<property name="visible">True</property>
|
||||
@@ -776,10 +807,14 @@
|
||||
<object class="GtkBox" id="flags_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="flags_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="flags_label">
|
||||
@@ -852,6 +887,47 @@
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="extra_flags_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="extra_pids_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Extra:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="extra_pids_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">20</property>
|
||||
<property name="max_width_chars">20</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="placeholder_text">c:000000,etc.</property>
|
||||
<signal name="changed" handler="on_extra_pids_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="caids_grid">
|
||||
<property name="visible">True</property>
|
||||
@@ -862,10 +938,10 @@
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text">C:0000,C:a1b2,etc.</property>
|
||||
<property name="width_chars">15</property>
|
||||
<property name="max_width_chars">26</property>
|
||||
<property name="width_chars">20</property>
|
||||
<property name="max_width_chars">20</property>
|
||||
<property name="primary_icon_stock">gtk-edit</property>
|
||||
<property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property>
|
||||
<property name="placeholder_text">C:0000,C:a1b2,etc.</property>
|
||||
<signal name="changed" handler="on_cas_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -889,7 +965,7 @@
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -1092,21 +1168,6 @@
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="sat_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="input_purpose">number</property>
|
||||
<property name="adjustment">sat_pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tid_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -1161,6 +1222,50 @@
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="sat_pos_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="sat_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="input_purpose">number</property>
|
||||
<property name="adjustment">sat_pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="pos_side_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item id="E">E</item>
|
||||
<item id="W">W</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
import re
|
||||
import os
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.commons import run_idle
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from app.commons import run_idle, log
|
||||
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, A_MODULATION)
|
||||
from app.settings import SettingsType
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
|
||||
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
|
||||
from .dialogs import show_dialog, DialogType, Action, get_builder
|
||||
from .main_helper import get_base_model
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
|
||||
|
||||
@@ -41,19 +70,18 @@ class ServiceDetailsDialog:
|
||||
"on_tr_edit_toggled": self.on_tr_edit_toggled,
|
||||
"update_reference": self.update_reference,
|
||||
"on_cas_entry_changed": self.on_cas_entry_changed,
|
||||
"on_extra_pids_entry_changed": self.on_extra_pids_entry_changed,
|
||||
"on_digit_entry_changed": self.on_digit_entry_changed,
|
||||
"on_non_empty_entry_changed": self.on_non_empty_entry_changed}
|
||||
"on_non_empty_entry_changed": self.on_non_empty_entry_changed,
|
||||
"on_cancel": lambda item: self._dialog.destroy()}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True)
|
||||
self._builder = builder
|
||||
|
||||
self._dialog = builder.get_object("service_details_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._s_type = settings.setting_type
|
||||
self._tr_type = None
|
||||
self._tr_type = TrType.Satellite
|
||||
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
|
||||
self._picons_dir_path = settings.picons_local_path
|
||||
self._services_view = srv_view
|
||||
@@ -70,6 +98,7 @@ class ServiceDetailsDialog:
|
||||
self._DIGIT_PATTERN = re.compile("\\D")
|
||||
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
|
||||
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
|
||||
# Buttons
|
||||
self._apply_button = builder.get_object("apply_button")
|
||||
self._create_button = builder.get_object("create_button")
|
||||
@@ -105,6 +134,7 @@ class ServiceDetailsDialog:
|
||||
self._stream_id_entry = self._digit_elements.get("stream_id_entry")
|
||||
self._tr_flag_entry = self._digit_elements.get("tr_flag_entry")
|
||||
self._namespace_entry = self._non_empty_elements.get("namespace_entry")
|
||||
self._extra_pids_entry = builder.get_object("extra_pids_entry")
|
||||
# Service elements
|
||||
self._name_entry = builder.get_object("name_entry")
|
||||
self._package_entry = builder.get_object("package_entry")
|
||||
@@ -119,6 +149,7 @@ class ServiceDetailsDialog:
|
||||
self._pids_grid = builder.get_object("pids_grid")
|
||||
# Transponder elements
|
||||
self._sat_pos_button = builder.get_object("sat_pos_button")
|
||||
self._pos_side_box = builder.get_object("pos_side_box")
|
||||
self._pol_combo_box = builder.get_object("pol_combo_box")
|
||||
self._fec_combo_box = builder.get_object("fec_combo_box")
|
||||
self._rate_lp_combo_box = builder.get_object("rate_lp_combo_box")
|
||||
@@ -136,7 +167,7 @@ class ServiceDetailsDialog:
|
||||
self._TRANSPONDER_ELEMENTS = (self._sat_pos_button, self._pol_combo_box, self._invertion_combo_box,
|
||||
self._sys_combo_box, self._freq_entry, self._transponder_id_entry,
|
||||
self._network_id_entry, self._namespace_entry, self._fec_combo_box,
|
||||
self._rate_entry, self._rate_lp_combo_box)
|
||||
self._rate_entry, self._rate_lp_combo_box, self._pos_side_box)
|
||||
|
||||
if self._action is Action.EDIT:
|
||||
self.update_data_elements()
|
||||
@@ -144,12 +175,7 @@ class ServiceDetailsDialog:
|
||||
self.init_default_data_elements()
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
pass
|
||||
self._dialog.destroy()
|
||||
|
||||
return response
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def init_default_data_elements(self):
|
||||
@@ -208,6 +234,8 @@ class ServiceDetailsDialog:
|
||||
self.update_ui_for_terrestrial()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
self.update_ui_for_cable()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
self.update_ui_for_atsc()
|
||||
else:
|
||||
self.set_sat_positions(srv.pos)
|
||||
|
||||
@@ -247,6 +275,7 @@ class ServiceDetailsDialog:
|
||||
def init_enigma2_pids(self, flags):
|
||||
pids = list(filter(lambda x: x.startswith("c:"), flags))
|
||||
if pids:
|
||||
extra_pids = []
|
||||
for pid in pids:
|
||||
if pid.startswith(Pids.VIDEO.value):
|
||||
self._video_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
@@ -259,15 +288,19 @@ class ServiceDetailsDialog:
|
||||
elif pid.startswith(Pids.AC3.value):
|
||||
self._ac3_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.VIDEO_TYPE.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.BIT_STREAM_DELAY.value):
|
||||
self._bitstream_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.PCM_DELAY.value):
|
||||
self._pcm_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.SUBTITLE.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
else:
|
||||
extra_pids.append(pid)
|
||||
|
||||
self._extra_pids_entry.set_text(",".join(extra_pids))
|
||||
|
||||
def init_enigma2_transponder_data(self, srv):
|
||||
""" Transponder data initialisation """
|
||||
@@ -309,6 +342,11 @@ class ServiceDetailsDialog:
|
||||
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
|
||||
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
|
||||
elif tr_type is TrType.ATSC:
|
||||
self._sys_combo_box.set_active(0)
|
||||
self.select_active_text(self._mod_combo_box, A_MODULATION.get(tr_data[2]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[1]).name)
|
||||
|
||||
# Should be called last to properly initialize the reference
|
||||
self._srv_type_entry.set_text(data[4])
|
||||
|
||||
@@ -335,7 +373,8 @@ class ServiceDetailsDialog:
|
||||
|
||||
def set_sat_positions(self, sat_pos):
|
||||
""" Sat positions initialisation """
|
||||
self._sat_pos_button.set_value(float(sat_pos))
|
||||
self._sat_pos_button.set_value(float(sat_pos[:-1]))
|
||||
self._pos_side_box.set_active_id(sat_pos[-1:])
|
||||
|
||||
def on_system_changed(self, box):
|
||||
if not self._tr_edit_switch.get_active():
|
||||
@@ -375,18 +414,19 @@ class ServiceDetailsDialog:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.on_edit() if self._action is Action.EDIT else self.on_new()
|
||||
self._dialog.destroy()
|
||||
if self.on_edit() if self._action is Action.EDIT else self.on_new():
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_new(self):
|
||||
""" Create new service. """
|
||||
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
return True
|
||||
|
||||
def on_edit(self):
|
||||
""" Edit current service. """
|
||||
fav_id, data_id = self.get_srv_data()
|
||||
# transponder
|
||||
# Transponder
|
||||
transponder = self._old_service.transponder
|
||||
if self._tr_edit_switch.get_active():
|
||||
try:
|
||||
@@ -396,17 +436,24 @@ class ServiceDetailsDialog:
|
||||
transponder = self.get_terrestrial_transponder_data()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
transponder = self.get_cable_transponder_data()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
transponder = self.get_atsc_transponder_data()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
log("Edit service error: {}".format(e))
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
|
||||
else:
|
||||
if self._transponder_services_iters:
|
||||
self.update_transponder_services(transponder)
|
||||
# service
|
||||
self.update_transponder_services(transponder, self.get_sat_position())
|
||||
# Service
|
||||
service = self.get_service(fav_id, data_id, transponder)
|
||||
old_fav_id = self._old_service.fav_id
|
||||
if old_fav_id != fav_id:
|
||||
if fav_id in self._services:
|
||||
msg = "{}\n\n\t{}".format("A similar service is already in this list!", "Are you sure?")
|
||||
if show_dialog(DialogType.QUESTION, transient=self._dialog, text=msg) != Gtk.ResponseType.OK:
|
||||
return False
|
||||
self.update_bouquets(fav_id, old_fav_id)
|
||||
|
||||
self._services[fav_id] = service
|
||||
|
||||
if self._old_service.picon_id != service.picon_id:
|
||||
@@ -414,7 +461,7 @@ class ServiceDetailsDialog:
|
||||
|
||||
flags = service.flags_cas
|
||||
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
|
||||
if flags:
|
||||
if self._s_type is SettingsType.ENIGMA_2 and flags:
|
||||
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
|
||||
if f_flags and Flag.is_new(int(f_flags[0][2:])):
|
||||
extra_data[Column.SRV_BACKGROUND] = self._new_color
|
||||
@@ -423,6 +470,7 @@ class ServiceDetailsDialog:
|
||||
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
|
||||
self.update_fav_view(self._old_service, service)
|
||||
self._old_service = service
|
||||
return True
|
||||
|
||||
def update_bouquets(self, fav_id, old_fav_id):
|
||||
self._services.pop(old_fav_id, None)
|
||||
@@ -438,14 +486,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):
|
||||
@@ -487,7 +538,9 @@ class ServiceDetailsDialog:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
return self.get_enigma2_flags()
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
return self._old_service.flags_cas
|
||||
flags = self._old_service.flags_cas.split(":")
|
||||
flags[1] = self.get_sat_position()
|
||||
return ":".join(flags)
|
||||
|
||||
def get_enigma2_flags(self):
|
||||
flags = ["p:{}".format(self._package_entry.get_text())]
|
||||
@@ -517,6 +570,9 @@ class ServiceDetailsDialog:
|
||||
pcm_pid = self._pcm_entry.get_text()
|
||||
if pcm_pid:
|
||||
flags.append("{}{:04x}".format(Pids.PCM_DELAY.value, int(pcm_pid)))
|
||||
extra_pids = self._extra_pids_entry.get_text()
|
||||
if extra_pids:
|
||||
flags.append(extra_pids)
|
||||
# flags
|
||||
f_flags = Flag.KEEP.value if self._keep_check_button.get_active() else 0
|
||||
f_flags = f_flags + Flag.HIDE.value if self._hide_check_button.get_active() else f_flags
|
||||
@@ -539,7 +595,9 @@ class ServiceDetailsDialog:
|
||||
return fav_id, data_id
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
|
||||
return fav_id, self._old_service.data_id
|
||||
data_id = self._old_service.data_id.split(":")
|
||||
data_id[1] = "{:x}".format(int(service_type))
|
||||
return fav_id, ":".join(data_id)
|
||||
|
||||
# ***************** Transponder ********************* #
|
||||
|
||||
@@ -547,27 +605,27 @@ class ServiceDetailsDialog:
|
||||
freq = self._freq_entry.get_text()
|
||||
fec = self._fec_combo_box.get_active_id()
|
||||
system = self._sys_combo_box.get_active_id()
|
||||
o_srv = self._old_service
|
||||
|
||||
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
|
||||
freq = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
pol = self._pol_combo_box.get_active_id()
|
||||
pos = str(round(self._sat_pos_button.get_value(), 1))
|
||||
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
|
||||
return freq, rate, pol, fec, system, pos
|
||||
elif self._tr_type is TrType.Terrestrial:
|
||||
o_srv = self._old_service
|
||||
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
|
||||
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
|
||||
elif self._tr_type is TrType.Cable:
|
||||
o_srv = self._old_service
|
||||
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
|
||||
|
||||
def get_satellite_transponder_data(self):
|
||||
sys = self._sys_combo_box.get_active_id()
|
||||
freq = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
freq = "{}000".format(self._freq_entry.get_text())
|
||||
rate = "{}000".format(self._rate_entry.get_text())
|
||||
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
|
||||
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
sat_pos = str(round(self._sat_pos_button.get_value(), 1)).replace(".", "")
|
||||
sat_pos = self.get_sat_position()
|
||||
|
||||
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
srv_sys = "0" # !!!
|
||||
|
||||
@@ -591,12 +649,17 @@ class ServiceDetailsDialog:
|
||||
srv_sys = None
|
||||
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
|
||||
|
||||
def get_sat_position(self):
|
||||
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
|
||||
sat_pos = str(round(sat_pos, 1)).replace(".", "")
|
||||
return sat_pos
|
||||
|
||||
def get_terrestrial_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
|
||||
# inversion, system, plp_id
|
||||
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
|
||||
tr_data[1] = self._freq_entry.get_text()
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
|
||||
@@ -606,28 +669,50 @@ class ServiceDetailsDialog:
|
||||
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
|
||||
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def get_cable_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
|
||||
tr_data[1] = self._freq_entry.get_text()
|
||||
tr_data[2] = self._rate_entry.get_text()
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = "{}000".format(self._rate_entry.get_text())
|
||||
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
|
||||
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def update_transponder_services(self, transponder):
|
||||
def get_atsc_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, inversion, modulation, system
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def update_transponder_services(self, transponder, sat_pos):
|
||||
for itr in self._transponder_services_iters:
|
||||
srv = self._current_model[itr][:Column.SRV_TOOLTIP]
|
||||
srv = self._current_model[itr][:]
|
||||
srv[Column.SRV_FREQ], srv[Column.SRV_RATE], srv[Column.SRV_POL], srv[Column.SRV_FEC], srv[
|
||||
Column.SRV_SYSTEM], srv[Column.SRV_POS] = self.get_transponder_values()
|
||||
srv[Column.SRV_TRANSPONDER] = transponder
|
||||
srv = Service(*srv)
|
||||
self._services[srv.fav_id] = self._services.pop(srv.fav_id)._replace(transponder=transponder)
|
||||
self._current_model.set(itr, {i: v for i, v in enumerate(srv)})
|
||||
|
||||
fav_id = srv[Column.SRV_FAV_ID]
|
||||
old_srv = self._services.pop(fav_id, None)
|
||||
if not old_srv:
|
||||
log("Update transponder services error: No service found for ID {}".format(srv[Column.SRV_FAV_ID]))
|
||||
continue
|
||||
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = srv[Column.SRV_CAS_FLAGS].split(":")
|
||||
flags[1] = sat_pos
|
||||
srv[Column.SRV_CAS_FLAGS] = ":".join(flags)
|
||||
|
||||
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
|
||||
self._current_model.set_row(itr, srv)
|
||||
|
||||
# ***************** Others *********************#
|
||||
|
||||
@@ -647,6 +732,9 @@ class ServiceDetailsDialog:
|
||||
def on_cas_entry_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self._CAID_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
|
||||
|
||||
def on_extra_pids_entry_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self._PIDS_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
|
||||
|
||||
def get_value_from_combobox_id(self, box: Gtk.ComboBox, dc: dict):
|
||||
cb_id = box.get_active_id()
|
||||
return get_key_by_value(dc, cb_id)
|
||||
@@ -656,16 +744,16 @@ class ServiceDetailsDialog:
|
||||
if active and self._action is Action.EDIT:
|
||||
self._transponder_services_iters = []
|
||||
response = TransponderServicesDialog(self._dialog,
|
||||
self._current_model,
|
||||
self._services_view,
|
||||
self._old_service.transponder,
|
||||
self._transponder_services_iters).show()
|
||||
if response == Gtk.ResponseType.CANCEL or response == -4:
|
||||
if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.DELETE_EVENT:
|
||||
switch.set_active(False)
|
||||
self._transponder_services_iters = None
|
||||
return
|
||||
|
||||
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
|
||||
or self._old_service.transponder_type in "tc"))
|
||||
or self._old_service.transponder_type in "tca"))
|
||||
|
||||
for elem in self._TRANSPONDER_ELEMENTS:
|
||||
elem.set_sensitive(active)
|
||||
@@ -679,6 +767,8 @@ class ServiceDetailsDialog:
|
||||
return False
|
||||
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
if self._extra_pids_entry.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_reference(self, entry, event=None):
|
||||
@@ -796,6 +886,18 @@ class ServiceDetailsDialog:
|
||||
self._transponder_id_entry.set_max_width_chars(8)
|
||||
self._network_id_entry.set_max_width_chars(8)
|
||||
|
||||
def update_ui_for_atsc(self):
|
||||
self.update_ui_for_cable()
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(1)
|
||||
tr_grid.remove_column(1)
|
||||
# Init models
|
||||
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
|
||||
system_model.append((TrType.ATSC.name,))
|
||||
[modulation_model.append((v,)) for k, v in A_MODULATION.items()]
|
||||
# Extra
|
||||
self._namespace_entry.set_max_width_chars(25)
|
||||
|
||||
def get_transponder_grid_for_non_satellite(self):
|
||||
self._pids_grid.set_visible(False)
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
@@ -813,23 +915,23 @@ class ServiceDetailsDialog:
|
||||
|
||||
|
||||
class TransponderServicesDialog:
|
||||
def __init__(self, transient, model, transponder, tr_iters):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
("tr_services_dialog", "transponder_services_liststore"))
|
||||
def __init__(self, transient, services_view, transponder, tr_iters):
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("tr_services_dialog", "transponder_services_liststore"))
|
||||
self._dialog = builder.get_object("tr_services_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._srv_model = builder.get_object("transponder_services_liststore")
|
||||
self.append_services(model, transponder, tr_iters)
|
||||
self.append_services(services_view, transponder, tr_iters)
|
||||
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
|
||||
|
||||
def append_services(self, model, transponder, tr_iters):
|
||||
def append_services(self, view, transponder, tr_iters):
|
||||
model = view.get_model()
|
||||
filter_model = model.get_model()
|
||||
for row in model:
|
||||
if row[Column.SRV_TRANSPONDER] == transponder:
|
||||
self._srv_model.append((row[Column.SRV_SERVICE], row[Column.SRV_PACKAGE], row[Column.SRV_TYPE],
|
||||
row[Column.SRV_SSID], row[Column.SRV_FREQ], row[Column.SRV_POS]))
|
||||
tr_iters.append(model.get_iter(row.path))
|
||||
itr = model.get_iter(row.path)
|
||||
tr_iters.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,21 @@
|
||||
import os
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from app.commons import run_task, run_idle
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings
|
||||
from app.ui.dialogs import show_dialog, DialogType
|
||||
from .main_helper import update_entry_data, scroll_to
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT
|
||||
|
||||
|
||||
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)")
|
||||
|
||||
def __init__(self, transient, settings: Settings):
|
||||
handlers = {"on_field_icon_press": self.on_field_icon_press,
|
||||
@@ -31,10 +26,13 @@ class SettingsDialog:
|
||||
"on_apply_profile_settings": self.on_apply_profile_settings,
|
||||
"on_connection_test": self.on_connection_test,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_set_color_switch_state": self.on_set_color_switch_state,
|
||||
"on_http_mode_switch_state": self.on_http_mode_switch_state,
|
||||
"on_yt_dl_switch_state": self.on_yt_dl_switch_state,
|
||||
"on_send_to_switch_state": self.on_send_to_switch_state,
|
||||
"on_set_color_switch": self.on_set_color_switch,
|
||||
"on_force_bq_name": self.on_force_bq_name,
|
||||
"on_http_mode_switch": self.on_http_mode_switch,
|
||||
"on_experimental_switch": self.on_experimental_switch,
|
||||
"on_yt_dl_switch": self.on_yt_dl_switch,
|
||||
"on_default_path_mode_switch": self.on_default_path_mode_switch,
|
||||
"on_default_data_path_changed": self.on_default_data_path_changed,
|
||||
"on_profile_add": self.on_profile_add,
|
||||
"on_profile_edit": self.on_profile_edit,
|
||||
"on_profile_remove": self.on_profile_remove,
|
||||
@@ -45,14 +43,28 @@ 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_view_popup_menu": self.on_view_popup_menu}
|
||||
"on_play_mode_changed": self.on_play_mode_changed,
|
||||
"on_transcoding_preset_changed": self.on_transcoding_preset_changed,
|
||||
"on_apply_presets": self.on_apply_presets,
|
||||
"on_digit_entry_changed": self.on_digit_entry_changed,
|
||||
"on_view_popup_menu": self.on_view_popup_menu,
|
||||
"on_list_font_reset": self.on_list_font_reset,
|
||||
"on_theme_changed": self.on_theme_changed,
|
||||
"on_theme_add": self.on_theme_add,
|
||||
"on_theme_remove": self.on_theme_remove,
|
||||
"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}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
# Settings
|
||||
self._ext_settings = settings
|
||||
self._settings = Settings(settings.settings)
|
||||
self._profiles = self._settings.profiles
|
||||
self._s_type = self._settings.setting_type
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "settings_dialog.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("settings_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
@@ -63,15 +75,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")
|
||||
@@ -80,6 +90,9 @@ class SettingsDialog:
|
||||
self._picons_field = builder.get_object("picons_field")
|
||||
self._picons_dir_field = builder.get_object("picons_dir_field")
|
||||
self._backup_dir_field = builder.get_object("backup_dir_field")
|
||||
self._default_data_dir_field = builder.get_object("default_data_dir_field")
|
||||
self._record_data_dir_field = builder.get_object("record_data_dir_field")
|
||||
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
|
||||
# Info bar
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
@@ -88,19 +101,50 @@ class SettingsDialog:
|
||||
self._enigma_radio_button = builder.get_object("enigma_radio_button")
|
||||
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
|
||||
self._support_ver5_switch = builder.get_object("support_ver5_switch")
|
||||
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
|
||||
# Streaming
|
||||
header_separator = builder.get_object("header_separator")
|
||||
self._apply_presets_button = builder.get_object("apply_presets_button")
|
||||
self._transcoding_switch = builder.get_object("transcoding_switch")
|
||||
self._edit_preset_switch = builder.get_object("edit_preset_switch")
|
||||
self._presets_combo_box = builder.get_object("presets_combo_box")
|
||||
self._video_bitrate_field = builder.get_object("video_bitrate_field")
|
||||
self._video_width_field = builder.get_object("video_width_field")
|
||||
self._video_height_field = builder.get_object("video_height_field")
|
||||
self._audio_bitrate_field = builder.get_object("audio_bitrate_field")
|
||||
self._audio_channels_combo_box = builder.get_object("audio_channels_combo_box")
|
||||
self._audio_sample_rate_combo_box = builder.get_object("audio_sample_rate_combo_box")
|
||||
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
|
||||
self._apply_presets_button.bind_property("visible", header_separator, "visible")
|
||||
self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive")
|
||||
self._play_in_built_radio_button = builder.get_object("play_in_built_radio_button")
|
||||
self._play_in_window_radio_button = builder.get_object("play_in_window_radio_button")
|
||||
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
|
||||
self._gst_lib_button = builder.get_object("gst_lib_button")
|
||||
self._vlc_lib_button = builder.get_object("vlc_lib_button")
|
||||
self._mpv_lib_button = builder.get_object("mpv_lib_button")
|
||||
# Program
|
||||
self._before_save_switch = builder.get_object("before_save_switch")
|
||||
self._before_downloading_switch = builder.get_object("before_downloading_switch")
|
||||
self._program_frame = builder.get_object("program_frame")
|
||||
self._extra_support_grid = builder.get_object("extra_support_grid")
|
||||
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
|
||||
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
|
||||
self._services_hints_switch = builder.get_object("services_hints_switch")
|
||||
self._lang_combo_box = builder.get_object("lang_combo_box")
|
||||
# Appearance
|
||||
self._list_font_button = builder.get_object("list_font_button")
|
||||
self._picons_size_button = builder.get_object("picons_size_button")
|
||||
self._tooltip_logo_size_button = builder.get_object("tooltip_logo_size_button")
|
||||
self._colors_grid = builder.get_object("colors_grid")
|
||||
self._set_color_switch = builder.get_object("set_color_switch")
|
||||
self._new_color_button = builder.get_object("new_color_button")
|
||||
self._extra_color_button = builder.get_object("extra_color_button")
|
||||
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
|
||||
# HTTP API
|
||||
# Extra
|
||||
self._support_http_api_switch = builder.get_object("support_http_api_switch")
|
||||
self._enable_y_dl_switch = builder.get_object("enable_y_dl_switch")
|
||||
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
|
||||
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
|
||||
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
|
||||
self._click_mode_disabled_button = builder.get_object("click_mode_disabled_button")
|
||||
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
|
||||
@@ -109,44 +153,70 @@ class SettingsDialog:
|
||||
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._enable_send_to_switch, "sensitive")
|
||||
self._enable_send_to_switch.bind_property("sensitive", builder.get_object("enable_send_to_label"), "sensitive")
|
||||
self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive")
|
||||
# EXPERIMENTAL
|
||||
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("yt_dl_box"), "sensitive")
|
||||
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
|
||||
# Enigma2 only
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("enable_http_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("enable_experimental_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
|
||||
# Profiles
|
||||
self._profile_view = builder.get_object("profile_tree_view")
|
||||
self._profile_add_button = builder.get_object("profile_add_button")
|
||||
self._profile_remove_button = builder.get_object("profile_remove_button")
|
||||
self._apply_profile_button = builder.get_object("apply_profile_button")
|
||||
self._apply_profile_button.bind_property("visible", builder.get_object("header_separator"), "visible")
|
||||
# Language
|
||||
self._lang_combo_box = builder.get_object("lang_combo_box")
|
||||
# Settings
|
||||
self._ext_settings = settings
|
||||
self._settings = Settings(settings.settings)
|
||||
self._profiles = self._settings.profiles
|
||||
self._s_type = self._settings.setting_type
|
||||
self.set_settings()
|
||||
self._apply_profile_button.bind_property("visible", header_separator, "visible")
|
||||
# Style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
|
||||
self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field)
|
||||
for el in self._digit_elems:
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self.init_ui_elements(self._s_type)
|
||||
self.init_profiles()
|
||||
|
||||
if self._settings.is_darwin:
|
||||
# Themes
|
||||
self._layout_switch = builder.get_object("layout_switch")
|
||||
self._layout_switch.bind_property("active", builder.get_object("bouquet_box"), "sensitive")
|
||||
self._layout_switch.set_active(self._ext_settings.alternate_layout)
|
||||
self._theme_frame = builder.get_object("theme_frame")
|
||||
self._theme_frame.set_visible(True)
|
||||
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
|
||||
self._theme_combo_box = builder.get_object("theme_combo_box")
|
||||
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
|
||||
self._dark_mode_switch = builder.get_object("dark_mode_switch")
|
||||
self._themes_support_switch = builder.get_object("themes_support_switch")
|
||||
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
|
||||
self.init_themes()
|
||||
|
||||
@run_idle
|
||||
def init_ui_elements(self, s_type):
|
||||
is_enigma_profile = s_type is SettingsType.ENIGMA_2
|
||||
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_header_bar()
|
||||
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
|
||||
self._program_frame.set_sensitive(is_enigma_profile)
|
||||
self._extra_support_grid.set_sensitive(is_enigma_profile)
|
||||
http_active = self._support_http_api_switch.get_active()
|
||||
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
|
||||
self._lang_combo_box.set_active_id(self._settings.language)
|
||||
self._lang_combo_box.set_active_id(self._ext_settings.language)
|
||||
self.on_info_bar_close() if is_enigma_profile else self.show_info_message(
|
||||
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
|
||||
|
||||
def init_profiles(self):
|
||||
p_def = self._settings.default_profile
|
||||
for p in self._profiles:
|
||||
self._profile_view.get_model().append((p, DEFAULT_ICON if p == p_def else None))
|
||||
model = self._profile_view.get_model()
|
||||
for ind, p in enumerate(self._profiles):
|
||||
icon = DEFAULT_ICON if p == p_def else None
|
||||
model.append((p, icon))
|
||||
if icon:
|
||||
scroll_to(ind, self._profile_view)
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
|
||||
|
||||
def update_header_bar(self):
|
||||
@@ -170,27 +240,25 @@ class SettingsDialog:
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
def on_settings_type_changed(self, item):
|
||||
profile = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
|
||||
self._s_type = profile
|
||||
self._settings.setting_type = profile
|
||||
self.on_reset()
|
||||
self.init_ui_elements(profile)
|
||||
s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
|
||||
if s_type is not self._s_type:
|
||||
self._settings.setting_type = s_type
|
||||
self._s_type = s_type
|
||||
self.on_reset()
|
||||
self.init_ui_elements(s_type)
|
||||
|
||||
def on_reset(self, item=None):
|
||||
self._settings.reset()
|
||||
self.set_settings()
|
||||
|
||||
def set_settings(self):
|
||||
self._s_type = self._settings.setting_type
|
||||
self._host_field.set_text(self._settings.host)
|
||||
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)
|
||||
@@ -200,15 +268,31 @@ class SettingsDialog:
|
||||
self._data_dir_field.set_text(self._settings.data_local_path)
|
||||
self._picons_dir_field.set_text(self._settings.picons_local_path)
|
||||
self._backup_dir_field.set_text(self._settings.backup_local_path)
|
||||
self._default_data_dir_field.set_text(self._settings.default_data_path)
|
||||
self._record_data_dir_field.set_text(self._settings.records_path)
|
||||
self._before_save_switch.set_active(self._settings.backup_before_save)
|
||||
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
|
||||
self.set_fav_click_mode(self._settings.fav_click_mode)
|
||||
self.set_play_stream_mode(self._settings.play_streams_mode)
|
||||
self.set_stream_lib(self._settings.stream_lib)
|
||||
self._load_on_startup_switch.set_active(self._settings.load_last_config)
|
||||
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
|
||||
self._services_hints_switch.set_active(self._settings.show_srv_hints)
|
||||
self._default_data_paths_switch.set_active(self._settings.profile_folder_is_default)
|
||||
self._transcoding_switch.set_active(self._settings.activate_transcoding)
|
||||
self._presets_combo_box.set_active_id(self._settings.active_preset)
|
||||
self.on_transcoding_preset_changed(self._presets_combo_box)
|
||||
self._picons_size_button.set_active_id(str(self._settings.list_picon_size))
|
||||
self._tooltip_logo_size_button.set_active_id(str(self._settings.tooltip_logo_size))
|
||||
self._list_font_button.set_font(self._settings.list_font)
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
|
||||
self._support_ver5_switch.set_active(self._settings.v5_support)
|
||||
self._force_bq_name_switch.set_active(self._settings.force_bq_names)
|
||||
self._support_http_api_switch.set_active(self._settings.http_api_support)
|
||||
self._enable_y_dl_switch.set_active(self._settings.enable_yt_dl)
|
||||
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
|
||||
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
|
||||
self._enable_send_to_switch.set_active(self._settings.enable_send_to)
|
||||
self._set_color_switch.set_active(self._settings.use_colors)
|
||||
new_rgb = Gdk.RGBA()
|
||||
@@ -218,19 +302,24 @@ class SettingsDialog:
|
||||
self._new_color_button.set_rgba(new_rgb)
|
||||
self._extra_color_button.set_rgba(extra_rgb)
|
||||
|
||||
def on_apply_profile_settings(self, item):
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._enigma_radio_button.activate()
|
||||
else:
|
||||
self._neutrino_radio_button.activate()
|
||||
|
||||
def on_apply_profile_settings(self, item=None):
|
||||
if not self.is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
|
||||
self._settings.setting_type = self._s_type
|
||||
self._settings.host = self._host_field.get_text()
|
||||
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()
|
||||
@@ -242,23 +331,46 @@ class SettingsDialog:
|
||||
self._settings.backup_local_path = self._backup_dir_field.get_text()
|
||||
|
||||
def apply_settings(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
self.on_apply_profile_settings()
|
||||
self._ext_settings.profiles = self._settings.profiles
|
||||
self._ext_settings.backup_before_save = self._before_save_switch.get_active()
|
||||
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
|
||||
self._ext_settings.fav_click_mode = self.get_fav_click_mode()
|
||||
self._ext_settings.play_streams_mode = self.get_play_stream_mode()
|
||||
self._ext_settings.stream_lib = self.get_stream_lib()
|
||||
self._ext_settings.language = self._lang_combo_box.get_active_id()
|
||||
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
|
||||
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
|
||||
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
|
||||
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
|
||||
self._ext_settings.default_data_path = self._default_data_dir_field.get_text()
|
||||
self._ext_settings.records_path = self._record_data_dir_field.get_text()
|
||||
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
|
||||
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
|
||||
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
|
||||
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
|
||||
self._ext_settings.list_font = self._list_font_button.get_font()
|
||||
|
||||
if self._ext_settings.is_darwin:
|
||||
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()
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._ext_settings.is_enable_experimental = self._enable_exp_switch.get_active()
|
||||
self._ext_settings.use_colors = self._set_color_switch.get_active()
|
||||
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
|
||||
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
|
||||
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
|
||||
self._ext_settings.force_bq_names = self._force_bq_name_switch.get_active()
|
||||
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl = self._enable_y_dl_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
|
||||
|
||||
self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
|
||||
@@ -270,16 +382,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:
|
||||
@@ -294,7 +405,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)
|
||||
@@ -316,7 +427,7 @@ class SettingsDialog:
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
self._message_label.set_text(get_message(text))
|
||||
|
||||
@run_idle
|
||||
def show_spinner(self, show):
|
||||
@@ -326,21 +437,40 @@ class SettingsDialog:
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_set_color_switch_state(self, switch, state):
|
||||
def on_set_color_switch(self, switch, state):
|
||||
self._colors_grid.set_sensitive(state)
|
||||
|
||||
def on_http_mode_switch_state(self, switch, state):
|
||||
def on_http_mode_switch(self, switch, state):
|
||||
self._click_mode_zap_button.set_sensitive(state)
|
||||
if any((self._click_mode_play_button.get_active(),
|
||||
self._click_mode_zap_button.get_active(),
|
||||
self._click_mode_zap_and_play_button.get_active())):
|
||||
self._click_mode_disabled_button.set_active(True)
|
||||
|
||||
def on_yt_dl_switch_state(self, switch, state):
|
||||
def on_experimental_switch(self, switch, state):
|
||||
if not state:
|
||||
self._support_ver5_switch.set_active(state)
|
||||
self._enable_send_to_switch.set_active(state)
|
||||
self._enable_yt_dl_switch.set_active(state)
|
||||
|
||||
def on_force_bq_name(self, switch, state):
|
||||
if self._main_stack.get_visible_child_name() != "extra":
|
||||
return
|
||||
|
||||
if state:
|
||||
msg = "Some images may have problems displaying the favorites list!"
|
||||
self.show_info_message(msg, Gtk.MessageType.WARNING)
|
||||
else:
|
||||
self.on_info_bar_close()
|
||||
|
||||
def on_yt_dl_switch(self, switch, state):
|
||||
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_send_to_switch_state(self, switch, state):
|
||||
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
|
||||
def on_default_path_mode_switch(self, switch, state):
|
||||
self._settings.profile_folder_is_default = state
|
||||
|
||||
def on_default_data_path_changed(self, entry):
|
||||
self._settings.default_data_path = entry.get_text()
|
||||
|
||||
def on_profile_add(self, item):
|
||||
model = self._profile_view.get_model()
|
||||
@@ -353,11 +483,7 @@ class SettingsDialog:
|
||||
self._profiles[name] = self._s_type.get_default_settings()
|
||||
model.append((name, None))
|
||||
scroll_to(len(model) - 1, self._profile_view)
|
||||
self.on_profile_selected(self._profile_view)
|
||||
p = name + "/"
|
||||
self._settings.data_local_path += p
|
||||
self._settings.picons_local_path += p
|
||||
self._settings.backup_local_path += p
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
self.on_reset()
|
||||
|
||||
def on_profile_edit(self, item=None):
|
||||
@@ -379,25 +505,30 @@ class SettingsDialog:
|
||||
self._profile_remove_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_profile_edited(self, render, path, new_value):
|
||||
p_name = render.get_property("text")
|
||||
p_name = self._profiles.pop(p_name, None)
|
||||
if p_name:
|
||||
row = self._profile_view.get_model()[path]
|
||||
row = self._profile_view.get_model()[path]
|
||||
old_name = row[0]
|
||||
if old_name == new_value:
|
||||
return
|
||||
|
||||
if new_value in self._profiles:
|
||||
show_dialog(DialogType.ERROR, self._dialog, "A profile with that name exists!")
|
||||
return
|
||||
|
||||
p_settings = self._profiles.pop(old_name, None)
|
||||
if p_settings:
|
||||
row[0] = new_value
|
||||
self._profiles[new_value] = p_name
|
||||
self._profiles[new_value] = p_settings
|
||||
self.update_local_paths(new_value, old_name)
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
|
||||
if p_name != new_value:
|
||||
self.update_local_paths(new_value)
|
||||
self.on_profile_selected(self._profile_view)
|
||||
|
||||
def update_local_paths(self, p_name, force_rename=False):
|
||||
def update_local_paths(self, p_name, old_name, force_rename=False):
|
||||
data_path = self._settings.data_local_path
|
||||
picons_path = self._settings.picons_local_path
|
||||
backup_path = self._settings.backup_local_path
|
||||
|
||||
self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, p_name)
|
||||
self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, p_name)
|
||||
self._settings.backup_local_path = "{}/{}/".format(Path(backup_path).parent, p_name)
|
||||
self._settings.data_local_path = p_name.join(data_path.rsplit(old_name, 1))
|
||||
self._settings.picons_local_path = p_name.join(picons_path.rsplit(old_name, 1))
|
||||
self._settings.backup_local_path = p_name.join(backup_path.rsplit(old_name, 1))
|
||||
|
||||
if force_rename:
|
||||
try:
|
||||
@@ -410,15 +541,14 @@ class SettingsDialog:
|
||||
except OSError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_profile_selected(self, view):
|
||||
def on_profile_selected(self, view, force=True):
|
||||
if force:
|
||||
self.on_apply_profile_settings()
|
||||
|
||||
model, paths = self._profile_view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
profile = model.get_value(model.get_iter(paths), 0)
|
||||
self._settings.current_profile = profile
|
||||
if self._settings.setting_type is SettingsType.ENIGMA_2:
|
||||
self._enigma_radio_button.activate()
|
||||
else:
|
||||
self._neutrino_radio_button.activate()
|
||||
self.set_settings()
|
||||
|
||||
def on_profile_set_default(self, item):
|
||||
@@ -437,10 +567,9 @@ class SettingsDialog:
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_main_settings_visible(self, stack, param):
|
||||
self._apply_profile_button.set_visible(stack.get_visible_child_name() == "profiles")
|
||||
|
||||
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)
|
||||
name = stack.get_visible_child_name()
|
||||
self._apply_profile_button.set_visible(name == "profiles")
|
||||
self._apply_presets_button.set_visible(name == "streaming")
|
||||
|
||||
def on_http_use_ssl_toggled(self, button):
|
||||
active = button.get_active()
|
||||
@@ -480,10 +609,215 @@ class SettingsDialog:
|
||||
|
||||
return FavClickMode.DISABLED
|
||||
|
||||
def on_play_mode_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "streaming" or not button.get_active():
|
||||
return
|
||||
|
||||
if self._settings.is_darwin:
|
||||
is_gst = self._gst_lib_button.get_active()
|
||||
self._play_in_built_radio_button.set_sensitive(is_gst)
|
||||
self._play_in_window_radio_button.set_active(not is_gst and self._play_in_built_radio_button.get_active())
|
||||
|
||||
if button.get_active():
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
@run_idle
|
||||
def set_play_stream_mode(self, mode):
|
||||
self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin)
|
||||
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
|
||||
self._play_in_window_radio_button.set_active(mode is PlayStreamsMode.WINDOW)
|
||||
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
|
||||
|
||||
if self._settings.is_darwin and self._settings.stream_lib != "gst":
|
||||
self._play_in_built_radio_button.set_sensitive(False)
|
||||
|
||||
def get_play_stream_mode(self):
|
||||
if self._play_in_built_radio_button.get_active():
|
||||
return PlayStreamsMode.BUILT_IN
|
||||
if self._play_in_window_radio_button.get_active():
|
||||
return PlayStreamsMode.WINDOW
|
||||
if self._get_m3u_radio_button.get_active():
|
||||
return PlayStreamsMode.M3U
|
||||
|
||||
return self._settings.play_streams_mode
|
||||
|
||||
def set_stream_lib(self, mode):
|
||||
self._vlc_lib_button.set_active(mode == "vlc")
|
||||
self._gst_lib_button.set_active(mode == "gst")
|
||||
self._mpv_lib_button.set_active(mode == "mpv")
|
||||
|
||||
def get_stream_lib(self):
|
||||
if self._gst_lib_button.get_active():
|
||||
return "gst"
|
||||
elif self._vlc_lib_button.get_active():
|
||||
return "vlc"
|
||||
return "mpv"
|
||||
|
||||
def on_transcoding_preset_changed(self, button):
|
||||
presets = self._settings.transcoding_presets
|
||||
prs = presets.get(button.get_active_id())
|
||||
self._video_bitrate_field.set_text(prs.get("vb", "0"))
|
||||
self._video_width_field.set_text(prs.get("width", "0"))
|
||||
self._video_height_field.set_text(prs.get("height", "0"))
|
||||
self._audio_bitrate_field.set_text(prs.get("ab", "0"))
|
||||
self._audio_channels_combo_box.set_active_id(prs.get("channels", "2"))
|
||||
self._audio_sample_rate_combo_box.set_active_id(prs.get("samplerate", "44100"))
|
||||
self._audio_codec_combo_box.set_active_id(prs.get("acodec", "mp3"))
|
||||
|
||||
def on_apply_presets(self, item):
|
||||
if not self.is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
presets = self._settings.transcoding_presets
|
||||
prs = presets.get(self._presets_combo_box.get_active_id())
|
||||
prs["vb"] = self._video_bitrate_field.get_text()
|
||||
prs["width"] = self._video_width_field.get_text()
|
||||
prs["height"] = self._video_height_field.get_text()
|
||||
prs["ab"] = self._audio_bitrate_field.get_text()
|
||||
prs["channels"] = self._audio_channels_combo_box.get_active_id()
|
||||
prs["samplerate"] = self._audio_sample_rate_combo_box.get_active_id()
|
||||
prs["acodec"] = self._audio_codec_combo_box.get_active_id()
|
||||
self._ext_settings.transcoding_presets = presets
|
||||
self._edit_preset_switch.set_active(False)
|
||||
|
||||
def on_digit_entry_changed(self, entry):
|
||||
if self._DIGIT_PATTERN.search(entry.get_text()):
|
||||
entry.set_name(self._DIGIT_ENTRY_NAME)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
|
||||
def is_data_correct(self, elems):
|
||||
return not any(elem.get_name() == self._DIGIT_ENTRY_NAME for elem in elems)
|
||||
|
||||
def on_view_popup_menu(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
def on_list_font_reset(self, button):
|
||||
self._list_font_button.set_font(APP_FONT)
|
||||
|
||||
# ******************* Themes *********************** #
|
||||
|
||||
def on_theme_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "appearance":
|
||||
return
|
||||
|
||||
self.set_theme_thumbnail_image(button.get_active_id())
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
@run_idle
|
||||
def set_theme_thumbnail_image(self, theme_name):
|
||||
img_path = "{}{}/gtk-3.0/thumbnail.png".format(self._ext_settings.themes_path, theme_name)
|
||||
self._theme_thumbnail_image.set_from_pixbuf(get_picon_pixbuf(img_path, 96))
|
||||
|
||||
def on_theme_add(self, button):
|
||||
self.add_theme(self._ext_settings.themes_path, self._theme_combo_box)
|
||||
|
||||
def on_theme_remove(self, button):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
|
||||
Gtk.Settings().get_default().set_property("gtk-theme-name", "")
|
||||
self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path)
|
||||
|
||||
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)
|
||||
|
||||
def on_icon_theme_add(self, button):
|
||||
self.add_theme(self._ext_settings.icon_themes_path, self._icon_theme_combo_box)
|
||||
|
||||
def on_icon_theme_remove(self, button):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
|
||||
Gtk.Settings().get_default().set_property("gtk-icon-theme-name", "")
|
||||
self.remove_theme(self._icon_theme_combo_box, self._ext_settings.icon_themes_path)
|
||||
|
||||
@run_idle
|
||||
def add_theme(self, path, button):
|
||||
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
self._theme_frame.set_sensitive(False)
|
||||
self.unpack_theme(response, path, button)
|
||||
|
||||
@run_task
|
||||
def unpack_theme(self, src, dst, button):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
|
||||
import subprocess
|
||||
log("Unpacking '{}' started...".format(src))
|
||||
p = subprocess.Popen(["tar", "-xvf", src, "-C", dst],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
p.communicate()
|
||||
log("Unpacking end.")
|
||||
finally:
|
||||
self.update_theme_button(button, dst)
|
||||
|
||||
@run_idle
|
||||
def update_theme_button(self, button, dst):
|
||||
exist = set(os.listdir(dst))
|
||||
current = {r[0] for r in button.get_model()}
|
||||
added = exist - current
|
||||
if added:
|
||||
theme = added.pop()
|
||||
if theme not in current:
|
||||
button.append(theme, theme)
|
||||
button.set_active_id(theme)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._theme_frame.set_sensitive(True)
|
||||
|
||||
@run_idle
|
||||
def remove_theme(self, button, path):
|
||||
theme = button.get_active_id()
|
||||
if not theme:
|
||||
self.show_info_message("No selected item!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
from shutil import rmtree
|
||||
|
||||
try:
|
||||
rmtree(path + theme, ignore_errors=True)
|
||||
except OSError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.theme_button_remove_active(button)
|
||||
|
||||
@run_idle
|
||||
def theme_button_remove_active(self, button):
|
||||
button.remove(button.get_active())
|
||||
button.set_active(0)
|
||||
|
||||
@run_idle
|
||||
def init_themes(self):
|
||||
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
|
||||
t_support = self._ext_settings.is_themes_support
|
||||
self._themes_support_switch.set_active(t_support)
|
||||
if t_support:
|
||||
# GTK
|
||||
try:
|
||||
for t in os.listdir(self._ext_settings.themes_path):
|
||||
self._theme_combo_box.append(t, t)
|
||||
self._theme_combo_box.set_active_id(self._ext_settings.theme)
|
||||
self.set_theme_thumbnail_image(self._ext_settings.theme)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except PermissionError as e:
|
||||
log("{}".format(e))
|
||||
# Icons
|
||||
try:
|
||||
for t in os.listdir(self._ext_settings.icon_themes_path):
|
||||
self._icon_theme_combo_box.append(t, t)
|
||||
self._icon_theme_combo_box.set_active_id(self._ext_settings.icon_theme)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except PermissionError as e:
|
||||
log("{}".format(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,72 @@
|
||||
#digit-entry {
|
||||
border-color: Red;
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
#status-bar-button {
|
||||
padding: 1px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
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 {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.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: 1px;
|
||||
}
|
||||
|
||||
.stack-switcher > button > label {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.stack-switcher > button.text-button {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
141
app/ui/timer_row.glade
Normal file
141
app/ui/timer_row.glade
Normal 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>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 Dmitriy Yefremov
|
||||
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
|
||||
@@ -26,20 +26,19 @@ THE SOFTWARE.
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<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 satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2019 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkWindow" id="main_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="window_position">mouse</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">splashscreen</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="decorated">False</property>
|
||||
@@ -49,20 +48,29 @@ Author: Dmitriy Yefremov
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<object class="GtkBox" id="tool_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<object class="GtkButton" id="previous_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">2</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="primary_icon_stock">gtk-dnd-multiple</property>
|
||||
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_previous" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="previous_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-previous</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -70,31 +78,156 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="next_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="next_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-next</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="GtkEntry" id="url_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Drag or paste the link here</property>
|
||||
<property name="margin_left">2</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="primary_icon_stock">gtk-paste</property>
|
||||
<signal name="activate" handler="on_url_activate" swapped="no"/>
|
||||
<signal name="changed" handler="on_url_changed" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="play_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Play</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_play" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="play_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-play</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Stop playback</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_stop" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="stop_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-stop</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Remove added links in the playlist</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkStatusIcon" id="status_icon">
|
||||
<property name="icon_name">insert-link</property>
|
||||
<property name="has_tooltip">True</property>
|
||||
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
|
||||
<signal name="popup-menu" handler="on_popup_menu" object="staus_popup_menu" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_query_tooltip" swapped="no"/>
|
||||
<object class="GtkImage" id="show_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">view-restore</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="staus_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="exit_menu_item">
|
||||
<property name="label">gtk-quit</property>
|
||||
<object class="GtkImageMenuItem" id="show_menu_item">
|
||||
<property name="label" translatable="yes">Show/Hide</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_exit" swapped="no"/>
|
||||
<property name="image">show_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkStatusIcon" id="status_icon">
|
||||
<property name="icon_name">demon-editor</property>
|
||||
<property name="has_tooltip">True</property>
|
||||
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
|
||||
<signal name="popup-menu" handler="on_popup_menu" object="staus_popup_menu" swapped="no"/>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
@@ -1,31 +1,69 @@
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import gi
|
||||
from gi.repository import GLib
|
||||
from app.connections import HttpRequestType
|
||||
|
||||
from app.commons import log
|
||||
from app.connections import HttpAPI
|
||||
from app.tools.yt import YouTube
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.iptv import get_yt_icon
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
class LinksTransmitter:
|
||||
""" The main class for the "send to" function.
|
||||
|
||||
def __init__(self, http_api, app_window):
|
||||
It used for direct playback of media links by the enigma2 media player.
|
||||
"""
|
||||
__STREAM_PREFIX = "4097:0:1:0:0:0:0:0:0:0:"
|
||||
|
||||
def __init__(self, http_api, app_window, settings):
|
||||
handlers = {"on_popup_menu": self.on_popup_menu,
|
||||
"on_status_icon_activate": self.on_status_icon_activate,
|
||||
"on_query_tooltip": self.on_query_tooltip,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
"on_url_activate": self.on_url_activate,
|
||||
"on_drag_data_received": self.on_drag_data_received,
|
||||
"on_exit": self.on_exit}
|
||||
"on_previous": self.on_previous,
|
||||
"on_next": self.on_next,
|
||||
"on_stop": self.on_stop,
|
||||
"on_clear": self.on_clear,
|
||||
"on_play": self.on_play}
|
||||
|
||||
self._http_api = http_api
|
||||
self._app_window = app_window
|
||||
self._is_status_icon = True
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "transmitter.glade")
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "transmitter.glade", handlers)
|
||||
|
||||
self._tray = builder.get_object("status_icon")
|
||||
self._main_window = builder.get_object("main_window")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._tool_bar = builder.get_object("tool_bar")
|
||||
self._popup_menu = builder.get_object("staus_popup_menu")
|
||||
self._restore_menu_item = builder.get_object("restore_menu_item")
|
||||
self._status_active = None
|
||||
self._status_passive = None
|
||||
self._yt = YouTube.get_instance(settings)
|
||||
|
||||
try:
|
||||
gi.require_version("AppIndicator3", "0.1")
|
||||
from gi.repository import AppIndicator3
|
||||
except (ImportError, ValueError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
self._tray = builder.get_object("status_icon")
|
||||
else:
|
||||
self._is_status_icon = False
|
||||
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE
|
||||
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE
|
||||
|
||||
category = AppIndicator3.IndicatorCategory.APPLICATION_STATUS
|
||||
path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg")
|
||||
path = str(path.resolve()) if path.is_file() else "demon-editor"
|
||||
self._tray = AppIndicator3.Indicator.new("DemonEditor", path, category)
|
||||
self._tray.set_status(self._status_active)
|
||||
self._tray.set_secondary_activate_target(builder.get_object("show_menu_item"))
|
||||
self._tray.set_menu(self._popup_menu)
|
||||
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
@@ -33,7 +71,10 @@ class LinksTransmitter:
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def show(self, show):
|
||||
self._tray.set_visible(show)
|
||||
if self._is_status_icon:
|
||||
self._tray.set_visible(show)
|
||||
elif self._status_active:
|
||||
self._tray.set_status(self._status_active if show else self._status_passive)
|
||||
if not show:
|
||||
self.hide()
|
||||
|
||||
@@ -48,12 +89,12 @@ class LinksTransmitter:
|
||||
window.hide() if visible else window.show()
|
||||
self._app_window.present() if visible else self._app_window.iconify()
|
||||
|
||||
def on_query_tooltip(self, icon, g, x, y, tooltip: Gtk.Tooltip):
|
||||
if self._main_window.get_visible() or not self._url_entry.get_text():
|
||||
return False
|
||||
def on_url_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self.is_url(entry.get_text()) else "digit-entry")
|
||||
|
||||
tooltip.set_text(self._url_entry.get_text())
|
||||
return True
|
||||
def on_url_activate(self, entry):
|
||||
gen = self.activate_url(entry.get_text())
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_drag_data_received(self, entry, drag_context, x, y, data, info, time):
|
||||
url = data.get_text()
|
||||
@@ -63,36 +104,70 @@ class LinksTransmitter:
|
||||
|
||||
def activate_url(self, url):
|
||||
self._url_entry.set_name("GtkEntry")
|
||||
result = urlparse(url)
|
||||
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
|
||||
|
||||
if result.scheme and result.netloc:
|
||||
self._url_entry.set_sensitive(False)
|
||||
if self.is_url(url):
|
||||
self._tool_bar.set_sensitive(False)
|
||||
yt_id = YouTube.get_yt_id(url)
|
||||
yield True
|
||||
|
||||
if yt_id:
|
||||
self._url_entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
|
||||
links, title = YouTube.get_yt_link(yt_id)
|
||||
links, title = self._yt.get_yt_link(yt_id, url)
|
||||
yield True
|
||||
if links:
|
||||
url = links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
|
||||
else:
|
||||
self.on_play(links)
|
||||
self.on_done(links)
|
||||
return
|
||||
else:
|
||||
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
|
||||
|
||||
self._http_api.send(HttpRequestType.PLAY, url, self.on_play)
|
||||
self._http_api.send(HttpAPI.Request.PLAY, url, self.on_done, self.__STREAM_PREFIX)
|
||||
yield True
|
||||
|
||||
def on_play(self, res):
|
||||
def on_done(self, res):
|
||||
""" Play callback """
|
||||
GLib.idle_add(self._url_entry.set_sensitive, True)
|
||||
res = res.get("e2state", None) if res else res
|
||||
self._url_entry.set_name("GtkEntry" if res else "digit-entry")
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, True)
|
||||
|
||||
def on_exit(self, item=None):
|
||||
self.show(False)
|
||||
def on_previous(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_PREV, None, self.on_done)
|
||||
|
||||
def on_next(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_NEXT, None, self.on_done)
|
||||
|
||||
def on_play(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_PLAY, None, self.on_done)
|
||||
|
||||
def on_stop(self, item):
|
||||
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(HttpAPI.Request.PLAYER_LIST, None, self.clear_playlist)
|
||||
|
||||
def clear_playlist(self, res):
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, not res)
|
||||
if "error_code" in res:
|
||||
log("Error clearing playlist. There may be no http connection.")
|
||||
self.on_done(res)
|
||||
return
|
||||
|
||||
for ref in res:
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, False)
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_REMOVE,
|
||||
ref.get("e2servicereference", ""),
|
||||
self.on_done,
|
||||
self.__STREAM_PREFIX)
|
||||
|
||||
@staticmethod
|
||||
def is_url(text):
|
||||
""" Simple url checking. """
|
||||
result = urlparse(text)
|
||||
return result.scheme and result.netloc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import locale
|
||||
import os
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
|
||||
import gi
|
||||
|
||||
from app.settings import Settings, SettingsException
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Notify", "0.7")
|
||||
from gi.repository import Gtk, Gdk, Notify
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN
|
||||
|
||||
# path to *.glade files
|
||||
# Init notify
|
||||
Notify.init("DemonEditor")
|
||||
# Setting mod mask for the keyboard depending on the platform.
|
||||
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
|
||||
# Path to *.glade files.
|
||||
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
|
||||
|
||||
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
|
||||
# translation
|
||||
# Translation.
|
||||
TEXT_DOMAIN = "demon-editor"
|
||||
APP_FONT = None
|
||||
|
||||
try:
|
||||
settings = Settings.get_instance()
|
||||
except SettingsException:
|
||||
@@ -24,6 +32,15 @@ else:
|
||||
if UI_RESOURCES_PATH == "app/ui/":
|
||||
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang")
|
||||
|
||||
st = Gtk.Settings().get_default()
|
||||
APP_FONT = st.get_property("gtk-font-name")
|
||||
if not settings.list_font:
|
||||
settings.list_font = APP_FONT
|
||||
|
||||
if settings.is_themes_support:
|
||||
st.set_property("gtk-theme-name", settings.theme)
|
||||
st.set_property("gtk-icon-theme-name", settings.icon_theme)
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(UI_RESOURCES_PATH + "icons")
|
||||
|
||||
@@ -39,23 +56,51 @@ EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index",
|
||||
DEFAULT_ICON = theme.load_icon("emblem-default", 16, 0) if theme.lookup_icon("emblem-default", 16, 0) else None
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_yt_icon(icon_name, size=24):
|
||||
""" Getting YouTube icon.
|
||||
|
||||
If the icon is not found in the icon themes, the "Info" icon is returned by default!
|
||||
"""
|
||||
default_theme = Gtk.IconTheme.get_default()
|
||||
if default_theme.has_icon(icon_name):
|
||||
return default_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
n_theme = Gtk.IconTheme.new()
|
||||
import glob
|
||||
|
||||
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
|
||||
n_theme.set_custom_theme(theme_name)
|
||||
if n_theme.has_icon(icon_name):
|
||||
return n_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
return default_theme.load_icon("info", size, 0)
|
||||
|
||||
|
||||
def show_notification(message, timeout=10000, urgency=1):
|
||||
""" Shows notification.
|
||||
|
||||
@param message: text to display
|
||||
@param timeout: milliseconds
|
||||
@param urgency: 0 - low, 1 - normal, 2 - critical
|
||||
"""
|
||||
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
|
||||
notify.set_urgency(urgency)
|
||||
notify.set_timeout(timeout)
|
||||
notify.show()
|
||||
|
||||
|
||||
class KeyboardKey(Enum):
|
||||
""" The raw(hardware) codes of the keyboard keys. """
|
||||
Q = 24
|
||||
E = 26
|
||||
R = 27
|
||||
T = 28
|
||||
U = 30
|
||||
O = 32
|
||||
P = 33
|
||||
S = 39
|
||||
D = 40
|
||||
H = 43
|
||||
L = 46
|
||||
F = 41
|
||||
X = 53
|
||||
C = 54
|
||||
V = 55
|
||||
B = 56
|
||||
W = 25
|
||||
Z = 52
|
||||
INSERT = 118
|
||||
@@ -68,6 +113,7 @@ class KeyboardKey(Enum):
|
||||
LEFT = 113
|
||||
RIGHT = 114
|
||||
F2 = 68
|
||||
F7 = 73
|
||||
SPACE = 65
|
||||
DELETE = 119
|
||||
BACK_SPACE = 22
|
||||
@@ -117,7 +163,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
|
||||
@@ -140,7 +186,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
|
||||
@@ -152,11 +198,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 """
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
VER="0.4.7_Pre-alpha"
|
||||
VER="1.0.10_Beta"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
|
||||
@@ -4,7 +4,21 @@ 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.
|
||||
Import function.
|
||||
Backup function.
|
||||
Support of picons.
|
||||
Importing services, downloading picons and updating satellites from the Web.
|
||||
Extended support of IPTV.
|
||||
Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
Export of bouquets with IPTV services in m3u.
|
||||
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
Playback of IPTV or other streams directly from the bouquet list.
|
||||
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
Simple FTP client (experimental).
|
||||
|
||||
Keyboard shortcuts:
|
||||
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
|
||||
@@ -25,24 +39,24 @@ Keyboard shortcuts:
|
||||
Ctrl + O - (re)load user data from current dir.
|
||||
Ctrl + D - load data from receiver.
|
||||
Ctrl + U/B upload data/bouquets to receiver.
|
||||
Ctrl + F - show/hide search bar.
|
||||
Ctrl + Shift + F - show/hide filter bar.
|
||||
|
||||
Extra:
|
||||
Import feature.
|
||||
Multiple selections in lists only with Space key (as in file managers).
|
||||
Ability to download picons and update satellites (transponders) from web.
|
||||
Ability to import into bouquet (Neutrino WEB TV) from m3u.
|
||||
Ability to export bouquets with IPTV services to m3u.
|
||||
Assignment EPG from DVB or XML for IPTV services(Enigma2 only).
|
||||
Preview (playing) IPTV or other streams directly from the bouquet list (should be installed VLC).
|
||||
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.
|
||||
|
||||
Note.
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
|
||||
Python >= 3.5.2, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.
|
||||
|
||||
Important:
|
||||
Main supported lamedb format is version 4. Versions 3 and 5 has only experimental support!
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support!
|
||||
For version **3** is only read mode available. When saving, version **4** format is used instead!
|
||||
|
||||
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the
|
||||
selected bouquets!** If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
|
||||
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
|
||||
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
|
||||
|
||||
For streams playback, this app supports VLC, MPV and GStreamer.
|
||||
Depending on your distro, you may need to install additional packages and libraries.
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
Package: demon-editor
|
||||
Version: 0.4.7-Pre-alpha
|
||||
Version: 1.0.10-Beta
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Essential: no
|
||||
Depends: python3 (>= 3.5)
|
||||
Depends: python3 (>= 3.5),
|
||||
python3-requests,
|
||||
python3-gi,
|
||||
python3-gi-cairo
|
||||
Recommends: libmpv1,
|
||||
python3-chardet
|
||||
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
|
||||
Description: Enigma2 channel and satellites list editor
|
||||
Description: Enigma2 channel and satellite list editor
|
||||
|
||||
@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
|
||||
Files: *
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
deb/usr/share/locale/be/LC_MESSAGES/demon-editor.mo
Normal file
BIN
deb/usr/share/locale/be/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
deb/usr/share/locale/pl/LC_MESSAGES/demon-editor.mo
Normal file
BIN
deb/usr/share/locale/pl/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
deb/usr/share/locale/tr/LC_MESSAGES/demon-editor.mo
Normal file
BIN
deb/usr/share/locale/tr/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
deb/usr/share/locale/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
BIN
deb/usr/share/locale/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
1251
po/be/demon-editor.po
Normal file
1251
po/be/demon-editor.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,19 @@
|
||||
# Copyright (C) 2018-2019 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#Charly, 2019.
|
||||
#
|
||||
# Charly, 2019.
|
||||
# Dmitriy Yefremov, 2020-2021.
|
||||
# Thomas Schmidt, 2021
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Charly\n"
|
||||
"Last-Translator: Dmitriy Yefremov\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "Charly"
|
||||
msgstr "Charly\nDmitriy Yefremov\nThomas Schmidt"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -22,7 +23,7 @@ msgid "Package"
|
||||
msgstr "Paket"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "Model"
|
||||
msgstr "Typ"
|
||||
|
||||
msgid "Picon"
|
||||
msgstr "Picon"
|
||||
@@ -31,7 +32,7 @@ msgid "Freq"
|
||||
msgstr "Freq"
|
||||
|
||||
msgid "Rate"
|
||||
msgstr "Bewertung"
|
||||
msgstr "SR"
|
||||
|
||||
msgid "Pol"
|
||||
msgstr "Pol."
|
||||
@@ -105,6 +106,9 @@ msgstr "Standardbezeichnung setzen"
|
||||
msgid "Insert marker"
|
||||
msgstr "Marker einfügen"
|
||||
|
||||
msgid "Insert space"
|
||||
msgstr "Leerzeichen einfügen"
|
||||
|
||||
msgid "Locate in services"
|
||||
msgstr "In den Diensten suchen"
|
||||
|
||||
@@ -160,7 +164,7 @@ msgid "Remove"
|
||||
msgstr "Entfernen"
|
||||
|
||||
msgid "Remove all unavailable"
|
||||
msgstr "Entfernt alle nicht verfügbaren"
|
||||
msgstr "Entfernen alle nicht verfügbaren"
|
||||
|
||||
msgid "Satellites editor"
|
||||
msgstr "Satelliten-Editor"
|
||||
@@ -201,8 +205,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:"
|
||||
@@ -516,6 +520,9 @@ msgstr "Bitte wählen Sie nur einen Eintrag aus!"
|
||||
msgid "No png file is selected!"
|
||||
msgstr "Es ist keine png-Datei ausgewählt!"
|
||||
|
||||
msgid "No profile selected!"
|
||||
msgstr "Kein Profil ausgewählt!"
|
||||
|
||||
msgid "No reference is present!"
|
||||
msgstr "Es liegt keine Referenz vor!"
|
||||
|
||||
@@ -660,22 +667,22 @@ msgid "Test connection"
|
||||
msgstr "Test Verbindung"
|
||||
|
||||
msgid "Double click on the service in the bouquet list:"
|
||||
msgstr "Doppelklicke auf das Service in der Bouquetliste:"
|
||||
msgstr "Doppelklick auf das Service in der Bouquet-Liste:"
|
||||
|
||||
msgid "Zap"
|
||||
msgstr "Zap"
|
||||
|
||||
msgid "Play stream"
|
||||
msgstr "Play Stream"
|
||||
msgstr "Stream abspielen"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Ausgeschaltet"
|
||||
msgstr "Deaktiviert"
|
||||
|
||||
msgid "Enable ver. 5 support (experimental)"
|
||||
msgstr "Ver.5 Unterstützung aktivieren (experimentell)"
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Lamedb ver. 5 Unterstützung aktivieren"
|
||||
|
||||
msgid "Enable HTTP API (experimental)"
|
||||
msgstr "HTTP-API aktivieren (experimentell)"
|
||||
msgid "Enable HTTP API"
|
||||
msgstr "HTTP-API aktivieren"
|
||||
|
||||
msgid "Switch(zap) the channel(Ctrl + Z)"
|
||||
msgstr "Umschalten des Kanals (Strg + Z)"
|
||||
@@ -684,4 +691,575 @@ msgid "Switch the channel and watch in the program(Ctrl + W)"
|
||||
msgstr "Kanal wechseln und im Programm ansehen(Strg + W)"
|
||||
|
||||
msgid "Play IPTV or other stream in the program(Ctrl + P)"
|
||||
msgstr "Wiedergabe von IPTV oder anderen Streams im Programm(Strg + P)"
|
||||
msgstr "Wiedergabe von IPTV oder anderen Streams im Programm(Strg + P)"
|
||||
|
||||
msgid "Export to m3u"
|
||||
msgstr "Exportieren nach m3u"
|
||||
|
||||
msgid "EPG configuration"
|
||||
msgstr "EPG Konfiguration"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "Anwenden"
|
||||
|
||||
msgid "EPG source"
|
||||
msgstr "EPG Quelle"
|
||||
|
||||
msgid "Service names source:"
|
||||
msgstr "Quelle der Dienstnamen:"
|
||||
|
||||
msgid "Main service list"
|
||||
msgstr "Hauptdienstliste"
|
||||
|
||||
msgid "XML file"
|
||||
msgstr "XML-Datei"
|
||||
|
||||
msgid "Use web source"
|
||||
msgstr "Web-Quelle verwenden"
|
||||
|
||||
msgid "Url to *.xml.gz file:"
|
||||
msgstr "Url zur *.xml.gz Datei:"
|
||||
|
||||
msgid "Enable filtering"
|
||||
msgstr "Filter aktivieren"
|
||||
|
||||
msgid "Filter by presence in the epg.dat file."
|
||||
msgstr "Filtern nach dem Vorhandensein in der epg.dat Datei."
|
||||
|
||||
msgid "Paths to the epg.dat file:"
|
||||
msgstr "Pfad zur epg.dat Datei:"
|
||||
|
||||
msgid "Local path:"
|
||||
msgstr "Local path:"
|
||||
|
||||
msgid "STB path:"
|
||||
msgstr "STB-Pfad:"
|
||||
|
||||
msgid "Update on start"
|
||||
msgstr "Update beim Start"
|
||||
|
||||
msgid "Auto configuration by service names."
|
||||
msgstr "Automatische Konfiguration nach Dienstnamen."
|
||||
|
||||
msgid "Save list to xml."
|
||||
msgstr "Liste in XML speichern."
|
||||
|
||||
msgid "Download XML file error."
|
||||
msgstr "Fehler beim Herunterladen der XML-Datei."
|
||||
|
||||
msgid "Unsupported file type:"
|
||||
msgstr "Nicht unterstützter Dateityp:"
|
||||
|
||||
msgid "Unpacking data error."
|
||||
msgstr "Fehler beim Entpacken der Daten."
|
||||
|
||||
msgid "XML parsing error:"
|
||||
msgstr "XML Parsing-Fehler:"
|
||||
|
||||
msgid "Count of successfully configured services:"
|
||||
msgstr "Anzahl der erfolgreich konfigurierten Dienste:"
|
||||
|
||||
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
|
||||
msgstr "Die aktuelle epg.dat Datei enthält keine Referenzen für die Dienste dieses Bouquets!"
|
||||
|
||||
msgid "Use HTTP"
|
||||
msgstr "HTTP verwenden"
|
||||
|
||||
msgid "Close playback"
|
||||
msgstr "Wiedergabe schließen"
|
||||
|
||||
msgid "Import YouTube playlist"
|
||||
msgstr "YouTube-Wiedergabeliste importieren"
|
||||
|
||||
msgid ""
|
||||
"Found a link to the YouTube resource!\n"
|
||||
"Try to get a direct link to the video?"
|
||||
msgstr ""
|
||||
"Link zur YouTube-Ressource gefunden!\n"
|
||||
"Soll versucht werden, einen Direkt-Link zum Video zu erzeugen?"
|
||||
|
||||
msgid "Playlist import"
|
||||
msgstr "Playlist-Import"
|
||||
|
||||
msgid "Getting link error:"
|
||||
msgstr "Link-Fehler erhalten:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Extras"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Profileinstellungen anwenden"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Art der Einstellungen:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Standard wiederherstellen"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Sprache:"
|
||||
|
||||
msgid "Load the last open configuration at program startup"
|
||||
msgstr "Laden der zuletzt geöffneten Konfiguration beim Programmstart"
|
||||
|
||||
msgid "Enable direct playback bar"
|
||||
msgstr "Aktivieren der direkten Wiedergabeleiste"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Receiver"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Kanal im Programm ansehen"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Zap und Abspielen"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Link hineinziehen oder einfügen"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Hinzugefügte Links in der Wiedergabeliste entfernen"
|
||||
|
||||
msgid "A bouquet with that name exists!"
|
||||
msgstr "Bouquet mit diesem Namen existiert!"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Details"
|
||||
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "Reset"
|
||||
|
||||
msgid "File"
|
||||
msgstr "Datei"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Picons-Manager"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr "Explorer"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "Satellit URL:"
|
||||
|
||||
msgid "Cut"
|
||||
msgstr "Ausschneiden"
|
||||
|
||||
msgid "Paste"
|
||||
msgstr "Einfügen"
|
||||
|
||||
msgid "To the top"
|
||||
msgstr "Zum Anfang"
|
||||
|
||||
msgid "To the end"
|
||||
msgstr "Zum Ende"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Darstellung"
|
||||
|
||||
msgid "Lock"
|
||||
msgstr "Sperre"
|
||||
|
||||
msgid "Parent lock"
|
||||
msgstr "Elternsperre"
|
||||
|
||||
msgid "Hide/Skip"
|
||||
msgstr "Ausblenden/Überspringen"
|
||||
|
||||
msgid "IPTV tools"
|
||||
msgstr "IPTV-Tools"
|
||||
|
||||
msgid "Make profile folder as default for the additional data"
|
||||
msgstr "Profilordner als Standard für die zusätzlichen Daten festlegen"
|
||||
|
||||
msgid "Default data path:"
|
||||
msgstr "Standard-Datenpfad:"
|
||||
|
||||
msgid "Streams record path:"
|
||||
msgstr "Streams Aufnahmepfad:"
|
||||
|
||||
msgid "Record"
|
||||
msgstr "Aufnahme"
|
||||
|
||||
msgid "Record:"
|
||||
msgstr "Aufnahme:"
|
||||
|
||||
msgid "Record to disk:"
|
||||
msgstr "Aufnahme auf Festplatte:"
|
||||
|
||||
msgid "Streaming"
|
||||
msgstr "Streaming"
|
||||
|
||||
msgid "Activate transcoding"
|
||||
msgstr "Aktivieren der Transkodierung"
|
||||
|
||||
msgid "Presets:"
|
||||
msgstr "Voreinstellungen:"
|
||||
|
||||
msgid "Video options:"
|
||||
msgstr "Video-Optionen:"
|
||||
|
||||
msgid "Audio options:"
|
||||
msgstr "Audio-Optionen:"
|
||||
|
||||
msgid "Bitrate (kb/s):"
|
||||
msgstr "Bitrate (kb/s):"
|
||||
|
||||
msgid "Codec:"
|
||||
msgstr "Codec:"
|
||||
|
||||
msgid "Width (px):"
|
||||
msgstr "Breite (px):"
|
||||
|
||||
msgid "Height (px):"
|
||||
msgstr "Höhe (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Kanäle:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Sample-Rate (Hz):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Stream-Abspielmodus:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Integrierter Player"
|
||||
|
||||
msgid "In a separate window"
|
||||
msgstr "In einem separaten Fenster"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Nur m3u-Datei erhalten"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
msgstr "Änderungen speichern und anschließend das Programm neustarten, um die Einstellungen zu übernehmen."
|
||||
|
||||
msgid "Some images may have problems displaying the favorites list!"
|
||||
msgstr "Einige Bilder können Probleme in der Anzeige der Favoritenliste haben!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Arbeitet im Standby-Modus oder der Transponder ist bereits in Verwendung!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Keine Verbindung zum Receiver!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Signalpegel"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Receiver-Info"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
msgstr "Ein Profil mit diesem Namen existiert!"
|
||||
|
||||
msgid "Show short info as hints in the main services list"
|
||||
msgstr "Kurzinfos als Tooltips in der Hauptliste anzeigen"
|
||||
|
||||
msgid "Show detailed info as hints in the bouquet list"
|
||||
msgstr "Detaillierte Informationen als Tooltips in der Bouquetliste anzeigen"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Aktivieren der alternativen Benennung für Bouquet-Dateien"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Ermöglicht Bouquet-Dateien mit ihren Namen zu benennen."
|
||||
|
||||
msgid "Appearance"
|
||||
msgstr "Aussehen"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Theme Unterstützung aktivieren"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Gtk3-Theme:"
|
||||
|
||||
msgid "Icon Theme:"
|
||||
msgstr "Icon-Theme:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 Themes und Icons:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Daten löschen..."
|
||||
|
||||
msgid "Download from the receiver"
|
||||
msgstr "Downloaden vom Receiver"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Alle Picons vom dem Receiver entfernen"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Kanalreferenz"
|
||||
|
||||
msgid "Enable support for"
|
||||
msgstr "Unterstützung aktivieren für"
|
||||
|
||||
msgid "Auto-check for updates"
|
||||
msgstr "Automatische Prüfung auf Updates"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Dienste filtern"
|
||||
|
||||
msgid "Filter services in the main list."
|
||||
msgstr "Dienste in der Hauptliste filtern."
|
||||
|
||||
msgid "Destination:"
|
||||
msgstr "Ziel:"
|
||||
|
||||
msgid "EXPERIMENTAL!"
|
||||
msgstr "EXPERIMENTELL!"
|
||||
|
||||
msgid "Sorting data..."
|
||||
msgstr "Daten sortieren..."
|
||||
|
||||
msgid "There are unsaved changes.\n\n\t Save them now?"
|
||||
msgstr "Es gibt ungespeicherte Änderungen.\n\n\t Möchtest du jetzt speichern?"
|
||||
|
||||
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
|
||||
msgstr "Bist du sicher, dass du die Reihenfolge der Dienstleistungen\n\t in diesem Bouquet ändern willst?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Vom Receiver entfernen"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Screenshot"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Video"
|
||||
|
||||
msgid "The Neutrino has only experimental support. Not all features are supported!"
|
||||
msgstr "Die Neutrino hat nur experimentelle Unterstützung. Nicht alle Funktionen werden unterstützt!"
|
||||
|
||||
msgid "Enable experimental features"
|
||||
msgstr "Experimentelle Funktionen aktivieren"
|
||||
|
||||
msgid "Can't Playback!"
|
||||
msgstr "Kann nicht abgespielt werden!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Dunkler Modus 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-Import abgeschlossen!"
|
||||
|
||||
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 Internet"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Steuerung"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Timer"
|
||||
|
||||
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 aufnehmen"
|
||||
|
||||
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 "Wiederholt:"
|
||||
|
||||
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!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "Abspiel-Modus wurde geändert!\nStarte das Programm neu, um die Einstellungen zu übernehmen."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Stelle die Werte für TID, NID und Namespace für die korrekte Benennung der Picons ein!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Streams gefunden:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Download picons"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Fehler:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Zum Abspielen von Streams verwenden:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Schrift in den Listen:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Picons Größe in den Listen:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Logo-Größe in Tooltips:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Speichern als"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Duplikate markieren"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Nur für ausgewähltes Bouquet laden"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "Der Task wird abgebrochen!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Daten werden geladen!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Aufnahmen"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Hilfe"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2019 Frank Neirynck
|
||||
# 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"
|
||||
@@ -486,7 +486,7 @@ msgid "STB file paths:"
|
||||
msgstr "Rutas de ficheros del receptor:"
|
||||
|
||||
msgid "Local file paths:"
|
||||
msgstr "Rutas de ficheros local:"
|
||||
msgstr "Rutas de ficheros locales:"
|
||||
|
||||
# Dialogs messages
|
||||
msgid "Error. No bouquet is selected!"
|
||||
@@ -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"
|
||||
@@ -620,7 +620,7 @@ msgid "Before downloading from the receiver"
|
||||
msgstr "Antes de recibir del receptor"
|
||||
|
||||
msgid "Set background color for the services"
|
||||
msgstr "Determinar color de fondo de los servicios"
|
||||
msgstr "Fijar color de fondo de los servicios"
|
||||
|
||||
msgid "Marked as new:"
|
||||
msgstr "Marcado como nuevo:"
|
||||
@@ -666,22 +666,22 @@ msgid "Test connection"
|
||||
msgstr "Probar conexión"
|
||||
|
||||
msgid "Double click on the service in the bouquet list:"
|
||||
msgstr "Haga doble clic en el servicio en la lista de bouquet:"
|
||||
msgstr "Al hacer doble clic en el servicio en la lista de bouquet:"
|
||||
|
||||
msgid "Zap"
|
||||
msgstr "Zapear"
|
||||
|
||||
msgid "Play stream"
|
||||
msgstr "Reproducir flujo"
|
||||
msgstr "Reproducir stream"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Desactivado"
|
||||
|
||||
msgid "Enable ver. 5 support (experimental)"
|
||||
msgstr "Soporte para ver. 5 (experimental)"
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Soporte para lamedb ver. 5"
|
||||
|
||||
msgid "Enable HTTP API (experimental)"
|
||||
msgstr "Habilitar API HTTP (experimental)"
|
||||
msgid "Enable HTTP API"
|
||||
msgstr "Habilitar API HTTP"
|
||||
|
||||
msgid "Switch(zap) the channel(Ctrl + Z)"
|
||||
msgstr "Poner el canal (Ctrl + Z)"
|
||||
@@ -690,7 +690,7 @@ msgid "Switch the channel and watch in the program(Ctrl + W)"
|
||||
msgstr "Poner el canal y ver en el programa (Ctrl + W)"
|
||||
|
||||
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"
|
||||
@@ -771,10 +771,257 @@ msgid "Import YouTube playlist"
|
||||
msgstr "Importar lista de reproducción de YouTube"
|
||||
|
||||
msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
|
||||
msgstr "¡Encontrado enlace al recurso de YouTube!\n¿Intentar obtener un enlace directo al vídeo?"
|
||||
msgstr "¡Se ha encontrado enlace al recurso de YouTube!\n¿Intentar obtener un enlace directo al vídeo?"
|
||||
|
||||
msgid "Playlist import"
|
||||
msgstr "Importar lista de reproducción"
|
||||
|
||||
msgid "Getting link error:"
|
||||
msgstr "Error en el enlace:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Extra"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Aplicar ajustes de perfil"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Tipo de ajustes:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Por defecto"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Idioma:"
|
||||
|
||||
msgid "Load the last open configuration at program startup"
|
||||
msgstr "Cargar la última configuración usada al iniciar el programa"
|
||||
|
||||
msgid "Enable direct playback bar"
|
||||
msgstr "Habilitar la barra de reproducción directa"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Ver el canal en el programa"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Zapear y reproducir"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Soltar o pegar en enlace aquí"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Quitar los enlaces añadidos en la lista de reproducción"
|
||||
|
||||
msgid "A bouquet with that name exists!"
|
||||
msgstr "¡Ya existe un bouquet con ese nombre!"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Detalles"
|
||||
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "Reset"
|
||||
|
||||
msgid "File"
|
||||
msgstr "Fichero"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Gestor de picons"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr "Explorador"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "Url satélite:"
|
||||
|
||||
msgid "Cut"
|
||||
msgstr "Cortar"
|
||||
|
||||
msgid "Paste"
|
||||
msgstr "Pegar"
|
||||
|
||||
msgid "To the top"
|
||||
msgstr "Ir arriba"
|
||||
|
||||
msgid "To the end"
|
||||
msgstr "Al final"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
msgid "Lock"
|
||||
msgstr "Bloqueo"
|
||||
|
||||
msgid "Parent lock"
|
||||
msgstr "Bloqueo parental"
|
||||
|
||||
msgid "Hide/Skip"
|
||||
msgstr "Escoder/Saltar"
|
||||
|
||||
msgid "IPTV tools"
|
||||
msgstr "Intrumentos IPTV"
|
||||
|
||||
msgid "Make profile folder as default for the additional data"
|
||||
msgstr "Usar por defecto el directorio de perfil para los datos adionales"
|
||||
|
||||
msgid "Default data path:"
|
||||
msgstr "Ruta estándar de datos:"
|
||||
|
||||
msgid "Streams record path:"
|
||||
msgstr "Ruta de grabación del stream:"
|
||||
|
||||
msgid "Record"
|
||||
msgstr "Grabación"
|
||||
|
||||
msgid "Record:"
|
||||
msgstr "Grabación:"
|
||||
|
||||
msgid "Record to disk:"
|
||||
msgstr "Grabación en disco:"
|
||||
|
||||
msgid "Streaming"
|
||||
msgstr "Streaming"
|
||||
|
||||
msgid "Activate transcoding"
|
||||
msgstr "Activar transcodificación"
|
||||
|
||||
msgid "Presets:"
|
||||
msgstr "Preajustes:"
|
||||
|
||||
msgid "Video options:"
|
||||
msgstr "Opciones vídeo:"
|
||||
|
||||
msgid "Audio options:"
|
||||
msgstr "Opciones audio:"
|
||||
|
||||
msgid "Bitrate (kb/s):"
|
||||
msgstr "Bitrate (kb/s):"
|
||||
|
||||
msgid "Codec:"
|
||||
msgstr "Códec:"
|
||||
|
||||
msgid "Width (px):"
|
||||
msgstr "Ancho (px):"
|
||||
|
||||
msgid "Height (px):"
|
||||
msgstr "Alto (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Canales:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Frecuencia de muestreo (Hz):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Modo de reproducción de los streams:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Reproductor interno"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "Reproductor VLC"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Sólo bajar fichero m3u"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
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 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!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "¡Desconectado del receptor!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Nivel de señal"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Información sobre el receptor"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
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 consejo en la lista de bouquets"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Habilitar nombres alternativos para los ficheros de bouquets"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Permite nombrar ficheros de bouquets usando sus propios nombres."
|
||||
|
||||
msgid "Appearance"
|
||||
msgstr "Apariencia"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Habilitar compatibilidad con temas"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Теma Gtk3:"
|
||||
|
||||
msgid "Icon Theme:"
|
||||
msgstr "Icono del tema:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Tema Gtk3 e iconos:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Borrando datos..."
|
||||
|
||||
msgid "Download from the receiver"
|
||||
msgstr "Descargar desde el receptor"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Eliminar todos los picons del receptor"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Referencia del servicio"
|
||||
|
||||
msgid "Enable support for"
|
||||
msgstr "Habilitar soporte para"
|
||||
|
||||
msgid "Auto-check for updates"
|
||||
msgstr "Comprobación automática de actualizaciones"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Filtrar servicios"
|
||||
|
||||
msgid "Filter services in the main list."
|
||||
msgstr "Filtrar servicios en la lista principal."
|
||||
|
||||
msgid "Destination:"
|
||||
msgstr "Destino:"
|
||||
|
||||
msgid "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 ¿Desea guardarlos ahora?"
|
||||
|
||||
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
|
||||
msgstr "¿Está seguro de querer cambiar el orden\n\t de servicios en este bouquet?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Eliminar del receptor"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Captura de pantalla"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Vídео"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2019 Frank Neirynck
|
||||
# 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 ""
|
||||
@@ -10,11 +10,6 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "Frank Neirynck <frank@insink.be>"
|
||||
@@ -206,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:"
|
||||
@@ -676,11 +671,11 @@ msgstr "Speel stream af"
|
||||
msgid "Disabled"
|
||||
msgstr "Uitgeschakeld"
|
||||
|
||||
msgid "Enable ver. 5 support (experimental)"
|
||||
msgstr "Ondersteuning voor ver. 5 inschakelen (experimenteel)"
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Ondersteuning voor lamedb ver. 5 inschakelen"
|
||||
|
||||
msgid "Enable HTTP API (experimental)"
|
||||
msgstr "HTTP API inschakelen (experimenteel)"
|
||||
msgid "Enable HTTP API"
|
||||
msgstr "HTTP API inschakelen"
|
||||
|
||||
msgid "Switch(zap) the channel(Ctrl + Z)"
|
||||
msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)"
|
||||
@@ -776,4 +771,245 @@ msgid "Playlist import"
|
||||
msgstr "Importeer playlist"
|
||||
|
||||
msgid "Getting link error:"
|
||||
msgstr "Volgende Link error gekregen:"
|
||||
msgstr "Volgende Link error gekregen:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Extra"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Pas profiel settings toe"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Settings type:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Stel standaard in"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Taal:"
|
||||
|
||||
msgid "Load the last open configuration at program startup"
|
||||
msgstr "Laad de laatst geopende configuratie op bij opstart programma"
|
||||
|
||||
msgid "Enable direct playback bar"
|
||||
msgstr "Laat onmiddelijk playback bar toe"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Laat rechtstreeks versturen van and playback en media links op de ontvanger toe"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Bekijk het kanaal in het programma"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Zap en speel af"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Sleep of plak link naar hier"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Verwijder toegevoegde links uit playlist"
|
||||
|
||||
msgid "A bouquet with that name exists!"
|
||||
msgstr "Er bestaat al een boeket met deze naam!"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Details"
|
||||
|
||||
msgid "Profile"
|
||||
msgstr "Profiel"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "Reset"
|
||||
|
||||
msgid "File"
|
||||
msgstr "File"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Picons manager"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr "Explorer"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "Satelliet url:"
|
||||
|
||||
msgid "Cut"
|
||||
msgstr "Knip"
|
||||
|
||||
msgid "Paste"
|
||||
msgstr "Plak"
|
||||
|
||||
msgid "To the top"
|
||||
msgstr "Naar de top"
|
||||
|
||||
msgid "To the end"
|
||||
msgstr "Naar het einde"
|
||||
|
||||
msgid "View"
|
||||
msgstr "View"
|
||||
|
||||
msgid "Lock"
|
||||
msgstr "Slot"
|
||||
|
||||
msgid "Parent lock"
|
||||
msgstr "Ouderlijk Slot"
|
||||
|
||||
msgid "Hide/Skip"
|
||||
msgstr "Verberg/Spring"
|
||||
|
||||
msgid "IPTV tools"
|
||||
msgstr "IPTV instrumenten"
|
||||
|
||||
msgid "Make profile folder as default for the additional data"
|
||||
msgstr "Maak profiel map standaard voor addionele data"
|
||||
|
||||
msgid "Default data path:"
|
||||
msgstr "Standaard data pad:"
|
||||
|
||||
msgid "Streams record path:"
|
||||
msgstr "Opnamepad streams:"
|
||||
|
||||
msgid "Record"
|
||||
msgstr "Opnemen"
|
||||
|
||||
msgid "Record:"
|
||||
msgstr "Opnemen:"
|
||||
|
||||
msgid "Record to disk:"
|
||||
msgstr "Opnemen op schijf:"
|
||||
|
||||
msgid "Streaming"
|
||||
msgstr "Streaming"
|
||||
|
||||
msgid "Activate transcoding"
|
||||
msgstr "Activeer transcodering"
|
||||
|
||||
msgid "Presets:"
|
||||
msgstr "Onstellingen:"
|
||||
|
||||
msgid "Video options:"
|
||||
msgstr "Video opties:"
|
||||
|
||||
msgid "Audio options:"
|
||||
msgstr "Audio opties:"
|
||||
|
||||
msgid "Bitrate (kb/s):"
|
||||
msgstr "Bitrate (kb/s):"
|
||||
|
||||
msgid "Codec:"
|
||||
msgstr "Codec:"
|
||||
|
||||
msgid "Width (px):"
|
||||
msgstr "Breedte (px):"
|
||||
|
||||
msgid "Height (px):"
|
||||
msgstr "Hoogte (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Kanalen:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Sample rate (Hz):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Speel in streams mode:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Ingebouwde speler"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "VLC Media speler"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Enkel de m3u file ophalen"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
msgstr "Het programma opslaan en herstarten om settings toe te passen."
|
||||
|
||||
msgid "Some images may have problems displaying the favorites list!"
|
||||
msgstr "Sommige afbeeldingen kunnen problemen opleveren bij vertonen in de favorietenlijst!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Werkt in standby mode of op dehuiduge actieve transponder!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Geen verbinding met de ontvanger!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Signaal niveau"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Informatie over de ontvanger"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
msgstr "Er bestaat al een profiel met die naam!"
|
||||
|
||||
msgid "Show short info as hints in the main services list"
|
||||
msgstr "Toon korte info als hints in de hoofd servicelijst"
|
||||
|
||||
msgid "Show detailed info as hints in the bouquet list"
|
||||
msgstr "Toon gedetalleerde info als hints in de boeket lijst"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Laat alternatieve boeket benamingen toe"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Laat toe om boeket te noemen naar bestandsbenaming."
|
||||
|
||||
msgid "Appearance"
|
||||
msgstr "Uitzicht"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Laat themaondersteuning toe"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Gtk3 Thema:"
|
||||
|
||||
msgid "Icon Theme:"
|
||||
msgstr "Icoon Thema:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 en Icoon Themas:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Wist data ..."
|
||||
|
||||
msgid "Download from the receiver"
|
||||
msgstr "Download van de ontvanger"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Service referentie"
|
||||
|
||||
msgid "Auto-check for updates"
|
||||
msgstr "Auto-check voor updates"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Filter diensten"
|
||||
|
||||
msgid "Filter services in the main list."
|
||||
msgstr "Filter diensten in de hoofdlijst."
|
||||
|
||||
msgid "Destination:"
|
||||
msgstr "Doel:"
|
||||
|
||||
msgid "EXPERIMENTAL!"
|
||||
msgstr "EXPERIMENTEEL!"
|
||||
|
||||
msgid "Sorting data..."
|
||||
msgstr "Data ordenen..."
|
||||
|
||||
msgid "There are unsaved changes.\n\n\t Save them now?"
|
||||
msgstr "Er zijn niet-bwaarde wijzigingen.\n\n\t Nu opslaan?"
|
||||
|
||||
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
|
||||
msgstr "Ben je zeker dat je de volgorde\n\t van de diensten in dit boeket wil wijzigen?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Verwijder van de ontvanger"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Schermafbeelding"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Vidео"
|
||||
|
||||
|
||||
1004
po/pl/demon-editor.po
Executable file
1004
po/pl/demon-editor.po
Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2019 Frank Neirynck
|
||||
# Copyright (C) 2018-2020 Frank Neirynck
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#Frank Neirynck <frank@insink.be>, 2018-2019.
|
||||
@@ -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:"
|
||||
@@ -663,11 +663,11 @@ msgstr "Play stream"
|
||||
msgid "Disabled"
|
||||
msgstr "Desativado"
|
||||
|
||||
msgid "Enable ver. 5 support (experimental)"
|
||||
msgstr "Ativar ver. 5 suporte (experimental)"
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Ativar lamedb ver. 5 suporte"
|
||||
|
||||
msgid "Enable HTTP API (experimental)"
|
||||
msgstr "Ativar HTTP API (experimental)"
|
||||
msgid "Enable HTTP API"
|
||||
msgstr "Ativar HTTP API"
|
||||
|
||||
msgid "Switch(zap) the channel(Ctrl + Z)"
|
||||
msgstr "Mudar(zap) o canal(Ctrl + Z)"
|
||||
@@ -763,4 +763,250 @@ msgid "Playlist import"
|
||||
msgstr "Importação de lista de reprodução"
|
||||
|
||||
msgid "Getting link error:"
|
||||
msgstr "Obtendo erro de link:"
|
||||
msgstr "Obtendo erro de link:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Extra"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Aplicar ajustes de perfil"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Tipo de ajustes:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Por defecto"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Idioma:"
|
||||
|
||||
msgid "Load the last open configuration at program startup"
|
||||
msgstr "Cargar la última configuración abierta al iniciar el programa"
|
||||
|
||||
msgid "Enable direct playback bar"
|
||||
msgstr "Habilitar la barra de reproducción directa"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Ver el canal en el programa"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Zapear y reproducir"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Soltar o pegar en enlace aquí"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Quitar los enlaces añadidos en la lista de reproducción"
|
||||
|
||||
msgid "A bouquet with that name exists!"
|
||||
msgstr "¡Ya existe un bouquet con ese nombre!"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Detalles"
|
||||
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "Reset"
|
||||
|
||||
msgid "File"
|
||||
msgstr "Archivo"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Picons manager"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr "Explorador"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "Url Satelite:"
|
||||
|
||||
msgid "Cut"
|
||||
msgstr "Cortar"
|
||||
|
||||
msgid "Paste"
|
||||
msgstr "Pegar"
|
||||
|
||||
msgid "To the top"
|
||||
msgstr "Ir arriba"
|
||||
|
||||
msgid "To the end"
|
||||
msgstr "Al final"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
msgid "Lock"
|
||||
msgstr "Bloqueo"
|
||||
|
||||
msgid "Parent lock"
|
||||
msgstr "Bloqueo parental"
|
||||
|
||||
msgid "Hide/Skip"
|
||||
msgstr "Escoder/Saltar"
|
||||
|
||||
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"
|
||||
|
||||
msgid "Default data path:"
|
||||
msgstr "Ruta estandar de datos:"
|
||||
|
||||
msgid "Streams record path:"
|
||||
msgstr "Ruta de gravacion de stream:"
|
||||
|
||||
msgid "Record"
|
||||
msgstr "Gravar"
|
||||
|
||||
msgid "Record:"
|
||||
msgstr "Gravar:"
|
||||
|
||||
msgid "Record to disk:"
|
||||
msgstr "Gravar en disco:"
|
||||
|
||||
msgid "Streaming"
|
||||
msgstr "Streameando"
|
||||
|
||||
msgid "Activate transcoding"
|
||||
msgstr "Activer transcodificacion"
|
||||
|
||||
msgid "Presets:"
|
||||
msgstr "Presets:"
|
||||
|
||||
msgid "Video options:"
|
||||
msgstr "Opciones Video:"
|
||||
|
||||
msgid "Audio options:"
|
||||
msgstr "Opciones Audio:"
|
||||
|
||||
msgid "Bitrate (kb/s):"
|
||||
msgstr "Bitrate (kb/s):"
|
||||
|
||||
msgid "Codec:"
|
||||
msgstr "Codec:"
|
||||
|
||||
msgid "Width (px):"
|
||||
msgstr "Ancho (px):"
|
||||
|
||||
msgid "Height (px):"
|
||||
msgstr "Alto (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Canales:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Sample rate (Гц):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Tocar en modo streams:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Reproductor interno"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "Reproductor VLC"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Solo bajar archivo *.m3u"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
msgstr "Guarde y reinicie 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!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Funciona en modo de espera o transpondedor activo actual!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Sin conexión al receptor!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Nivel de señal"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Informacion sobre receptor"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
msgstr "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"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Habilitar nombres alternativos de archivos de bouquet"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Le permite nombrar archivos de bouquet usando sus nombres."
|
||||
|
||||
msgid "Appearance"
|
||||
msgstr "Apariencia"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Habilitar compatibilidad con temas"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Тема Gtk3:"
|
||||
|
||||
msgid "Icon Theme:"
|
||||
msgstr "Тема Icono:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Tema Gtk3 e Iconos:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Borrando datos ..."
|
||||
|
||||
msgid "Download from the receiver"
|
||||
msgstr "Descargar desde el receptor"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Eliminar todos los picons del receptor"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Referencia de servicio"
|
||||
|
||||
msgid "Enable support for"
|
||||
msgstr "Habilitar soporte para"
|
||||
|
||||
msgid "Auto-check for updates"
|
||||
msgstr "Verificación automática de actualizaciones"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Filtrar servicios"
|
||||
|
||||
msgid "Filter services in the main list."
|
||||
msgstr "Filtrar servicios en la lista principal."
|
||||
|
||||
msgid "Destination:"
|
||||
msgstr "Destino:"
|
||||
|
||||
msgid "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?"
|
||||
|
||||
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?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Retirar del receptor"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Captura de pantalla"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Vidео"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2019 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
@@ -104,6 +104,9 @@ msgstr "Имя по умолчанию"
|
||||
msgid "Insert marker"
|
||||
msgstr "Вставить маркер"
|
||||
|
||||
msgid "Insert space"
|
||||
msgstr "Вставить пробел"
|
||||
|
||||
msgid "Locate in services"
|
||||
msgstr "Найти в списке сервисов"
|
||||
|
||||
@@ -200,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 "Адрес ресивера:"
|
||||
@@ -507,6 +510,9 @@ msgstr "Пожалуйста, выберите только один элеме
|
||||
msgid "No png file is selected!"
|
||||
msgstr "Не выбран png файл!"
|
||||
|
||||
msgid "No profile selected!"
|
||||
msgstr "Не выбран профиль!"
|
||||
|
||||
msgid "No reference is present!"
|
||||
msgstr "Ссылка не найдена!"
|
||||
|
||||
@@ -662,17 +668,17 @@ msgstr "Воспр. потока"
|
||||
msgid "Disabled"
|
||||
msgstr "Выкл."
|
||||
|
||||
msgid "Enable ver. 5 support (experimental)"
|
||||
msgstr "Включить поддержку вер. 5 (экспериментально)"
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Включить поддержку lamedb вер. 5"
|
||||
|
||||
msgid "Enable HTTP API (experimental)"
|
||||
msgstr "Включить HTTP API (экспериментально)"
|
||||
msgid "Enable HTTP API"
|
||||
msgstr "Включить HTTP API"
|
||||
|
||||
msgid "Switch(zap) the channel(Ctrl + Z)"
|
||||
msgstr "Переключить канал(Ctrl + Z)"
|
||||
|
||||
msgid "Switch the channel and watch in the program(Ctrl + W)"
|
||||
msgstr "Переклють канал и просмотр в программе(Ctrl + W)."
|
||||
msgstr "Переключить канал и просмотр в программе(Ctrl + W)."
|
||||
|
||||
msgid "Play IPTV or other stream in the program(Ctrl + P)"
|
||||
msgstr "Воспроизведение IPTV или другого потока в программе(Ctrl + P)"
|
||||
@@ -763,3 +769,480 @@ msgstr "Импорт плейлиста"
|
||||
|
||||
msgid "Getting link error:"
|
||||
msgstr "Ошибка получения ссылки:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Дополнительно"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Применить настройки профиля"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Тип настроек:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Установить по умолчанию"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Язык:"
|
||||
|
||||
msgid "Load the last open configuration at program startup"
|
||||
msgstr "Загружать последнюю открытую конфигурацию при запуске программы"
|
||||
|
||||
msgid "Enable direct playback bar"
|
||||
msgstr "Включить панель прямого воспроизведения"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Включает прямую отправку и воспроизведение медиа-ссылок на ресивере"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Просмотр канала в программе"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Перекл. и просмотр"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Перетащите или вставьте ссылку здесь"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Удалить добавленные ссылки из плейлиста"
|
||||
|
||||
msgid "A bouquet with that name exists!"
|
||||
msgstr "Букет с таким именем существует!"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Подробно"
|
||||
|
||||
msgid "Profile"
|
||||
msgstr "Профиль"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "Сброс"
|
||||
|
||||
msgid "File"
|
||||
msgstr "Файл"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Менеджер пиконов"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr "Проводник"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "URL cпутника:"
|
||||
|
||||
msgid "Cut"
|
||||
msgstr "Вырезать"
|
||||
|
||||
msgid "Paste"
|
||||
msgstr "Вставить"
|
||||
|
||||
msgid "To the top"
|
||||
msgstr "В начало"
|
||||
|
||||
msgid "To the end"
|
||||
msgstr "В конец"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Вид"
|
||||
|
||||
msgid "Lock"
|
||||
msgstr "Замок"
|
||||
|
||||
msgid "Parent lock"
|
||||
msgstr "Родительский замок"
|
||||
|
||||
msgid "Hide/Skip"
|
||||
msgstr "Скрыть/Пропустить"
|
||||
|
||||
msgid "IPTV tools"
|
||||
msgstr "Инструменты IPTV"
|
||||
|
||||
msgid "Make profile folder as default for the additional data"
|
||||
msgstr "Установить папку профиля по умолчанию для доп. данных"
|
||||
|
||||
msgid "Default data path:"
|
||||
msgstr "Путь к данным по умолчанию:"
|
||||
|
||||
msgid "Streams record path:"
|
||||
msgstr "Путь к записям потоков:"
|
||||
|
||||
msgid "Record"
|
||||
msgstr "Запись"
|
||||
|
||||
msgid "Record:"
|
||||
msgstr "Запись:"
|
||||
|
||||
msgid "Record to disk:"
|
||||
msgstr "Запись на диск:"
|
||||
|
||||
msgid "Streaming"
|
||||
msgstr "Потоки"
|
||||
|
||||
msgid "Activate transcoding"
|
||||
msgstr "Активировать перекодировку"
|
||||
|
||||
msgid "Presets:"
|
||||
msgstr "Предустановки:"
|
||||
|
||||
msgid "Video options:"
|
||||
msgstr "Опции видео:"
|
||||
|
||||
msgid "Audio options:"
|
||||
msgstr "Опции аудио:"
|
||||
|
||||
msgid "Bitrate (kb/s):"
|
||||
msgstr "Битрейт (kb/s):"
|
||||
|
||||
msgid "Codec:"
|
||||
msgstr "Кодек:"
|
||||
|
||||
msgid "Width (px):"
|
||||
msgstr "Ширина (px):"
|
||||
|
||||
msgid "Height (px):"
|
||||
msgstr "Высота (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Каналы:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Частота дискр. (Гц):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Режим воспроизведения потоков:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Встроенный плеер"
|
||||
|
||||
msgid "In a separate window"
|
||||
msgstr "В отдельном окне"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Получить файл *.m3u"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
msgstr "Сохраните и перезапустите программу, чтобы применить настройки."
|
||||
|
||||
msgid "Some images may have problems displaying the favorites list!"
|
||||
msgstr "Некоторые образы могут иметь проблемы с отображением списка избранного!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Работает в режиме ожидания или текущем активном транспондере!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Нет соединение с ресивером!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Уровень сигнала"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Информация о ресивере"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
msgstr "Профиль с таким именем существует!"
|
||||
|
||||
msgid "Show short info as hints in the main services list"
|
||||
msgstr "Показывать краткую информацию в виде подсказок в основном списке услуг"
|
||||
|
||||
msgid "Show detailed info as hints in the bouquet list"
|
||||
msgstr "Показывать подробную информацию в виде подсказок в списке букетов"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Включить альтернативное именование файлов букета"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Позволяет называть файлы букетов, используя их имена."
|
||||
|
||||
msgid "Appearance"
|
||||
msgstr "Внешний вид"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Включить поддержку тем"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Тема Gtk3:"
|
||||
|
||||
msgid "Icon Theme:"
|
||||
msgstr "Тема значков:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 темы и иконки:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Удаление данных ..."
|
||||
|
||||
msgid "Download from the receiver"
|
||||
msgstr "Загрузить с ресивера"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Удалить все пиконы с ресивера"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Сервисная ссылка"
|
||||
|
||||
msgid "Enable support for"
|
||||
msgstr "Включить поддержку"
|
||||
|
||||
msgid "Auto-check for updates"
|
||||
msgstr "Автопроверка обновлений"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Фильтровать сервисы"
|
||||
|
||||
msgid "Filter services in the main list."
|
||||
msgstr "Фильтровать сервисы в основном списке."
|
||||
|
||||
msgid "Destination:"
|
||||
msgstr "Назначение:"
|
||||
|
||||
msgid "EXPERIMENTAL!"
|
||||
msgstr "ЭКСПЕРИМЕНТАЛЬНО!"
|
||||
|
||||
msgid "Sorting data..."
|
||||
msgstr "Сортировка данных..."
|
||||
|
||||
msgid "There are unsaved changes.\n\n\t Save them now?"
|
||||
msgstr "Имеются несохранённые изменения.\n\n\t Сохранить их сейчас?"
|
||||
|
||||
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
|
||||
msgstr "Вы уверены, что хотите изменить порядок\n\t сервисов в этом букете?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Удалить с ресивера"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Скриншот"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Видео"
|
||||
|
||||
msgid "The Neutrino has only experimental support. Not all features are supported!"
|
||||
msgstr "Neutrino имеет только экспериментальную поддержку. Поддерживаются не все функции!"
|
||||
|
||||
msgid "Enable experimental features"
|
||||
msgstr "Включить экспериментальные функции"
|
||||
|
||||
msgid "Can't Playback!"
|
||||
msgstr "Не удается воспроизвести!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Включить темный режим"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Извлечь..."
|
||||
|
||||
msgid "Unsupported format!"
|
||||
msgstr "Неподдерживаемый формат!"
|
||||
|
||||
msgid "Combine with the current data?"
|
||||
msgstr "Объединить с текущими данными?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "Импорт данных завершен!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Текущий сервис"
|
||||
|
||||
msgid "Open folder"
|
||||
msgstr "Открыть папку"
|
||||
|
||||
msgid "Open archive"
|
||||
msgstr "Открыть архив"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Импорт из сети"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Управление"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Таймеры"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Таймер"
|
||||
|
||||
msgid "Add timer"
|
||||
msgstr "Добавить таймер"
|
||||
|
||||
msgid "Hr."
|
||||
msgstr "ч."
|
||||
|
||||
msgid "Min."
|
||||
msgstr "мин."
|
||||
|
||||
msgid "Power"
|
||||
msgstr "Питание"
|
||||
|
||||
msgid "Standby"
|
||||
msgstr "Режим ожидания"
|
||||
|
||||
msgid "Wake Up"
|
||||
msgstr "Пробуждение"
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Перезагрузка"
|
||||
|
||||
msgid "Restart GUI"
|
||||
msgstr "Перезагрузить графический интерфейс"
|
||||
|
||||
msgid "Shutdown"
|
||||
msgstr "Выключение"
|
||||
|
||||
msgid "Shut down"
|
||||
msgstr "Выключить"
|
||||
|
||||
msgid "Do Nothing"
|
||||
msgstr "Ничего не делать"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Авто"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Сделать скриншот"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Включен:"
|
||||
|
||||
msgid "Name:"
|
||||
msgstr "Имя:"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "Описание:"
|
||||
|
||||
msgid "Service:"
|
||||
msgstr "Сервис:"
|
||||
|
||||
msgid "Service reference:"
|
||||
msgstr "Сервисная ссылка:"
|
||||
|
||||
msgid "Event ID:"
|
||||
msgstr "ID события:"
|
||||
|
||||
msgid "Begins:"
|
||||
msgstr "Начало:"
|
||||
|
||||
msgid "Ends:"
|
||||
msgstr "Окончание:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Повтор:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Действие:"
|
||||
|
||||
msgid "After event:"
|
||||
msgstr "После события:"
|
||||
|
||||
msgid "Location:"
|
||||
msgstr "Расположение:"
|
||||
|
||||
msgid "Mo"
|
||||
msgstr "Пн"
|
||||
|
||||
msgid "Tu"
|
||||
msgstr "Вт"
|
||||
|
||||
msgid "We"
|
||||
msgstr "Ср"
|
||||
|
||||
msgid "Th"
|
||||
msgstr "Чт"
|
||||
|
||||
msgid "Fr"
|
||||
msgstr "Пт"
|
||||
|
||||
msgid "Sa"
|
||||
msgstr "Сб"
|
||||
|
||||
msgid "Su"
|
||||
msgstr "Вс"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Обновление сервисов"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Создать папку"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "FTP-клиент"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "Размер файла слишком велик!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Соединение"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Разъединить"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Размер"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Дата"
|
||||
|
||||
msgid "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 "Подобный сервис уже есть в этом списке!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "Изменен режим воспроизведения!\nПерезапустите программу для применения настроек."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Установите значения TID, NID и пр. имен для правильного именования пиконов!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Обнаружено потоков:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Загрузить пиконы"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Ошибок:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Использовать для воспроизведения потоков:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Шрифт в списках:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Размер пиконов в списках:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Размер логотипа во всплывающих подсказках:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Сохранить как"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Отметить дубликаты"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Загрузить только для выбранного букета"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "Задание отменено!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Выполняется загрузка данных!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Записи"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Справка"
|
||||
|
||||
1271
po/tr/demon-editor.po
Normal file
1271
po/tr/demon-editor.po
Normal file
File diff suppressed because it is too large
Load Diff
1246
po/zh_CN/demon-editor.po
Normal file
1246
po/zh_CN/demon-editor.po
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user